Top > MyPage
 

Spring MVC(2)

Spring MVCフレームワークを使った、ウェブアプリケーションの構築の手順のメモ書きです。

大雑把な流れ

  1. web.xmlでDispatcherServletの設定(最初だけ)
  2. controllerの作成(StrutsにけるAction)
  3. controllerのマッピングの設定(どのURLがどのcontrollerを利用するかを設定ファイルに書き込む。実際には色々な設定の仕方があるが、今回はデフォルト)
  4. ViewResolverの作成。これはあるcontrollerから、jspやらvelocityに遷移させる際に参照する。今回はデフォルト。
  5. velocityファイルの作成。

最初に戻る

いつものHello World

DispatcherServletの設定

  1. web.xmlに以下のservletを書き込む。現在xdocletを使ってweb.xmlを書き出しているので、実際にはsrc/mergeにある servlets.xmlの中に書き込むことになる。
      <servlet>
        <servlet-name>webcms</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
  2. 同様に上記のservletのmappingをweb.xmlに書き込む。ファイル名はsrc/mergeservlet-mappings.xml
      <servlet-mapping>
        <servlet-name>webcms</servlet-name>
        <url-pattern>*.htm</url-pattern>
      </servlet-mapping>

このDispatcherServletはstrutsにおける、ActionServletにあたる。

最初に戻る

ContextLoaderListenerのweb.xmlへの登録。

これは、Springの設定ファイルを読み込ませるためのもの。そのファイル名はwebcms-servlet.xmlとなり、ここのwebcmsは、servlet-nameにあたる。

<listener>
    <listener-class> org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

最初に戻る

controllerの作成

SpringにおけるcontrollerはstrutsにけるActionにあたる(MVCのC-controller)。

適当なパッケージで(ここではcom.chikkun.webcms.test)HelloWorldControllerを作成する。

/*
 * 作成日: 2005/09/13
 *
 */
package com.chikkun.webcms.test;

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

import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;

public class HelloWorldController implements Controller {

  public ModelAndView handleRequest(HttpServletRequest request,
      HttpServletResponse response) throws Exception {
    return new ModelAndView("hello.htm", "message", greeting);
  }

  /**
   * <code>greeting</code> の挨拶のテキスト
   */
  public String greeting;

  /**
   * @param greetig
   */
  public void setGreeting(String g) {
    this.greeting = g;
  }

  /**
   * @param args
   */

}

最初に戻る

controller(HelloWorldController)の登録

上記のWEB-INF/webcms-servlet.xmlに次の設定を書き込む。実際にはsrc/webapps/WEB-INF/webcms-servlet.xmlに書き込んでおけば、mavenがwarファイルへと固めるときにコピーしてくれる。

<bean name="/hello.htm" class="com.chikkun.webcms.test.HelloWorldController">
    <property name="greeting">
        <!-- 日本語は文字化けちゃうよ~ -->
        <value>Hello, Spring MVC!</value>
    </property>
</bean>

最初に戻る

view resolverの設定

デフォルトではJSPなので、velocityを使いたかったら、少々多めにwebcms-servlet.xmlに書き込まなくてはならない。

webcms-servlet.xmlにはview resolverの他に、velocityConfigurerも登録する必要がある(jspの場合はいらない)。

文字化けに相当苦労したけれど、以下のように指定しておけば、今のところ文字化けは出ていない。

    <bean id="velocityConfig"
        class="org.springframework.web.servlet.view.velocity.VelocityConfigurer">
        <property name="resourceLoaderPath">
            <value>WEB-INF/vm/</value>
        </property>
        <property name="configLocation">
            <value>/WEB-INF/velocity.properties</value>
        </property>
    </bean>
    <bean id="viewResolver"
        class="org.springframework.web.servlet.view.velocity.VelocityViewResolver">
        <property name="contentType">
            <value>text/html; charset=Shift_JIS</value>
        </property>
        <property name="cache">
            <value>false</value>
        </property>
        <property name="suffix">
            <value>.vm</value>
        </property>
        <property name="exposeSpringMacroHelpers">
            <value>true</value>
        </property>
        <property name="toolboxConfigLocation">
            <value>/WEB-INF/toolbox.xml</value>
        </property> 
    </bean>
        

最終的にwebcms-servlet.xmlは以下のようになる。

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN"
   "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
    <bean name="/hello.htm" class="com.chikkun.webcms.test.HelloWorldController">
        <property name="greeting">
            <value>Hello, Spring MVC!</value>
        </property>
    </bean>
    <bean id="velocityConfig"
        class="org.springframework.web.servlet.view.velocity.VelocityConfigurer">
        <property name="resourceLoaderPath">
            <value>WEB-INF/vm/</value>
        </property>
        <property name="configLocation">
            <value>/WEB-INF/velocity.properties</value>
        </property>
    </bean>
    <bean id="viewResolver"
        class="org.springframework.web.servlet.view.velocity.VelocityViewResolver">
        <property name="contentType">
            <value>text/html; charset=Shift_JIS</value>
        </property>
        <property name="cache">
            <value>false</value>
        </property>
        <property name="suffix">
            <value>.vm</value>
        </property>
        <property name="exposeSpringMacroHelpers">
            <value>true</value>
        </property>
        <property name="toolboxConfigLocation">
            <value>/WEB-INF/toolbox.xml</value>
        </property> 
    </bean>
</beans>
        

何かと文字化けを起こしてしまった。色々回避する努力をしてみましたが、駄目でした。誰か教えて!!

後日談色々文字化けに苦労したけれど、最終的には、何とかうまくいった。

            <value>text/html; charset=Shift_JIS</value>
        

が良かったみたい。

最初に戻る

velocityファイル(hello.vm)の作成

helloWorldcontrollerの遷移先であるhello.vmの作成

return new ModelAndView("hello.htm", "message", greeting);の最初のhello.htmがhello.vmになるので、WEB-INF/vm/hello.vmを 作成する(実際にはこの指定の仕方は、上記のViewResolverの設定の仕方で色々できるが、とりあえずデフォルトで)。

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=Windows-31J">
<title>Spring MVC test1</title>
</head> 
<html>
<body>
$message
</body>
</html>
                

最初に戻る

前回の落ち穂拾い

流れ

上記の図のように

  1. http://localhost:8080/webcms/hello.htmがリクエストされると、まずはDispatcherServletが呼び出され
  2. 呼び出されたDispatcherServletは、BeanNameHandleMapping(デフォルト)から、controllerを見つけ出します。このBeanNameHandleMapping以外に
    • SimpleUrlHandlerMapping
    • CommonsPathMapHandlerMapping
    がありますが、このうちの1番目のSimpleUrlHandlerMappingを今回使う予定です。
  3. 上記のmappingにより、DispatcherServleは作成したHelloWorldControllerに処理を委譲します。
  4. 委譲されたHelloWorldControllerが処理を実行し、ModelAndViewオブジェクトを返します。このオブジェクトには、 return new ModelAndView("hello.htm", "message", greeting);とあるように ModelAndView("beanの名前->viewの名前","変数の名前",モデルの名前);となります。もう少し詳しく見ると、
    • BeanNameHandleMappingは以下のようにwebcms-servlet.xmlに以下のように定義されていました。
          <bean name="/hello.htm" class="com.chikkun.webcms.test.HelloWorldController">
              <property name="greeting">
                  <value>Hello, Spring MVC!</value>
              </property>
          </bean>
                                  
      これはhttp://localhost:8080/webcms/hello.htmがリクエストされると、com.chikkun.webcms.test.HelloWorldControllerが 呼び出されるということですが、同時に、hello.vmが最後に表示されることを意味します(BeanNameHandleMappingを利用した場合には)。これが "beanの名前->viewの名前"の意味になります。
    • また、上記のwebcms-servlet.xmlの定義で
              <property name="greeting">
                  <value>Hello, Spring MVC!</value>
              </property>
                                  
      は、呼び出されるcom.chikkun.webcms.test.HelloWorldControllerのgreetingというオブジェクトに「Hello, Spring MVC!」が代入される ことを意味します。greetingという変数の値をvelocityのhello.vmの中ではmessageという変数で参照できると言うことになります。ちなみにhello.vmの 中味は
      <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
      <html>
      <head>
      <meta http-equiv="Content-Type" content="text/html; charset=Windows-31J">
      <title>Spring MVC test1</title>
      </head> 
      <html>
      <body>
      $message
      <!--上記!!-->
      </body>
      </html>
                                  
      これがModelAndView("beanの名前->viewの名前","変数の名前",モデルの名前)の後半2つの引数の意味になります。 ちなみに、モデルというのはcontrollerとviewとの間の値の受け渡しをするbeanで、formとcontrollerとの間の値の受け渡しをするbeanを commandと言います。これが結構わかりづらいので、しっかり覚えておく必要があります。
  5. ViewResolverはjspが標準で、今ひとつvelocityの場合はどうなのかというところがわからないのですが、 jspの場合には
    • ResourcebundleViewResolver
    • InternalResourceViewResolver
    などがあり、VelocityViewResolverは上記のInternalResourceViewResolverに相当するそうです。ただこれだとbean名(URL名)とvelocityの名前が1:1になり、 bean名を変更するときや、view(velocityファイル)を兼用する場合には不便なので、もう少々調べる必要があるようです。
コントローラーの種類と継承関係

controllerは上記のように色々ありますが、とりあえず次の4つが使えれば良いと思う。

  1. AbstractController(単にページを見せたいときなど)
  2. MultiActionController(複数のリクエストに対して、それぞれに対応したリクエストの処理を1つに実装する場合、strutsの DispatchActionあたりに近いのかも)
  3. SimpleFormController(フォームからデータを受け取ったり、validateしたりする場合)
  4. AbstractWizardFormController(wizardのように、いくつもの一連の作業が必要な場合にそのフローを制御したい場合)

今回は3番目のSimpleFormControllerを利用してみます。

フォームデータの送信とvalidation

ユーザー名とパスワードを入力するフォームから、送信ボタンをクリックすると、データを取得し、DBに登録し、そして次のページでそれらを表示するような簡単なものを作成する。

ただし、ユーザー名、パスワードは必ず入力する必要があり、ユーザー名は半角英数で4文字以上10文字以下、パスワードは必ず半角アルファベットと半角数字を混ぜて、5文字以上20文字以下とする。

もし上記に反した場合は元のフォームに戻し、そこにエラーメッセージを表示させる。

だいたいの流れは次のよう。

  1. htmlのフォームinput.vmの作成。
  2. 書く処理が成功した場合の繊維先のinputSuccess.vmの作成。
  3. formから受け取ったデータを保持するBeanの作成(Springではコマンドと呼ぶ)
  4. 実際うまくいくかわからないが、上記のBean自体をHibernateの永続オブジェクトにするために、上記のコマンドにXdocletのタグを書き込む。 XdocletのSpring用のタグを参照。
  5. hibernate.cfg.xmlのマッピングに上記のクラスを書き込む。
  6. Antで実際のテーブルが出来るかどうかを確認する。
  7. 上記同様、 webcms-servlet.xmlにbeanの設定を書き込むための、Xdocletのタグを書き込む。同時に、Antのbuild.xmlにもその設定を書き込む。 XdocletのSpring用のタグを参照。
  8. ant schemaexportでDBのテーブルを作成。
  9. Hibernate のDAOを作成。今回は保存と削除のみ。
  10. Springの設定に、DAO等の設定を書き込む。
  11. DatabaseTestCase(Junitの拡張版)でテスト。
  12. ビジネスロジック(データを保存する)クラスUserAndPassEnrollの作成
  13. Spring MVCのControllerを作成(SimpleformController)。
  14. 上記のformに対するValidatorの作成(Commonsではない)。
  15. 上記で利用する、Commmons Validator(Springではない)の設定ファイルを作成。これもXdocletwを使ってみよう。 XdocletのSpring用のタグを参照。
  16. webcms-servlet.xmlにControllerのbeanの作成。こやつもXdocletで自動で。
  17. 上記をJunitでテスト。
  18. tomcatを立ち上げて、最終確認。

formの作成

ごく簡単なWEB-INF/vm/input.vm

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<HTML>
<HEAD>
<META http-equiv="Content-Type" content="text/html; charset=Windows-31J">
<TITLE>ユーザー名とパスワード入力</TITLE>
</HEAD>
<BODY>
<FORM  action="userAndPass.htm" name="user" method="post">
#springBind("command.user")
<font color="red">${status.errorMessage}</font>
<br/>

ユーザー名<INPUT size="20" type="text" maxlength="10" name="user" value="$!status.value">
<br/>
#springBind("command.pass")
<font color="red">${status.errorMessage}</font>
<br/>
パスワード<INPUT size="20" type="password" maxlength="20" name="pass" value="$!status.value">
<INPUT type="submit" name="button" value="送信">
</FORM>
</BODY>
</HTML>
                

Velocityについては簡単に触れると、本来JSPの場合Springタグが用意されているのだが、Velocityでは #springBind("command.pass")で使えるようになる。ここのcommandとは、後述するControllerにおける return new ModelAndView(getSuccessView(), "command", command);の、2番目の引数に一致する (本当に、ちょい自信がない)。それでpassはcommandの中のフィールド名。

こうしておくと、status.errorMessageやstatus.valueで値を取り出せる。statusは固定。

最初に戻る

フォーム送信後のWEB-INF/vm/inputSuccess.vm

これも、極簡単に

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<HTML>
<HEAD>
<META http-equiv="Content-Type" content="text/html; charset=Windows-31J">
<META http-equiv="Content-Style-Type" content="text/css">
<TITLE>ユーザー名とパスワード入力後</TITLE>
</HEAD>
<BODY>
#springBind("command.user")
ユーザー名:${status.value}
<br/>
#springBind("command.pass")
パスワード:${status.value}
</BODY>
</HTML>
                

最初に戻る

formの値を保持するクラス、UserAndPassCommand.classを作成

これは、strutsのActionFormにあたるものだが、POJOであるところが大きく異なる。今回は単に2つの フィールドがあるだけなので、ユーザー名とパスワードのフィールドとgetterとsetterだけのクラス。

/*
 * 作成日: 2005/09/14
 *
 */
package com.chikkun.webcms.test;

import org.apache.commons.lang.builder.ToStringBuilder;
import org.apache.commons.lang.builder.HashCodeBuilder;
import org.apache.commons.lang.builder.EqualsBuilder;

/**
 * @author Chiku Kazuro
 */
public class UserAndPassCommand {

  public Integer id;
  
  /**
   * <code>user</code>はユーザー名。
   */
  public String user;

  /**
   * <code>pass</code> はパスワード。
   */
  public String pass;


  /**
   * デフォルトコンストラクター
   */
  public UserAndPassCommand() {
    super();
  }

  /**
   * おなじみequals()メソッド。
   * @see java.lang.Object#equals(java.lang.Object)
   */
  public boolean equals(final Object other) {
    if (!(other instanceof UserAndPassCommand))
      return false;
    UserAndPassCommand castOther = (UserAndPassCommand) other;
    return new EqualsBuilder().append(id, castOther.id).append(user,
        castOther.user).append(pass, castOther.pass).isEquals();
  }


  /**
   * おなじみhashCode
   * @see java.lang.Object#hashCode()
   */
  public int hashCode() {
    return new HashCodeBuilder().append(id).append(user).append(pass)
        .toHashCode();
  }

  /**
   * おなじみtoString()。idとuserとpass全部を表示。
   * @see java.lang.Object#toString()
   */
  public String toString() {
    return new ToStringBuilder(this).append("id", id).append("user", user)
        .append("pass", pass).toString();
  }

  
  /**
   * @return ID
   */
  public Integer getId() {
    return this.id;
  }

  /**
   * @param id
   */\
  public void setId(Integer id) {
    this.id = id;
  }

  /**
   * @return パスワード
   */
  public String getPass() {
    return this.pass;
  }

  /**
   * @param p
   */
  public void setPass(String p) {
    this.pass = p;
  }

  /**
   * @return user名
   */
  public String getUser() {
    return this.user;
  }

  /**
   * @param ユーザー名
   */
  public void setUser(String u) {
    this.user = u;
  }
}
                

最初に戻る

HibernateのXdocletタグを書き込む。

テーブル名は、クラスの定義の上、フィールドの型やフィールド名はgetterの上に書き込む。上記のソースに書き加えたところだけを引用する。

/**
 * @author Chiku Kazuro
 * 
 * @hibernate.class table="TEST"
 */
public class UserAndPassCommand {
  
  /**
   * @return ID
   * @hibernate.id column="ID" generator-class="native"
   * @hibernate.generator-param name="sequence" value="test_id_seq"
   */
  public Integer getId() {
    return this.id;
  }
  /**
   * @return パスワード
   * @hibernate.property column="PASS" length="64" not-null="true"
   */
  public String getPass() {
    return this.pass;
  }
  /**
   * @return user名
   * @hibernate.property column="NAME" length="64" not-null="true"
   */
  public String getUser() {
    return this.user;
  }
}
        

ここではTESTというテーブルを作成し、フィールドはIDというシークエンス(これはHibernateが自動で設定してくれるので、実際には全くソースには書く必要がない)と PASSという64文字でnot null、NAMEは64文字でnot nullというものを指定してる。

さらに忘れてはいけないのが webcms/src/conf/hibernate.cfg.xmlに次の一行を書き込む。

    <mapping resource="com/chikkun/webcms/test/UserAndPassCommand.hbm.xml"/>
    

これはant hibernateで作成されるマッピングファイルで次の行うテーブルの作成時に作成される。

最初に戻る

Ant でテーブルの作成

ant schemaexportでテーブルを作成する。schemaexportタスクはdepends hibernateでhibernateタグはdepends propertyなので、 それらのタスクも自動で実行されるので、ant schemaexportだけでテーブルが作成される。

確認はwebcms/WEB-INF/database/webcms.logの中を見ることによって確認できる。 ただし、HSQLDBを立ち上げることを忘れないように。

最初に戻る

Springの設定ファイルにUserAndPassCommandのbeanの設定をXdocletを使って書き出す。

先ほどのUserAndPassCommandのソースの中に、今度もXdocletのタグを書き込むことになる。

/**
 * @author Chiku Kazuro
 * 
 * @hibernate.class table="TEST"
 * @spring.bean  name="command"
 */
public class UserAndPassCommand {
        

次にAntのbuild.xmlにもwebcms-servlet.xmlに書き出す部分をを書き足す。少々複雑なのは、現在strutsでもspringを使っており、なおかつ、 定義ファイルが違うので、書き出す元となるソースを限定している。

	  <target name="spring2" description="make webcms-servlet.xml for spring on Test">
	    <delete failonerror="false">
	      <fileset dir="src/webapp/WEB-INF" includes="webcms-servlet.xml"/>
	    </delete>
	  	<taskdef
	        name="springdoclet"
	        classname="xdoclet.modules.spring.SpringDocletTask"
	        classpathref="build.classpath"/>
	    <springdoclet destdir="src/webapp/WEB-INF" mergedir="src">
	      <fileset dir="${src.dir}">
	        <include name="com/chikkun/webcms/test/*.java"/>
	        <include name="com/chikkun/common/validator/*.java"/>
	      </fileset>
	      <springxml destinationFile="webcms-servlet.xml" xmlencoding="${local.encoding}"/>
	    </springdoclet>
	  </target>
        

最初に戻る

DAOの作成

Hibernateを使ってデータのやりとりをするDAOを作成。今回は保存と削除のみ作成することにする。

/*
 * 作成日: 2005/09/25
 *
 */
package com.chikkun.webcms.test;

import org.springframework.dao.DataAccessException;
import org.springframework.orm.hibernate.support.HibernateDaoSupport;

import com.chikkun.webcms.database.UserAlreadyExistException;

public class TestDAO  extends HibernateDaoSupport implements ITestDAO {

  /**
   * @see com.chikkun.webcms.test.ITestDAO#save(com.chikkun.webcms.test.UserAndPassCommand)
   */
  public final void save(final UserAndPassCommand u) throws UserAlreadyExistException {
    try {
      getHibernateTemplate().save(u);
      getHibernateTemplate().flush();
    } catch (DataAccessException err) {
      throw new UserAlreadyExistException(u.getUser()
              + "既に登録されています!", err);
    } catch (Exception ex) {
      ex.printStackTrace();
    }
  }

  /**
   * @see com.chikkun.webcms.test.ITestDAO#delete(com.chikkun.webcms.test.UserAndPassCommand)
   */
  public final void delete(final UserAndPassCommand u) {
    
    getHibernateTemplate().delete(u);
    getHibernateTemplate().flush();
  }

}
        

上記にあるようにgetHibernateTemplate()を使っている。これはsessionの管理をSpringに任せ、かつ、AOPを使おうという意図である。

その為には、次の説のようにSpringの設定が必要になる。

最初に戻る

DAOのSpring用の設定。

    <bean id="testDAO" class="com.chikkun.webcms.test.TestDAO">
        <property name="sessionFactory">
            <ref bean="sessionFactory"/>
        </property>
    </bean>

    <bean id="testService"
        class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
        <property name="transactionManager">
            <ref bean="transactionManager"/>
        </property>
        <property name="target">
            <ref bean="testDAO"/>
        </property>
        <property name="proxyInterfaces">
            <value>com.chikkun.webcms.test.ITestDAO</value>
        </property>

        <property name="transactionAttributes">
            <props>
                <prop key="save*">PROPAGATION_REQUIRED,-UserAlreadyExistException</prop>
                <prop key="delete*">PROPAGATION_REQUIRED</prop>
            </props>
        </property>
        <property name="postInterceptors">
            <list>
            <ref bean="throwsLoggingAdvisor"/>
            <ref bean="loggingInterceptor"/>
            </list>
        </property>
    </bean>
        

上記のように、testDAOを直接使わずに、testServiceで定義してあるものを介して、利用することにする。トランザクションとロギングをAOPで実現している。 ただ

        <property name="proxyInterfaces">
            <value>com.chikkun.webcms.test.ITestDAO</value>
        </property>
        

にあるように、testDAOで直接受けるのではないので、Interfaceでインスタンス(com.chikkun.webcms.test.ITestDAO)を受け取ることになるので、 そのInterfaceを作成しなくてはならない。順番が逆だが、Eclipseのリファクタリングを使うと、とっても簡単。ソース上で右クリックして「Refacto->Extract Ingerface」で あっという間に作成される。出来たのが次のもの。

/*
 * 作成日: 2005/09/25
 *
 */
package com.chikkun.webcms.test;

import com.chikkun.webcms.database.UserAlreadyExistException;

public interface ITestDAO {

  /**
   * データをsaveする。
   * @param u UserAndPassCommand
   * @throws UserAlreadyExistException
   */
  public abstract void save(final UserAndPassCommand u)
      throws UserAlreadyExistException;

  /**
   * データを削除する。
   * @param u
   */
  public abstract void delete(final UserAndPassCommand u);

}

最初に戻る

DatabaseTestCaseを使っての、DBのテスト

DatabaseTestCase(DBUnit)を使って、先ほどのDAOのテストをしてみよう。といっても、よくわかっていないところもあって、若干不安だが・・・・。

まずDBUnitを使うと何がいいかというと、DBは刻々と状況が変わるので、なかなかテストしづらいが、このDBunitを使うと、 現在のDBのデータをバックアップを取っておき、新しいデータに置き換えて、テストをした後に、最後に元に戻すと言うことが、比較的簡単にできる、ということです。 手順としては

  1. データのバックアップ
  2. 自分の用意したデータに変更(必須ではない、と思う)
  3. テスト
  4. バックアップを元に戻す

最初は自分の用意したデータを用意。これはxmlファイルで用意する。形式は単純です。

<?xml version="1.0" encoding="UTF-8"?>
<dataset>
    <test id="1" pass="kazukun" name="sakai"/>
    <test id="2" pass="manabu" name="sato"/>
    <test id="3" pass="masami" name="ono"/>
</dataset>
        

次に、データのバックアップ、データの元に戻すclassを作成する(色々使い回しが出来そうなので)。まずはバックアップ(これもDatabaseTestCaseを利用)

package com.chikkun.webcms.database;

import java.io.FileWriter;
import java.io.IOException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.ResourceBundle;

import org.dbunit.database.DatabaseConnection;
import org.dbunit.database.DatabaseSequenceFilter;
import org.dbunit.database.IDatabaseConnection;
import org.dbunit.dataset.DataSetException;
import org.dbunit.dataset.FilteredDataSet;
import org.dbunit.dataset.IDataSet;
import org.dbunit.dataset.filter.ITableFilter;
import org.dbunit.dataset.xml.FlatXmlDataSet;

/**
 * データエクスポート DBのテーブル -> XMLファイル.
 */
public class ExportXML {
  //環境が変わったらここを変更
  private final static String TARGET_DIR = "WEB-INF/database/";
  private String tableNames[];
  /**
   * defaultのコンストラクタ.
   */
  public ExportXML(String[] tableName) {
    super();
    this.tableNames = tableName;
  }

  /**
   * 与えられたテーブル名を、xmlファイルとして書き出す。
   * tableName.xmlとなる。
   */
  public void exportToXML(){
    // コネクション取得
    try {

      IDatabaseConnection dbConn = getConnection();
      // 任意のテーブル(message)をエクスポート

      System.out.println("<< データExport開始 >>");

      ITableFilter filter = new DatabaseSequenceFilter(dbConn);

      for (int idx = 0; idx < tableNames.length; idx++) {
        System.out.println(tableNames[idx]);
        String[] table = new String[] {tableNames[idx]};
        IDataSet userDataSet = new FilteredDataSet(filter, dbConn
            .createDataSet(table));
        FlatXmlDataSet.write(userDataSet, new FileWriter(TARGET_DIR + tableNames[idx]
            + ".xml"), "Windows-31J");
      }
      System.out.println("<< データExport終了 >>");
    } catch (DataSetException err) {
      err.printStackTrace();
    } catch (SQLException err) {
      err.printStackTrace();
    } catch (IOException err) {
      err.printStackTrace();
    } catch (Exception err) {
      err.printStackTrace();
    }
  }

  /**
   * @param tableNames
   *          テーブルの名前の配列
   * @throws Exception
   *           何らかの例外
   */
  public final void extractTables(final String[] tableNames) throws Exception {
    IDatabaseConnection connection = getConnection();

    for (int i = 0; i < tableNames.length; i++) {
      String tableName = tableNames[i];

      IDataSet partialDataSet = connection
          .createDataSet(new String[] {tableName});
      FlatXmlDataSet.write(partialDataSet, new FileWriter(TARGET_DIR
          + "/" + tableName + ".xml"), "Windows-31J");
    }
  }

  /**
   * @throws Exception
   *           接続できない
   * @return IDatabaseConnection コネクションの生成
   */
  public final IDatabaseConnection getConnection() throws Exception {

    ResourceBundle resourcebundle = ResourceBundle
        .getBundle("Resources/ApplicationResources");
    String url = resourcebundle.getString("database.url");
    String driver = resourcebundle.getString("database.driver");
    String user = "sa";
    String password = "";

    // コネクションの取得
    Class.forName(driver);
    Connection conn = DriverManager.getConnection(url, user, password);
    IDatabaseConnection dbConn = new DatabaseConnection(conn);
    return dbConn;
  }
}
        

次に、データを戻すクラス。

package com.chikkun.webcms.database;

import java.io.FileInputStream;
import java.io.IOException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.ResourceBundle;

import org.dbunit.database.DatabaseConnection;
import org.dbunit.database.IDatabaseConnection;
import org.dbunit.dataset.DataSetException;
import org.dbunit.dataset.IDataSet;
import org.dbunit.dataset.xml.FlatXmlDataSet;
import org.dbunit.operation.DatabaseOperation;

/**
 * バックアップした XML -> DB.
 */
public class ImportXML {
  //保存場所を変えるときはこれを書き換える。
  private final static String TARGET_DIR = "WEB-INF/database/";
  private String tableNames[];

  public ImportXML(String[] tableNames) {
    super();
    this.tableNames = tableNames;
  }

  public void ImportToDB(){
    // コネクション取得
    try {

      IDatabaseConnection dbConn = getConnection();
      // 任意のテーブル(message)をエクスポート

      System.out.println("<< データImport開始 >>");

      for (int idx = 0; idx < tableNames.length; idx++) {
        System.out.println(tableNames[idx]);
        IDataSet dataSet = new FlatXmlDataSet(new FileInputStream(TARGET_DIR + tableNames[idx] + ".xml"));
        DatabaseOperation.CLEAN_INSERT.execute(dbConn, dataSet);
      }

    } catch (DataSetException err) {
      err.printStackTrace();
    } catch (SQLException err) {
      err.printStackTrace();
    } catch (IOException err) {
      err.printStackTrace();
    } catch (Exception err) {
      err.printStackTrace();
    }

    System.out.println("<< データImport終了 >>");
  }
  /**
   * @throws Exception
   *           接続できない
   * @return IDatabaseConnection コネクションの生成
   */
  public final IDatabaseConnection getConnection() throws Exception {

    ResourceBundle resourcebundle = ResourceBundle
        .getBundle("Resources/ApplicationResources");
    String url = resourcebundle.getString("database.url");
    String driver = resourcebundle.getString("database.driver");
    String user = "sa";
    String password = "";

    // コネクションの取得
    Class.forName(driver);
    Connection conn = DriverManager.getConnection(url, user, password);
    IDatabaseConnection dbConn = new DatabaseConnection(conn);
    return dbConn;
  }

}
        

さて、上記2つを使い、TestCaseのソースは次のよう。

/*
 * 作成日: 2005/09/26
 *
 */
package com.chikkun.webcms.database;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ResourceBundle;

import org.dbunit.Assertion;
import org.dbunit.DatabaseTestCase;
import org.dbunit.DatabaseUnitException;
import org.dbunit.database.DatabaseConnection;
import org.dbunit.database.IDatabaseConnection;
import org.dbunit.dataset.DataSetException;
import org.dbunit.dataset.DefaultDataSet;
import org.dbunit.dataset.DefaultTable;
import org.dbunit.dataset.IDataSet;
import org.dbunit.dataset.ITable;
import org.dbunit.dataset.xml.FlatXmlDataSet;
import org.dbunit.operation.DatabaseOperation;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.FileSystemXmlApplicationContext;

import com.chikkun.webcms.test.TestDAO;
import com.chikkun.webcms.test.UserAndPassCommand;

public class TestDatabaseSample extends DatabaseTestCase {

  private TestDAO dao = null;

  public void testInsert() {

    // ステートメントオブジェクトを生成
    UserAndPassCommand command = new UserAndPassCommand();
    
    command.setPass("kazukun");
    command.setUser("chikkun");
    dao.save(command);
    
    command.setPass("kazukun");
    command.setUser("坂井和郎");
    dao.save(command);

    try {
      ITable expectedSample = new FlatXmlDataSet(new FileReader(
          "WEB-INF/database/userAndPassResult.xml")).getTable("test");
      
      IDataSet actualDataSet = getConnection().createDataSet();
      ITable actualSample = actualDataSet.getTable("test");
      // 実際の結果と予測した結果の比較
      Assertion.assertEquals(expectedSample, actualSample);
    } catch (DataSetException err) {
      err.printStackTrace();
    } catch (FileNotFoundException err) {
      err.printStackTrace();
    } catch (IOException err) {
      err.printStackTrace();
    } catch (SQLException err) {
      err.printStackTrace();
    } catch (DatabaseUnitException err) {
      err.printStackTrace();
    } catch (Exception err) {
      err.printStackTrace();
    }
  }

  public static void main(String[] args) {
    junit.textui.TestRunner.run(TestDatabaseSample.class);
  }

  /**
   * @exception Exception
   *              何らかの例外
   * @return dbConn これはHibernateを使うのでいらないか データベースコネクションを作成.
   */
  protected final IDatabaseConnection getConnection() throws Exception {
    ResourceBundle resourcebundle = ResourceBundle
        .getBundle("Resources/ApplicationResources");
    String url = resourcebundle.getString("database.url");
    String driver = resourcebundle.getString("database.driver");

    String user = "sa";
    String password = "";

    // コネクションの取得
    Class.forName(driver);
    Connection conn = DriverManager.getConnection(url, user, password);
    IDatabaseConnection dbConn = new DatabaseConnection(conn);
    return dbConn;
  }

  /**
   * @exception Exception
   *              何らかの例外
   * @return IDataSet データベースの初期化用データを取得
   */
  protected final IDataSet getDataSet() throws Exception {
    IDataSet dataSet = new FlatXmlDataSet(new FileInputStream(
        "WEB-INF/database/userAndPass.xml"));
    return dataSet;
  }

  /**
   * @exception Exception
   *              何らかの例外 テストの最後の処理.
   */
  protected final void tearDown() throws Exception {
    super.tearDown();
    
    ImportXML ix = new ImportXML(new String[]{"test"});
    
    ix.ImportToDB();
  }

  protected final void setUp() throws Exception {

    ExportXML ex = new ExportXML(new String[]{"test"});

    ex.exportToXML();
    super.setUp();
  
    //最初のダミーデータが3つなので、sequenceの値を4にしておく。
    Connection con;
    try {
      Class.forName("org.hsqldb.jdbcDriver");

      con = DriverManager.getConnection("jdbc:hsqldb:hsql://localhost","sa","");
      Statement stmt = con.createStatement();
      String sql =  "ALTER TABLE TEST ALTER COLUMN ID RESTART WITH 4;";//"ALTER SEQUENCE ID RESTART WITH 1;";
      stmt.executeQuery(sql);
      stmt.close();
      con.close();

    } catch (ClassNotFoundException err1) {
      // TODO 自動生成された catch ブロック
      err1.printStackTrace();
    } catch (SQLException err1) {
      // TODO 自動生成された catch ブロック
      err1.printStackTrace();
    }

  }

  public TestDatabaseSample() {
    super();
  }

  public TestDatabaseSample(String arg0) {
    super(arg0); // C:\eclipse\workspace\webcms\src\webapp\WEB-INF\webcms-servlet.xml
    ApplicationContext context = new FileSystemXmlApplicationContext(
        "src/webapp/WEB-INF/webcms-servlet.xml");

    BeanFactory beanFactory = (BeanFactory) context;

    dao = (TestDAO) beanFactory.getBean("testDAO");
  }
}
        

ほとんど、test自体は何もしていませんが、一応テストに通ったので、ビジネスロジックを作成しよう。

最初に戻る

ビジネスロジックのbean作成UserAndPassEnrollの作成

今回はDBに登録するだけ。Xdocletのタグもしっかり書き込む。

/*
 * 作成日: 2005/09/14
 *
 */
package com.chikkun.webcms.test;

/**
 * @author Chiku Kazuro
 * @spring.bean name="enroll"
 * @spring.property
 *  name="dao"
 *  ref="testService"
 */
public class UserAndPassEnroll {

  /**
   * <code>data</code> コマンドクラス
   */
  private UserAndPassCommand data;
  
  private ITestDAO dao;
  
  /**
   * 実際には、ここでデータベースなどに登録する。
   * @param d
   */
  public void enroll(UserAndPassCommand d) {
    data = d;
    
    dao.save(data);
  }

  public ITestDAO getDao() {
    return this.dao;
  }

  public void setDao(ITestDAO dao) {
    this.dao = dao;
  }
}
                

最初に戻る

controllerの作成

Controllerは色々、便利なメソッドが定義しているそうだが、それは今後の改題として、とりあえず簡単なものを作成。

/*
 * 作成日: 2005/09/14
 *
 */
package com.chikkun.webcms.test;

import org.springframework.validation.BindException;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.SimpleFormController;



/**
 * @author Chiku Kazuro
 * @spring.bean
 *  name="/userAndPass.htm"
 * @spring.property
 *   name="formView" value="input"
 * @spring.property
 *   name="successView" value="inputSuccess"
 * @spring.property
 *   name="validator" ref="userAndPassValidator"
 */
public class UserAndPassInputController extends SimpleFormController {

  /**
   * コンストラクターでコマンドクラスを登録
   */
  public UserAndPassInputController() {
    setCommandClass(UserAndPassCommand.class);
  }

  /**
   * メインのメソッド。
   * 
   * @see org.springframework.web.servlet.mvc.SimpleFormController#doSubmitAction(java.lang.Object)
   */
  protected ModelAndView onSubmit(Object command, BindException errors)
      throws Exception {
    UserAndPassCommand upCommand = (UserAndPassCommand) command;
    util.enroll(upCommand);
    return new ModelAndView(getSuccessView(), "command", command);
  }

  /**
   * <code>util</code> は、データの処理をするクラス。<br>
   * 実際にはDBに登録などをするのだろうが、今回はSystem.out.printlnするだけ。
   */
  private UserAndPassEnroll util;

  /**
   * getter
   * 
   * @return util
   */
  public UserAndPassEnroll getUtil() {
    return this.util;
  }

  /**
   * setter
   * 
   * @param u
   */
  public void setUtil(UserAndPassEnroll u) {
    this.util = u;
  }
}
                

最初に戻る

controllerの設定の書き込み

これは上記の、@springタグで書き出してくれるので

ant spring2でおしまい。

最初に戻る

SpringのValidatorの作成(Commonsじゃない!)

前前節の@springタグで書いた

 * @spring.property
 *   name="validator" ref="userAndPassValidator"
 

Validatorを作成します。現在作っているUserAndPassInputControllerSimpleFormController を継承していて、このcontrollerは上記のように設定しておくと、自動でvalidatorを実行し、成功したらinputSuccess、失敗したらinput に飛ばされます(これら2つのresolverの定義はまだ)。

また、GET(つまり最初のリクエストの場合)の場合もinputに飛ばされます。

また、validatorの使い方はここを参考にされたし。

今回はuserはrequiredとmaxlength(最大何文字か)、passはrequiredとminlengthを指定しています(設定はここ)

/*
 * 作成日: 2005/09/20
 *
 */
package com.chikkun.webcms.test;

import org.springframework.validation.Errors;
import org.springframework.validation.Validator;

import com.chikkun.common.validator.ValidateUtils;

/**
 * @author Chiku Kazuro
 * @spring.bean name="userAndPassValidator"
 */
public class UserAndPassValidator implements Validator {

  public boolean supports(Class clazz) {
    return clazz.equals(UserAndPassCommand.class);
  }

  public void validate(Object command, Errors errors) {
    UserAndPassCommand up = (UserAndPassCommand) command;

    ValidateUtils util = new ValidateUtils("command","validator-spring.properties");
    // errors.reject("name",errMessage);

    if(!util.validateField("user",up)){
      errors.rejectValue("user","", util.getErrorMessage("user"));
      System.out.print(util.getErrorMessage("user"));
    }
    if(!util.validateField("pass",up)){
      errors.rejectValue("pass","",util.getErrorMessage("pass"));
      System.out.print(util.getErrorMessage("pass"));
    }

  }
}
        

最初に戻る

Commons Validatorの設定

上記のUserAndPassValidatorで使われている「util.validateField("user",up)」などの使い方はValidatorの使い方を参考してください。

設定の仕方を簡単に書くと、まずはStrutsの場合と同様に、validator.xmlに定義を書きます。ただ、このvalidator.xmlは com.chikkun.common.validator.ValidatorLoader.classと同じ場所に置きます(いずれに変えなきゃ)。

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE form-validation PUBLIC
          "-//Apache Software Foundation//DTD Commons Validator Rules Configuration 1.1.3//EN"
          "http://jakarta.apache.org/commons/dtds/validator_1_1_3.dtd">
<form-validation>
    <formset>
        <form name="command">
            <field property="user" depends="required,maxlength">
                <arg0 key="form.name.displayname"/>
                <arg1 name="maxlength" key="${var:maxlength}"/>
                <var>
                    <var-name>maxlength</var-name>
                    <var-value>12</var-value>
                </var>
            </field>
            <field property="pass" depends="required,minlength">
                <arg0 key="form.pass.displayname"/>
                <arg1 key="${var:minlength}"/>
                <var>
                    <var-name>minlength</var-name>
                    <var-value>5</var-value>
                </var>
            </field>
        </form>
    </formset>
</form-validation>
        

最初に戻る

いよいよ、Junitで確認

Spring MVCのテストには、springでmockを用意してくれている。と思ったら、spring.jarの中には入っていないらしい。

そこで、新しくproject.xmlに書き込む。そしたら

  • maven eclipse(そうするとダウンロードが始まる)
  • eclipseのF5を押す。
  • eclipseのpropertyでbuild pathのチェックマークを入れる。
  <groupId>springframework</groupId>
  <artifactId>spring-mock</artifactId>
  <version>1.2.5</version>
  <type>jar</type>
      <properties>
        <cactus.bundle>true</cactus.bundle>
      </properties>
     </dependency>
        

今回は簡単なチェック。

/*
 * 作成日: 2005/09/27
 *
 */
package com.chikkun.webcms.test;

import junit.framework.TestCase;

import org.springframework.beans.factory.BeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.FileSystemXmlApplicationContext;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.validation.BindException;
import org.springframework.validation.Errors;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;

public class UserAndPassMockTest extends TestCase {

  private Controller controller;
  
  public static void main(String[] args) {
    junit.textui.TestRunner.run(UserAndPassMockTest.class);
  }

  public UserAndPassMockTest(String arg0) {
    super(arg0);
  }
  //成功の場合
 public void testOnSubmitSuccess(){
    MockHttpServletRequest req = new MockHttpServletRequest("POST","userAndPass.htm");
    req.addParameter("user","sakai");
    req.addParameter("pass","kazukunro");
    ModelAndView mv = null;
    try {
      mv = controller.handleRequest(req,new MockHttpServletResponse());
    } catch (Exception err) {
      err.printStackTrace();
    }
    //"command"はbeanの名前(と思う)
    Errors errors = (Errors) mv.getModel().get( BindException.ERROR_KEY_PREFIX + "command");
    assertNull("Errors",errors);
    
    //遷移先のチェック
    assertEquals("viewName","inputSuccess",mv.getViewName());
    
    }

 //失敗の場合
 public void testOnSubmitFailure(){
   MockHttpServletRequest req = new MockHttpServletRequest("POST","userAndPass.htm");
   req.addParameter("user","sakai");
   //パスワードは5文字以上
   req.addParameter("pass","kazu");
   ModelAndView mv = null;
   try {
     mv = controller.handleRequest(req,new MockHttpServletResponse());
   } catch (Exception err) {
     err.printStackTrace();
   }
   
   Errors errors = (Errors) mv.getModel().get( BindException.ERROR_KEY_PREFIX + "command");
   assertNotNull("Errors",errors);
   
   //エラーmessageのチェック
   assertEquals("message","パスワードは5文字以上にしてください。",errors.getFieldError("pass").getDefaultMessage());

   //遷移先のチェック
   assertEquals("viewName","input",mv.getViewName());
   }

  protected void setUp() throws Exception {
    ApplicationContext context = new FileSystemXmlApplicationContext(
    "src/webapp/WEB-INF/webcms-servlet.xml");

    BeanFactory beanFactory = (BeanFactory) context;
    controller = (Controller) beanFactory.getBean("/userAndPass.htm");
    
    super.setUp();
  }

  protected void tearDown() throws Exception {
    super.tearDown();
  }
}
        

やった~。通ったぞ!!

最初に戻る

一応確認

  1. http://localhost:8080/webcms/userAndPass.htmにアクセスする。
  2. 何も入れないと、赤いエラーメッセージが加わった、入力画面に戻る。
  3. userに「sakai」を入力し、passに「baka(5文字以上でないとだめ)」を入れると、同様にエラーメッセージを表示して、また入力画面に戻る。
  4. userに「sakai」を入力し、passに「kazuro」を入れると、入力したデータを表示して、おしまい。

最初に戻る

落ち穂拾い

本に出ていたprocessFormSubmissionというSimpleFormControllerのメソッドを入れてみよう。

SimpleFormControllerのJavadocはSimpleFormControllerにあります。

もともと、validatorなどで、エラーが起こった場合呼び出されるメソッドで、 This implementation calls showForm in case of errors, and delegates to onSubmit's full version else.ということでなので、onSubmitの代わりにこのメソッドが委譲されると言うことだろう。

今回は、input.vmにキャンセルボタンをつけて、このボタンが押されたら、「キャンセルされました」という表示をさせてみよう。

まずはその表示用のvm(cancel.vm)の作成。

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=Windows-31J">
<title>キャンセル</title>
</head>
<body>

キャンセルされました。

<a href="userAndPass.htm">もとに戻る</a>
</body>
        

さらに以前からあるinput.vmにキャンセルボタンを追加。

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<HTML>
<HEAD>
<META http-equiv="Content-Type" content="text/html; charset=Windows-31J">
<TITLE>ユーザー名とパスワード入力</TITLE>
</HEAD>
<BODY>
<FORM  action="userAndPass.htm" name="user" method="post">
#springBind("command.user")
<font color="red">${status.errorMessage}</font>
<br/>

ユーザー名<INPUT size="20" type="text" maxlength="10" name="user" value="$!status.value">
<br/>
#springBind("command.pass")
<font color="red">${status.errorMessage}</font>
<br/>
パスワード<INPUT size="20" type="password" maxlength="20" name="pass" value="$!status.value">
<br/>
<INPUT type="submit" name="button" value="送信"><input type="submit" name="cancel" value="キャンセル">
</FORM>
</BODY>
</HTML>
        

そして、次にControllerであるUserAndPassInputControllerにprocessFormSubmissionを書き込んでみよう。

/*
 * 作成日: 2005/09/14
 *
 */
package com.chikkun.webcms.test;

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

import org.springframework.validation.BindException;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.SimpleFormController;
import org.springframework.web.util.WebUtils;

/**
 * @author Chiku Kazuro
 * @spring.bean name="/userAndPass.htm"
 * @spring.property name="formView" value="input"
 * @spring.property name="successView" value="inputSuccess"
 * @spring.property name="validator" ref="userAndPassValidator"
 * @spring.property name="util" ref="enroll"
 */
public class UserAndPassInputController extends SimpleFormController {

  /**
   * <code>util</code> は、データの処理をするクラス。<br>
   * 実際にはDBに登録などをするのだろうが、今回はSystem.out.printlnするだけ。
   */
  private UserAndPassEnroll util;

  
  /**
   * コンストラクターでコマンドクラスを登録
   */
  public UserAndPassInputController() {
    setCommandClass(UserAndPassCommand.class);
  }

  /**
   * メインのメソッド。
   * 
   * @see org.springframework.web.servlet.mvc.SimpleFormController#doSubmitAction(java.lang.Object)
   */
  protected ModelAndView onSubmit(Object command, BindException errors)
      throws Exception {
    UserAndPassCommand upCommand = (UserAndPassCommand) command;
    util.enroll(upCommand);
    return new ModelAndView(getSuccessView(), "command", command);
  }

  protected ModelAndView processFormSubmission(HttpServletRequest request,
      HttpServletResponse response, Object command, BindException errors) throws Exception{
    if(WebUtils.hasSubmitParameter(request,"cancel")){
      return new ModelAndView("cancel");
    }
    return super.processFormSubmission(request, response, command, errors);
  }

  /**
   * getter
   * 
   * @return util
   */
  public UserAndPassEnroll getUtil() {
    return this.util;
  }

  /**
   * setter
   * 
   * @param u
   */
  public void setUtil(UserAndPassEnroll u) {
    this.util = u;
  }
}
        

すんなりうまくいった。strutsの場合はLookupDispatchActionを使って、ボタンごとにメソッドを変える方法でやっていたけど、この方が簡単だ!

最初に戻る

同じユーザ名は登録できないようにする

実際にはTESTというテーブルのuserというフィールドはunique属性をつけてないが、通常はやはりuniqueじゃなきゃいけないので、 validatorで行うか、先ほどのメソッドprocessFormSubmissionで行うかのどちらかだけれど、今回は後者でやってみよう。

まずは、processFormSubmissionを少し書き換える。

/*
 * 作成日: 2005/09/14
 *
 */
package com.chikkun.webcms.test;

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

import org.springframework.validation.BindException;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.SimpleFormController;
import org.springframework.web.util.WebUtils;

/**
 * @author Chiku Kazuro
 * @spring.bean name="/userAndPass.htm"
 * @spring.property name="formView" value="input"
 * @spring.property name="successView" value="inputSuccess"
 * @spring.property name="validator" ref="userAndPassValidator"
 * @spring.property name="util" ref="enroll"
 * @spring.property name="dao" ref="testService"
 */
public class UserAndPassInputController extends SimpleFormController {

  /**
   * <code>util</code> は、データの処理をするクラス。<br>
   * 実際にはDBに登録などをするのだろうが、今回はSystem.out.printlnするだけ。
   */
  private UserAndPassEnroll util;

  private ITestDAO dao;

  /**
   * コンストラクターでコマンドクラスを登録
   */
  public UserAndPassInputController() {
    setCommandClass(UserAndPassCommand.class);
  }

  /**
   * メインのメソッド。
   * 
   * @see org.springframework.web.servlet.mvc.SimpleFormController#doSubmitAction(java.lang.Object)
   */
  protected ModelAndView onSubmit(Object command, BindException errors)
      throws Exception {
    UserAndPassCommand upCommand = (UserAndPassCommand) command;
    util.enroll(upCommand);
    return new ModelAndView(getSuccessView(), "command", command);
  }

  protected ModelAndView processFormSubmission(HttpServletRequest request,
      HttpServletResponse response, Object command, BindException errors)
      throws Exception {
    UserAndPassCommand upCommand = (UserAndPassCommand) command;
    if (WebUtils.hasSubmitParameter(request, "cancel")) {
      return new ModelAndView("cancel");
    }
    if(dao.isExist(upCommand)){
      errors.rejectValue("user","","そのユーザー名は登録されています");
    }
    return super.processFormSubmission(request, response, command, errors);
  }

  /**
   * getter
   * 
   * @return util
   */
  public UserAndPassEnroll getUtil() {
    return this.util;
  }

  /**
   * setter
   * 
   * @param u
   */
  public void setUtil(UserAndPassEnroll u) {
    this.util = u;
  }

  public ITestDAO getDao() {
    return this.dao;
  }

  public void setDao(ITestDAO dao) {
    this.dao = dao;
  }
}
        

processFormSubmissionメソッドに追加したと同時に、xdocletのタグを@spring.property name="dao" ref="testService"を追加した。

うんちょっと詰まったけど、まあくまくいった。

最初に戻る

SpringWebFlow

まずは結論から言うと、Velocityでなんとかいこうと半日すったもんだしたが、結局うまくいかず(springのタグが使えない)、結局jspで行くことにした 。 したがて、相当書き直す羽目になった(--;)。

その上、jspとvmの混在がなかなかできなくて、相当苦労した。やれやれ。さて・・・・。

formがいくつかあって、それらで入力したデータをDBに登録するような場合(Wizard)、いろいろと面倒なことがあります。

例えば、1つ1つのアクションが違うURLにマッピングされているような場合、途中から始められると困るだとか、フロー自体が途中で分岐するなどの 複雑なことをすると、色々考えなければならないことがたくさん出てきます。

そこでSpringに標準で付いているAbstractWizardFormControllerを利用する方法もあるが、この際だからということで、 Spring Web Flow なるものを使おうかと思います(ただし、あとで検索した際のページには本家のspringWebFlowも結構いけているらしい(-.-)。

ただ複雑なものを作るのは億劫なので、次のようなものを考えます。

SpringWebFlowのフロー

概要

けっこう話しが面倒なので、自分の頭の整理もかねて、概要を記す。

  1. 使用するクラス
    • com.ervacon.springframework.web.servlet.mvc.webflow.BindAndValidateCommandAction

      フォームからのデータをBindしvalidateするクラス。これは元々すでにあるもので、webcms-servlet.xmlのmergeファイルに定義だけ書く。 アプリケーションを呼びだれたとき、まず呼び出される。

      このクラスは設定ファイルで、データを入れる「コマンドクラス」とvalidateする「validatorクラス」を指定する。

              <bean id="bindAndValidateUandPAction"
                  class="com.ervacon.springframework.web.servlet.mvc.webflow.BindAndValidateCommandAction" singleton="false">
                  <property name="commandName"><value>command2</value></property>
              <property name="commandClass"><value>com.chikkun.webcms.test.Test2CommandBean</value></property>
      <property name="validator"><ref
                          bean="firstValidator"/></property>
              </bean>
                      
    • Validatorクラス。

      上記のBindAndValidateCommandActionでは、validatorを指定するので、それを作成。

    • 自分で作成するActionクラス。MVCのM。これはcom.ervacon.springframework.web.servlet.mvc.webflow.Actionをimplements する必要がある。

      public final String execute(final HttpServletRequest request,final HttpServletResponse response, final Map model)を定義し、 必要な処理をこのメソッドに書く。また戻り値はreturn "ok"などの文字列で、この戻り値で、後で説明するwebflowの定義で、処理を分岐する。

    • あとは自分のやりたいビジネスロジックを記述するBean。
  2. 設定ファイル
    • webcms-servlet.xml
      • 自分で作成するクラスはXdocletの@springタグを用いて書き出させる。
      • BindAndValidateCommandActionのような用意されているものを使う場合はsrc/spring-bean.xmlに書き、mergeさせる。
    • input-flow.xml

      flowの定義を書く。これが一番重要!

最初に戻る

input-flow.xml

これが一番重要なところですが、まずは全部を表示して、その後細かく説明する。

<?xml version="1.0" encoding="Windows-31J"?>
<!DOCTYPE web-flow PUBLIC "-//ERVACON//DTD SPRING WEB FLOW//EN"
                           "web-flow.dtd">
<web-flow name="inputFlow">
	<start-state state="userAndPass"/>
	<view-state id="userAndPass" view="inputUandP">
		<transition name="fromUandP" to="bindAndValidateUandP"/>
	</view-state>
	<action-state id="bindAndValidateUandP">
		<action name="bindAndValidateAct1" bean="bindAndValidateUandPAction"/>
		<transition name="bindAndValidateAct1.ok" to="UandPAction"/>
		<transition name="bindAndValidateAct1.error" to="userAndPass"/>
	</action-state>
	<action-state id="UandPAction">
		<action name="firstAction" bean="firstAction"/>
		<transition name="firstAction.ok" to="ageAndSex"/>
	</action-state>
	<view-state id="ageAndSex" view="inputAandS">
		<transition name="fromAandS" to="bindAndValidateAandS"/>
		<transition name="previous" to="userAndPass"/>
	</view-state>
	<action-state id="bindAndValidateAandS">
		<action name="bindAndValidateAct2" bean="bindAndValidateAandSAction"/>
		<transition name="bindAndValidateAct2.ok" to="AandSAction"/>
		<transition name="bindAndValidateAct2.error" to="ageAndSex"/>
	</action-state>
	<action-state id="AandSAction">
		<action name="secondAction" bean="secondAction"/>
		<transition name="secondAction.ok" to="mailAndAddress"/>
	</action-state>
	<view-state id="mailAndAddress" view="inputMandA">
		<transition name="fromMandA" to="bindAndValidateMandA"/>
		<transition name="previous" to="ageAndSex"/>
	</view-state>
	<action-state id="bindAndValidateMandA">
		<action name="bindAndValidateAct3" bean="bindAndValidateMandAAction"/>
		<transition name="bindAndValidateAct3.ok" to="MandAAction"/>
		<transition name="bindAndValidateAct3.error" to="mailAndAddress"/>
	</action-state>
	<action-state id="MandAAction">
		<action name="thirdAction" bean="thirdAction"/>
		<transition name="thirdAction.ok" to="confirm"/>
	</action-state>
	<view-state id="confirm" view="inputConfirm">
		<transition name="pre" to="mailAndAddress"/>
		<transition name="prepre" to="ageAndSex"/>
		<transition name="preprepre" to="userAndPass"/>
		<transition name="save" to="bindAndValidateAll"/>
	</view-state>
	<action-state id="bindAndValidateAll">
		<action name="bindAndValidateAct4" bean="bindAndValidateAllAction"/>
		<transition name="bindAndValidateAct4.ok" to="AllAction"/>
		<transition name="bindAndValidateAct4.error" to="confirm"/>
	</action-state>
	<action-state id="AllAction">
		<action name="lastAction" bean="lastAction"/>
		<transition name="lastAction.ok" to="end"/>
	</action-state>
	<end-state id="end" view="inputComplete"/>
</web-flow>
        

まずはxmlのルートはweb-flowでこの中に定義を書いていく。属性のnameは名前で、特に他とは関わらない模様(何でもOK)。

そして、次に書き込むのは(僕が知っている範囲では---subflowはまだわからん)、4つのstateを書くことになる。

  1. view-state
  2. action-state
  3. end-state

の4つだが、まずはstart-state。

  1. start-state
    <start-state state="userAndPass"/>
                

    属性のstateで指定したものから始めるという意味。今回はuserAndPassという(実際にはidで指定)次のview-stateに飛べ、ということになる。

  2. view-state
    <view-state id="userAndPass" view="inputUandP">
       <transition name="fromUandP" to="bindAndValidateUandP"/>
    </view-state>
    

    start-stateでuserAndPassが指定してあるので、ここのidがそれなので、飛んでくる。

    そして、ブラウザに見せるviewがinputUandPであることを意味している。ただこれが実際にはどのファイルになるかは、view resolverの設定で決まってくるが、 今回では、後に示すように、デフォルトはvmだが、そこにない場合はjspとなるようにしてある(したがって同名のvmがあるとまずい)。

    そして、jspの中のhiddenの以下のような値_eventの値がfromUandPがあり、submitボタンで実行されたらbindAndValidateUandPという処理へ行け、 ということになる。

    <INPUT type="hidden" name="_event" value="fromUandP">
    
  3. そして、前項で飛ばされてきたbindAndValidateUandPだが、先のview-stateでJspなりVelocityなりを表示させて、 フォームを実行させてhiddenの_evenの内容で次の処理に分岐させるものだが、今回は、実際の処理にはいる。

    ただbindAndValidateUandPは、自分で作成するクラスではなく、 もともと用意されているクラスで(com.ervacon.springframework.web.servlet.mvc.webflow.BindAndValidateCommandAction)、それをSpringのBeanの定義のところに 書いておくわけだが、このクラスがやることは

    • フォームの値をコマンド(どうもこの「コマンド」という言葉に慣れないな。要するにデータを入れておくただのJava Bean)にBindする
    • フォームの値の検証(必須ではない)

    の2種類で、上記の2種類の処理で全くエラーが起こらなかったらokを、エラーが起こったらerrorを文字列で返してくる。

                        
    	<action-state id="bindAndValidateUandP">
    		<action name="bindAndValidateAct1" bean="bindAndValidateUandPAction"/>
    		<transition name="bindAndValidateAct1.ok" to="UandPAction"/>
    		<transition name="bindAndValidateAct1.error" to="userAndPass"/>
    	</action-state>                    
                    

    上記の設定で言えば、

                        <action name="bindAndValidateAct1" bean="bindAndValidateUandPAction"/>
                    

    赤いところがActionのBeanの名前になっているが、これが次のbindAndValidateAct1.okのドットの左側になり、右側のokは前述したように、返ってきた文字列になる。 つまり、データのBindもValidatorもokだったら、ということになる。

    次に、黄色いbindAndValidateUandPAction方だが、これが実際のSpringにおけるBeanの定義名で、webcms-servlet.xmlに書き込む(実際にはmergeファイルのspring-beans.xml)。 実際には、以下のように定義してある。

    コマンドクラス(実際のソース)の指定と、validator(実際のソース)クラスの指定を行っている。

                        
            <bean id="bindAndValidateUandPAction"
                class="com.ervacon.springframework.web.servlet.mvc.webflow.BindAndValidateCommandAction" singleton="false">
                <property name="commandName"><value>command2</value></property>
            <property name="commandClass"><value>com.chikkun.webcms.test.Test2CommandBean</value></property>
            <property name="validator"><ref bean="firstValidator"/></property>
            </bean>
                        
                    
  4. BindとValidateがうまくいったら、次に進む。次が実際のActionになる。

                        
    	<action-state id="UandPAction">
    		<action name="firstAction" bean="firstAction"/>
    		<transition name="firstAction.ok" to="ageAndSex"/>
    	</action-state>                    
                    

    前項同様に、

                        <action name="firstAction" bean="firstAction"/>
                    

    赤色が、ActionのBeanの名前で、黄色がそのSpringのBeanの定義名である。そして、赤のActionのBean名も同様に、このクラスのメソッド(execute)で 返される文字列によって分岐するように設定する。今回は1方向にしか進めないようにしてあるのでokだけだ(ageAndSex)。

                        <transition name="firstAction.ok" to="ageAndSex"/>
                    
    

    また、このクラスは自分で作る必要があり、最初に述べたようにcom.ervacon.springframework.web.servlet.mvc.webflow.Actionをimplemtnsした ものだ。そして、FirstAction.java.htmlにあるように

    public final String execute(final HttpServletRequest request, final HttpServletResponse response, final Map model)

    を定義して、

    return "ok";

    で文字列を返すが、okでなくてもかまわない。また、条件分岐で色々な文字列を返し、flowの定義で分岐することも可能だ。

さて、上記以外に多少コメントすると、


	<view-state id="confirm" view="inputConfirm">
		<transition name="pre" to="mailAndAddress"/>
		<transition name="prepre" to="ageAndSex"/>
		<transition name="preprepre" to="userAndPass"/>
		<transition name="save" to="bindAndValidateAll"/>
		          

で、フォームからpreやprepre、preprepre、saveがいつもの_eventで返ってきた場合、preは前のmailAndAdress、prepreだったら前の前のageAndSexに飛ぶようにしてある。 もちろん、本来の処理のsaveだったら最後の処理へと飛ぶわけだ。

実際のJSPは次のよう。

              
<FORM name="inputForm" action="inputFlow.htm" method="post"><INPUT
    type="hidden" name="_flowId"
    value="<%=request.getAttribute("flowId") %>">

     <INPUT type="hidden" name="_event" value="save">

     <INPUT type="hidden"
    name="_currentState" value="confirm"> <input type="submit"
    name="previous" value="登録"> <INPUT type="hidden" name="user"
    value="<c:out value="${user}"/>"> <INPUT type="hidden" name="pass"
    value="<c:out value="${pass}"/>"> <INPUT type="hidden" name="age"
    value="<c:out value="${age}"/>"> <INPUT type="hidden" name="sex"
    value="<c:out value="${sex}"/>"> <INPUT type="hidden" name="mail"
    value="<c:out value="${mail}"/>"> <INPUT type="hidden"
    name="address" value="<c:out value="${address}"/>"></form>
</br>

<FORM name="inputForm" action="inputFlow.htm" method="post"><INPUT
    type="hidden" name="_flowId"
    value="<%=request.getAttribute("flowId") %>">
    
     <INPUT type="hidden" name="_event" value="pre">
         
     <INPUT type="hidden" name="_currentState"
    value="confirm"> <input type="submit" name="pre" value="1つ前"></form>
<FORM name="inputForm" action="inputFlow.htm" method="post"><INPUT
    type="hidden" name="_flowId"
    value="<%=request.getAttribute("flowId") %>">
    <INPUT type="hidden" name="_event" value="prepre">
    <INPUT type="hidden"
    name="_currentState" value="confirm"> <input type="submit"
    name="prepre" value="2つ前"></form>
<FORM name="inputForm" action="inputFlow.htm" method="post"><INPUT
    type="hidden" name="_flowId"
    value="<%=request.getAttribute("flowId") %>">
    
    <INPUT type="hidden" name="_event" value="preprepre">
        
    <INPUT type="hidden"
    name="_currentState" value="confirm"> <input type="submit"
    name="preprepre" value="3つ前"></form>              
          

さらにもう1つ。

              
	<end-state id="end" view="inputComplete"/>
	              
          

これは最後のstateになるわけだが、必須だというわけでない。今回は使っているので、ここでたぶん、_flowIdあたるが消されてしまうのだと思う。

というのも最後に、データをDBに登録して、リダイレクトしてJSPを表示させているが、戻るボタンで戻ろうとすると、例外が発生している。end-stateにしなければ、大丈夫かもしれない(未確認)。 ただ、反対に二重登録されないような方法を講じなくてはならないだろう。

最初に戻る

webcms-servlet.xml

何度も言うように、実際はwebcms-servlet.xmlに直接ではなく、spring-beans.xmlに書く事になる。

                
	<bean id="search" name="/inputFlow.htm" class="com.ervacon.springframework.web.servlet.mvc.webflow.WebFlowController">
	    <property name="cacheSeconds"><value>0</value></property>
	    <property name="webFlowName"><value>inputFlow</value></property>
	</bean>
        <bean id="inputFlow" class="com.ervacon.springframework.web.servlet.mvc.webflow.SimpleWebFlow">
            <property name="webFlowResource">
                <value>/WEB-INF/input-flow.xml</value>
            </property>
        </bean>

        <bean id="bindAndValidateUandPAction"
            class="com.ervacon.springframework.web.servlet.mvc.webflow.BindAndValidateCommandAction" singleton="false">
            <property name="commandName"><value>command2</value></property>
        <property name="commandClass"><value>com.chikkun.webcms.test.Test2CommandBean</value></property>
        <property name="validator"><ref bean="firstValidator"/></property>
        </bean>

        <bean id="bindAndValidateAandSAction"
            class="com.ervacon.springframework.web.servlet.mvc.webflow.BindAndValidateCommandAction" singleton="false">
            <property name="commandName"><value>command2</value></property>
        <property name="commandClass"><value>com.chikkun.webcms.test.Test2CommandBean</value></property>
        <property name="validator"><ref bean="secondValidator"/></property>
        </bean>

        <bean id="bindAndValidateMandAAction"
            class="com.ervacon.springframework.web.servlet.mvc.webflow.BindAndValidateCommandAction" singleton="false">
            <property name="commandName"><value>command2</value></property>
        <property name="commandClass"><value>com.chikkun.webcms.test.Test2CommandBean</value></property>
        <property name="validator"><ref bean="thirdValidator"/></property>
        </bean>


        <bean id="bindAndValidateAllAction"
            class="com.ervacon.springframework.web.servlet.mvc.webflow.BindAndValidateCommandAction" singleton="false">
            <property name="commandName"><value>command2</value></property>
        <property name="commandClass"><value>com.chikkun.webcms.test.Test2CommandBean</value></property>
        <property name="validator"><ref bean="lastValidator"/></property>
        </bean>


        <bean id="exceptionResolver"
           class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
        <property name="exceptionMappings">
          <props>
              <prop key="org.springframework.web.servlet.support.SessionRequiredException">index-webflow</prop>
          </props>
        </property>
        </bean>                
            

基本的に書かなければいけないのは次の4+α

  1. com.ervacon.springframework.web.servlet.mvc.webflow.WebFlowControllerのBeanの設定。

    ここでは、キャッシュの時間と、flowの名前をpropertyとして指定している。

     <property name="cacheSeconds"><value>0</value></property>
    <property name="webFlowName"><value>inputFlow</value></property>
                        
  2. com.ervacon.springframework.web.servlet.mvc.webflow.SimpleWebFlowのBeanの設定。

    ここでは、flowの設定ファイル名を指定している。

    <property name="webFlowResource">
        <value>/WEB-INF/input-flow.xml</value>
    </property>
                        
  3. com.ervacon.springframework.web.servlet.mvc.webflow.BindAndValidateCommandActionのBeanの設定。

    上記のflowの定義のところで触れたもので、

            <bean id="bindAndValidateUandPAction"
                class="com.ervacon.springframework.web.servlet.mvc.webflow.BindAndValidateCommandAction" singleton="false">
                <property name="commandName"><value>command2</value></property>
            <property name="commandClass"><value>com.chikkun.webcms.test.Test2CommandBean</value></property>
            <property name="validator"><ref bean="firstValidator"/></property>
            </bean>
                        

    というように、コマンドクラスとvalidatorクラスを指定している。

  4. 上記の定義に直接書くのではなく、xdocletを使って、自動で書き出さなければならないのに、先ほどのActionValidator、 その他Actionなどの中で使うビジネスロジックのbeanなどがある。

    /**
     * @author Chiku Kazuro
     * @spring.bean name="lastValidator"
     * @spring.property name="dao" ref="test2Service"
     */
    public final class LastValidator implements Validator
                        

    のように書く。

  5. +αのorg.springframework.web.servlet.handler.SimpleMappingExceptionResolverの設定。

    これは重要なことだが、このflowが始まる前にsessionが始まっていないと例外が出てしまう。

    そこで、今回はあまりうまい方法ではないが(いくつもflowがあったらどうしよう?)、org.springframework.web.servlet.support.SessionRequiredException が投げられたら、index-webflow(resolverで定義)に飛ばすようにしている(その中で、sessionを始めている)。ログインする必要のあるような場合は問題がないが、そうでない場合は 何らかの方法を考えなくてはならない。実際には

    <%@ page session="true" %>
                        

    をJSPの中で書いておくだけでOKなんだが。

            <bean id="exceptionResolver"
               class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
            <property name="exceptionMappings">
              <props>
                  <prop key="org.springframework.web.servlet.support.SessionRequiredException">index-webflow</prop>
              </props>
            </property>
            </bean>                    

最初に戻る

view resolver

今回はvelocityとjspの混在になってしまったので、resolverを少々変えた。

                
    <bean id="bundleViewResolver" class="org.springframework.web.servlet.view.ResourceBundleViewResolver">
        <property name="basename"><value>views</value></property>
     </bean>
                
            

これはview.propertiesに定義を書いておいて、view名を実際のファイルとマッピングします。この view.propertiesは、

<property name="basename"><value>views</value></property>
            

viewがそれで、拡張子のpropertiesは省略。その定義の書き方だが

inputUandP.class=org.springframework.web.servlet.view.JstlView
inputUandP.url=/WEB-INF/jsp/inputUandP.jsp

inputAandS.class=org.springframework.web.servlet.view.JstlView
inputAandS.url=/WEB-INF/jsp/inputAandS.jsp

inputComplete.class=org.springframework.web.servlet.view.RedirectView
inputComplete.url=/webcms/test2Redirect.htm

#test2Redirect.class=org.springframework.web.servlet.view.JstlView
#test2Redirect.url=/WEB-INF/jsp/inputComplete.jsp

inputMandA.class=org.springframework.web.servlet.view.JstlView
inputMandA.url=/WEB-INF/jsp/inputMandA.jsp

inputConfirm.class=org.springframework.web.servlet.view.JstlView
inputConfirm.url=/WEB-INF/jsp/inputConfirm.jsp

index-webflow.class=org.springframework.web.servlet.view.JstlView
index-webflow.url=/index-webflow.jsp
            

のように書き、2行ペアで1つのviewのマッピングを書く。

  • inputUandP.classで拡張子の左側がview名、右側がそれを表示するViewクラスの名前。
  • inputUandP.urlの左側は同じくview名、右側はその実際のファイル名。

最初に戻る

Jspのポイント

JSPはポイントがいくつかある。

  1. springのカスタムタグ
    • spring:hasBindErrors
      <spring:hasBindErrors name="test2">
      <FONT color="red">エラーです。</FONT>
      </spring:hasBindErrors>
      
                      

      は、requestの中にErrorsオブジェクトがあったらという意味。

    • spring:bind
                                  
       <spring:bind path="command2.user">
                                    
                              

      はrequestにあるコマンドクラスの中のuserというフィールドにBindする意味だが、

      ${status.value}でそのフィールドの値を取り出せ、

      ${status.errorMessage}では、validatorで設定したエラーメッセージを取り出せる。

  2. JSTL

    これはここあたりで、僕自身勉強するしかないのだが (なんせ今までvelocityが主だったし、strutsではstrutsのタグを使っていたんで)、ポイントは

                        
    <INPUT type="text" name="user" value='
    <c:if test="${user != ''}"><c:out value="${user}"/></c:if>
    <c:if test="${status.value != ''}"><c:out value="${status.value}"/></c:if>
    '>                    
                    

    で、$user$status.valueの違いだが、$userはActionの中で、例えば

      public final String execute(final HttpServletRequest request,
          final HttpServletResponse response, final Map model) {
        command2 = (Test2CommandBean) model.get("command2");
        model.put("age", command2.getAge());
        model.put("sex", command2.getSex());
        return "ok";
      }
      

    で、引数のMap modelに値を入れておいたものを、たぶん、取得するもの。一方、$status.valueはコマンドクラスの中の spring:bindでバインドされたフィールドの値を取り出せる。

    何が違うかというと、フォームの送信後、validatorしたあとエラーの場合は、先ほどのmapに値を入れるActionまで行かないので、値はコマンドクラスの中だけにあるので、 $status.valueでしか受け取れない。しかし一方、前のJSPから「ブラウザの戻るボタンじゃない」戻るで戻った場合には、コマンドクラスには値が入っていないので (Actionクラスを実行していないので)、Map modelからデータを取得できる。

最初に戻る

実際のJSPの例

以下に例を挙げる。

  1. User名とPasswordを入力する画面
  2. 年齢と性別を入れる画面
  3. メールアドレスと電話番号を入れる画面
  4. 確認画面
  5. 登録終了画面
  1. User名とPasswordを入力する画面(inputUandP.jsp)
    <%@ page session="false" %>
    <%@ page contentType="text/html; charset=Windows-31J" %>
    <%@ taglib uri="/WEB-INF/tld/spring.tld" prefix="spring" %>
    <%@ taglib prefix="c" uri="http://java.sun.com/jstl/core"%>
    
    <jsp:useBean id="command2" scope="request" class="com.chikkun.webcms.test.Test2CommandBean"/>
    <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
    <HTML>
    <HEAD>
    </HEAD>
    <BODY>
    
    <DIV align="left">ユーザー名とパスワード</DIV>
    <HR>
    <spring:hasBindErrors name="test2">
    <FONT color="red">エラーです。</FONT>
    </spring:hasBindErrors>
    
    <DIV align="left">
    <FORM name="inputForm" action="inputFlow.htm"  method="post">
    <INPUT type="hidden" name="_flowId" value="<%=request.getAttribute("flowId") %>">
    <INPUT type="hidden" name="_event" value="fromUandP">
    <INPUT type="hidden" name="_currentState" value="userAndPass">
    
    <TABLE>
     <TR>
      <TD>ログイン名</TD>
      <spring:bind path="command2.user">
      <TD><INPUT type="text" name="user" value='
    <c:if test="${user != ''}"><c:out value="${user}"/></c:if>
    <c:if test="${status.value != ''}"><c:out value="${status.value}"/></c:if>
    '>
       <font color="red"><c:out value="${status.errorMessage}"/></font></TD>
      </spring:bind>
     </TR>
     <TR>
      <TD>パスワード</TD>
      <spring:bind path="command2.pass">
      <TD><INPUT type="text" name="pass" value='
    <c:if test="${pass != ''}"><c:out value="${pass}"/></c:if>
    <c:if test="${status.value != ''}"><c:out value="${status.value}"/></c:if>
    '>
       <font color="red"><c:out value="${status.errorMessage}"/></font>
    </TD>
      </spring:bind>
     </TR>
    </TABLE>
    
    </FORM>
    </DIV>
    <HR>
    <INPUT type="button" onclick="javascript:document.inputForm.submit()" value="Execute">
    
    </BODY>
    </HTML>
                
  2. 年齢と性別を入れる画面(inputAandS.jsp)
    <%@ page session="false" %>
    <%@ page contentType="text/html; charset=Windows-31J" %>
    <%@ taglib uri="/WEB-INF/tld/spring.tld" prefix="spring" %>
    <%@ taglib prefix="c" uri="http://java.sun.com/jstl/core"%>
    <jsp:useBean id="command2" scope="request" class="com.chikkun.webcms.test.Test2CommandBean"/>
    <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
    <HTML>
    <HEAD>
    </HEAD>
    <BODY>
    
    <DIV align="left">年齢と性別の入力</DIV>
    
    <HR>
    
    <spring:hasBindErrors name="command2">
    <FONT color="red">エラーです。</FONT>
    <br/>
    </spring:hasBindErrors>
    
    入力されたユーザー名:<c:out value="${user}"/>
    <br/>
    入力されたパスワード:<c:out value="${pass}"/>
    <br/>
    
    
    <DIV align="left">
    <FORM name="inputForm" action="inputFlow.htm" method="post">
    <INPUT type="hidden" name="_flowId" value="<%=request.getAttribute("flowId") %>">
    <INPUT type="hidden" name="_event" value="fromAandS">
    <INPUT type="hidden" name="_currentState" value="ageAndSex">
    
    
    
    <TABLE>
     <TR>
      <TD>年齢</TD>
      <spring:bind path="command2.age">
      <TD><INPUT type="text" name="age" value="
    <c:if test="${age != ''}"><c:out value="${age}"/></c:if>
    <c:if test="${status.value != ''}"><c:out value="${status.value}"/></c:if>
    ">
       <font color="red"><c:out value="${status.errorMessage}"/></font>
    </TD>
      </spring:bind>
    </TR>
     <TR>
      <TD>性別</TD>
      <TD>
      <spring:bind path="command2.sex">
    <input type="radio" name="sex" value="male"
     <c:if test="${status.value == 'male'}">checked</c:if>
     <c:if test="${sex == 'male'}">checked</c:if>
    >男性
    <input type="radio" name="sex" value="female"
     <c:if test="${status.value == 'female'}">checked</c:if>
     <c:if test="${sex == 'female'}">checked</c:if>
    >女性
      </spring:bind>
    </TD>
     </TR>
    </TABLE>
    </FORM>
    </DIV>
    <HR>
    
    <INPUT type="button" onclick="javascript:document.inputForm.submit()" value="Execute">
    
    <FORM name="backForm" action="inputFlow.htm" method="post">
    <INPUT type="hidden" name="_flowId" value="<%=request.getAttribute("flowId")%>">
    <INPUT type="hidden" name="_event" value="previous">
    <input type="submit" name="previous" value="back"/>
    </FORM>
    
    </BODY>
    </HTML>
                
  3. メールアドレスと住所を入れる画面 (inputMandA.jsp)
    <%@ page session="false" %>
    <%@ page contentType="text/html; charset=Windows-31J" %>
    <%@ taglib uri="/WEB-INF/tld/spring.tld" prefix="spring" %>
    <%@ taglib prefix="c" uri="http://java.sun.com/jstl/core"%>
    <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
    <HTML>
    <HEAD>
    </HEAD>
    <BODY>
    
    <DIV align="left">メールアドレスと住所(県名)の入力</DIV>
    
    <HR>
    
    <spring:hasBindErrors name="command2">
    <FONT color="red">エラーです。</FONT>
    </spring:hasBindErrors>
    <br/>
    入力されたユーザー名:<c:out value="${user}"/>
    <br/>
    入力されたパスワード:<c:out value="${pass}"/>
    <br/>
    入力された年齢:
    <c:if test="${age != ''}"><c:out value="${age}"/></c:if>
    <c:if test="${age == '' || age == null}">記入されていません</c:if>
    <br/>
    入力された性別:
    <c:if test="${sex != ''}">
    <c:if test="${sex == 'male'}">男性</c:if>
    <c:if test="${sex == 'female'}">女性</c:if>
    </c:if>
    <c:if test="${sex == '' || sex == null}">記入されていません</c:if>
    <br/>
    
    <DIV align="left">
    <FORM name="inputForm" action="inputFlow.htm" method="post">
    <INPUT type="hidden" name="_flowId" value="<%=request.getAttribute("flowId")%>">
    <INPUT type="hidden" name="_event" value="fromMandA">
    <INPUT type="hidden" name="_currentState" value="mailAndAddress">
    
    <TABLE>
     <TR>
      <TD>メールアドレス</TD>
      <spring:bind path="command2.mail">
      <TD><INPUT type="text" size="16" name="mail" value="
    <c:if test="${mail != ''}"><c:out value="${mail}"/></c:if>
    <c:if test="${status.value != ''}"><c:out value="${status.value}"/></c:if>
    ">
       <font color="red"><c:out value="${status.errorMessage}"/></font>
    </TD>
      </spring:bind>
    
     </TR>
     <TR>
      <TD>県名</TD>
      <spring:bind path="command2.address">
      <TD>
    <select name="address">
    <option value="">都道府県
    <option value="北海道"
     <c:if test="${status.value == '北海道'}">selected</c:if>
     <c:if test="${address == '北海道'}">selected</c:if>
    >北海道
    <option value="青森県"
     <c:if test="${status.value == '青森県'}">selected</c:if>
     <c:if test="${address == '青森県'}">selected</c:if>
    >青森県
    <option value="岩手県"
     <c:if test="${status.value == '岩手県'}">selected</c:if>
     <c:if test="${address == '岩手県'}">selected</c:if>
    >岩手県
    <option value="宮城県"
     <c:if test="${status.value == '宮城県'}">selected</c:if>
     <c:if test="${address == '宮城県'}">selected</c:if>
    >宮城県
    <option value="秋田県"
     <c:if test="${status.value == '秋田県'}">selected</c:if>
     <c:if test="${address == '秋田県'}">selected</c:if>
    >秋田県
    <option value="山形県"
     <c:if test="${status.value == '山形県'}">selected</c:if>
     <c:if test="${address == '山形県'}">selected</c:if>
    >山形県
    <option value="福島県"
     <c:if test="${status.value == '福島県'}">selected</c:if>
     <c:if test="${address == '福島県'}">selected</c:if>
    >福島県
    <option value="茨城県"
     <c:if test="${status.value == '茨城県'}">selected</c:if>
     <c:if test="${address == '茨城県'}">selected</c:if>
    >茨城県
    <option value="栃木県"
     <c:if test="${status.value == '栃木県'}">selected</c:if>
     <c:if test="${address == '栃木県'}">selected</c:if>
    >栃木県
    <option value="群馬県"
     <c:if test="${status.value == '群馬県'}">selected</c:if>
     <c:if test="${address == '群馬県'}">selected</c:if>
    >群馬県
    <option value="埼玉県"
     <c:if test="${status.value == '埼玉県'}">selected</c:if>
     <c:if test="${address == '埼玉県'}">selected</c:if>
    >埼玉県
    <option value="千葉県"
     <c:if test="${status.value == '千葉県'}">selected</c:if>
     <c:if test="${address == '千葉県'}">selected</c:if>
    >千葉県
    <option value="東京都"
     <c:if test="${status.value == '東京都'}">selected</c:if>
     <c:if test="${address == '東京都'}">selected</c:if>
    >東京都
    <option value="神奈川県"
     <c:if test="${status.value == '神奈川県'}">selected</c:if>
     <c:if test="${address == '神奈川県'}">selected</c:if>
    >神奈川県
    <option value="新潟県"
     <c:if test="${status.value == '新潟県'}">selected</c:if>
     <c:if test="${address == '新潟県'}">selected</c:if>
    >新潟県
    <option value="富山県"
     <c:if test="${status.value == '富山県'}">selected</c:if>
     <c:if test="${address == '富山県'}">selected</c:if>
    >富山県
    <option value="石川県"
     <c:if test="${status.value == '石川県'}">selected</c:if>
     <c:if test="${address == '石川県'}">selected</c:if>
    >石川県
    <option value="福井県"
     <c:if test="${status.value == '福井県'}">selected</c:if>
     <c:if test="${address == '福井県'}">selected</c:if>
    >福井県
    <option value="山梨県"
     <c:if test="${status.value == '山梨県'}">selected</c:if>
     <c:if test="${address == '山梨県'}">selected</c:if>
    >山梨県
    <option value="長野県"
     <c:if test="${status.value == '長野県'}">selected</c:if>
     <c:if test="${address == '長野県'}">selected</c:if>
    >長野県
    <option value="岐阜県"
     <c:if test="${status.value == '岐阜県'}">selected</c:if>
     <c:if test="${address == '岐阜県'}">selected</c:if>
    >岐阜県
    <option value="静岡県"
     <c:if test="${status.value == '静岡県'}">selected</c:if>
     <c:if test="${address == '静岡県'}">selected</c:if>
    >静岡県
    <option value="愛知県"
     <c:if test="${status.value == '愛知県'}">selected</c:if>
     <c:if test="${address == '愛知県'}">selected</c:if>
    >愛知県
    <option value="三重県"
     <c:if test="${status.value == '三重県'}">selected</c:if>
     <c:if test="${address == '三重県'}">selected</c:if>
    >三重県
    <option value="滋賀県"
     <c:if test="${status.value == '滋賀県'}">selected</c:if>
     <c:if test="${address == '滋賀県'}">selected</c:if>
    >滋賀県
    <option value="京都府"
     <c:if test="${status.value == '京都府'}">selected</c:if>
     <c:if test="${address == '京都府'}">selected</c:if>
    >京都府
    <option value="大阪府"
     <c:if test="${status.value == '大阪府'}">selected</c:if>
     <c:if test="${address == '大阪府'}">selected</c:if>
    >大阪府
    <option value="兵庫県"
     <c:if test="${status.value == '兵庫県'}">selected</c:if>
     <c:if test="${address == '兵庫県'}">selected</c:if>
    >兵庫県
    <option value="奈良県"
     <c:if test="${status.value == '奈良県'}">selected</c:if>
     <c:if test="${address == '奈良県'}">selected</c:if>
    >奈良県
    <option value="和歌山県"
     <c:if test="${status.value == '和歌山県'}">selected</c:if>
     <c:if test="${address == '和歌山県'}">selected</c:if>
    >和歌山県
    <option value="鳥取県"
     <c:if test="${status.value == '鳥取県'}">selected</c:if>
     <c:if test="${address == '鳥取県'}">selected</c:if>
    >鳥取県
    <option value="島根県"
     <c:if test="${status.value == '島根県'}">selected</c:if>
     <c:if test="${address == '島根県'}">selected</c:if>
    >島根県
    <option value="岡山県"
     <c:if test="${status.value == '岡山県'}">selected</c:if>
     <c:if test="${address == '岡山県'}">selected</c:if>
    >岡山県
    <option value="広島県"
     <c:if test="${status.value == '広島県'}">selected</c:if>
     <c:if test="${address == '広島県'}">selected</c:if>
    >広島県
    <option value="山口県"
     <c:if test="${status.value == '山口県'}">selected</c:if>
     <c:if test="${address == '山口県'}">selected</c:if>
    >山口県
    <option value="徳島県"
     <c:if test="${status.value == '徳島県'}">selected</c:if>
     <c:if test="${address == '徳島県'}">selected</c:if>
    >徳島県
    <option value="香川県"
     <c:if test="${status.value == '香川県'}">selected</c:if>
     <c:if test="${address == '香川県'}">selected</c:if>
    >香川県
    <option value="愛媛県"
     <c:if test="${status.value == '愛媛県'}">selected</c:if>
     <c:if test="${address == '愛媛県'}">selected</c:if>
    >愛媛県
    <option value="高知県"
     <c:if test="${status.value == '高知県'}">selected</c:if>
     <c:if test="${address == '高知県'}">selected</c:if>
    >高知県
    <option value="福岡県"
     <c:if test="${status.value == '福岡県'}">selected</c:if>
     <c:if test="${address == '福岡県'}">selected</c:if>
    >福岡県
    <option value="佐賀県"
     <c:if test="${status.value == '佐賀県'}">selected</c:if>
     <c:if test="${address == '佐賀県'}">selected</c:if>
    >佐賀県
    <option value="長崎県"
     <c:if test="${status.value == '長崎県'}">selected</c:if>
     <c:if test="${address == '長崎県'}">selected</c:if>
    >長崎県
    <option value="熊本県"
     <c:if test="${status.value == '熊本県'}">selected</c:if>
     <c:if test="${address == '熊本県'}">selected</c:if>
    >熊本県
    <option value="大分県"
     <c:if test="${status.value == '大分県'}">selected</c:if>
     <c:if test="${address == '大分県'}">selected</c:if>
    >大分県
    <option value="宮崎県"
     <c:if test="${status.value == '宮崎県'}">selected</c:if>
     <c:if test="${address == '宮崎県'}">selected</c:if>
    >宮崎県
    <option value="鹿児島県"
     <c:if test="${status.value == '鹿児島県'}">selected</c:if>
     <c:if test="${address == '鹿児島県'}">selected</c:if>
    >鹿児島県
    <option value="沖縄県"
     <c:if test="${status.value == '沖縄県'}">selected</c:if>
     <c:if test="${address == '沖縄県'}">selected</c:if>
    >沖縄県
    </select>
       <font color="red"><c:out value="${status.errorMessage}"/></font>
    </TD>
      </spring:bind>
     </TR>
    </TABLE>
    </FORM>
    </DIV>
    <HR>
    
    <INPUT type="button" onclick="javascript:document.inputForm.submit()" value="Execute">
    
    <FORM name="backForm" action="inputFlow.htm" method="post">
    <INPUT type="hidden" name="_flowId" value="<%=request.getAttribute("flowId")%>">
    <INPUT type="hidden" name="_event" value="previous">
    <input type="submit" name="previous" value="back"/>
    </FORM>
    
    </BODY>
    </HTML>
                
  4. 確認画面 (inputConfirm.jsp)
    <%@ page session="false" %>
    <%@ page contentType="text/html; charset=Windows-31J" %>
    <%@ taglib uri="/WEB-INF/tld/spring.tld" prefix="spring" %>
    <%@ taglib prefix="c" uri="http://java.sun.com/jstl/core"%>
    
    <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
    <HTML>
    <HEAD>
    <META http-equiv="Content-Type" content="text/html; charset=Windows-31J">
    <META http-equiv="Content-Style-Type" content="text/css">
    <TITLE>登録完了</TITLE>
    </HEAD>
    <BODY>
    ユーザー名:<c:out value="${user}"/>
    <br/>
    パスワード:<c:out value="${pass}"/>
    <br/>
    年齢:
    <c:if test="${age != ''}"><c:out value="${age}"/></c:if>
    <c:if test="${age == '' || age == null}">記入されていません</c:if>
    <br/>
    性別:
    <c:if test="${sex != ''}">
    <c:if test="${sex == 'male'}">男性</c:if>
    <c:if test="${sex == 'female'}">女性</c:if>
    </c:if>
    <c:if test="${sex == '' || sex == null}">記入されていません</c:if>
    <br/>
    メールアドレス:<c:out value="${mail}"/>
    <br/>
    住所:<c:out value="${address}"/>
    <br/>
    
    で登録しますが、よろしいでしょうか。
    
    <FORM name="inputForm" action="inputFlow.htm"  method="post">
    <INPUT type="hidden" name="_flowId" value="<%=request.getAttribute("flowId") %>">
    <INPUT type="hidden" name="_event" value="save">
    <INPUT type="hidden" name="_currentState" value="confirm">
    <input type="submit" name="previous" value="登録">
    <INPUT type="hidden" name="user" value="<c:out value="${user}"/>">
    <INPUT type="hidden" name="pass" value="<c:out value="${pass}"/>">
    <INPUT type="hidden" name="age" value="<c:out value="${age}"/>">
    <INPUT type="hidden" name="sex" value="<c:out value="${sex}"/>">
    <INPUT type="hidden" name="mail" value="<c:out value="${mail}"/>">
    <INPUT type="hidden" name="address" value="<c:out value="${address}"/>">
    </form>
    </br>
    
    <FORM name="inputForm" action="inputFlow.htm"  method="post">
    <INPUT type="hidden" name="_flowId" value="<%=request.getAttribute("flowId") %>">
    <INPUT type="hidden" name="_event" value="pre">
    <INPUT type="hidden" name="_currentState" value="confirm">
    <input type="submit" name="pre" value="1つ前">
    </form>
    <FORM name="inputForm" action="inputFlow.htm"  method="post">
    <INPUT type="hidden" name="_flowId" value="<%=request.getAttribute("flowId") %>">
    <INPUT type="hidden" name="_event" value="prepre">
    <INPUT type="hidden" name="_currentState" value="confirm">
    <input type="submit" name="prepre" value="2つ前">
    </form>
    <FORM name="inputForm" action="inputFlow.htm"  method="post">
    <INPUT type="hidden" name="_flowId" value="<%=request.getAttribute("flowId") %>">
    <INPUT type="hidden" name="_event" value="preprepre">
    <INPUT type="hidden" name="_currentState" value="confirm">
    <input type="submit" name="preprepre" value="3つ前">
    </form>
    
    </BODY>
    </HTML>
                
  5. 登録終了画面 (これだけはvmにした)---test2Redirect.vm
    <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
    <HTML>
    <HEAD>
    <META http-equiv="Content-Type" content="text/html; charset=Windows-31J">
    <META http-equiv="Content-Style-Type" content="text/css">
    <TITLE>ユーザー名とパスワード入力後</TITLE>
    </HEAD>
    <BODY>
    
    DBに登録しました。
    
    <a href="inputFlow.htm">最初に戻る</a>
    
    </BODY>
    </HTML>
                  

最初に戻る