2008年6月12日木曜日

Spring Web Flow 1.0.5 を試してみる

挫折して悪口を言っているだけでは悔しいので昔のSpring WebFlwo(以下SWF)バージョン(1.0.5)から勉強しなおしてみます。

なぜバージョン1.0台か。
SWF2.0からJSFやAjaxとの連携が教化された旨の情報をどこかで読んだからです。その画面=Viewの対応強化にともなって設計の不透明さが増大したことは確かです。だとすれば1.0をみればSWFの設計コアがより分かり安いのではないか、と。

1.0には以下のような参考サイトもあります。かなりしっかり解説されていますね。大したものです。
Spring Web Flow解説(1)
2.0よりも少しは状況がマシではないかと期待しています。

(Spring WebFlowが今のままでは主流にならないという確信は変っていません。それでも勉強するのはほとんど意地です)

1.0.5はsourceforgeから辿って取得しました。

まずはphonebooksサンプルのビルドです。(パスが"/"なのはcygwinだからです。)
readmeにあったようにant distしてみます。
[phonebook/] $ pwd
/spring-webflow-1.0.5/projects/spring-webflow-samples/phonebook
[phonebook/] $ ant dist

無事にtarget/artifacts/war/swf-booking.warが作成されました。(このあたりも2.0よりはずいぶんスムースな気がします)

すぐにtomcat6.0で動かすこともできました。DBも不要のようです。
サンプルはこうでなくちゃいけません。

今回はここまでとします。できれば次回以降でシンプルなアプリを作成し、SWF2.0につなげたいと思っています。

2008年6月11日水曜日

Spring Web Flowを試してみる(3)~ だめだこりゃ

※挫折・負け惜しみの記録です。

サンプルアプリ(swf-booking-mvc)は結局ほとんど役に立ちませんでした。
機能使いすぎ。どれがswf必須で、どれがオプションなのかが分かりません。しょうがないので0からアプリの作成に挑戦してみます。

(3日ほど経過)

ダメです。シンプルな画面遷移だけのサンプルすら作れません。

「Developing a Spring Framework MVC application step-by-step」のようなドキュメントがない。付属のbooking-mvcサンプルとリファレンスマニュアルをつきあわせて読んでみても、どう作ればいいのかさっぱり分かりません。

最後の手段でソースをごりごり読んでみましたが、さっぱり理解できない。設定ファイルをロードしておいて、リクエストのパラメータからコントローラとビーンを引っ張って処理をしているんだろうな、という程度の認識で読んでも全く分かりませんでした。Struts 1.0はシンプルで分かりやすかったのに・・・

【まとめ】

・サンプルひどすぎ。あんなリッチなサンプルではWebFlowの機能が全然見えません
・ドキュメント貧弱すぎ。リファレンスマニュアルだけじゃ作れっこない

このテクノロジーはいずれ消えるだろうな、というのが今のところの感想です。

2008年6月10日火曜日

「障害を許さない」プロジェクトが破綻する理由(5)

■ システム障害はコントロールできない

ストア派の哲学者がかつて言ったそうです。「自分のコントロールできないものは諦めろ。自分がコントロールできる対象に集中しろ」と。

一応権威付けにストア派を持ち出してみましたが、実にまっとうな考えだと思います。

確かヤンキース松井だったと思いますが「不調の時いくらマスコミに叩かれても僕は気にしません。ぜなら僕はマスコミをコントロールできないから。自分がコントロールできること(つまり練習)をひたすらやるだけです」という旨のことをどこかで言っていた気がします。(例によってソースはありません)

まさにストイックな考え方ですね。自分がコントロールできる対象に集中すること。コントロールできないことは対象としない。

ではコントロールできないものとコントロールできるものとの違いはなんでしょうか。何を以って区別したらよいのでしょうか。

判断のセンスが問われるところです。

ヤンキース松井はマスコミをコントロール不能の対象としました。
でもひょっとしたらそう考えない人もいるかもしれません。悪口を言われたら言い返せばいい。論争によって黙らせることもできるかもしれない。裁判に訴えるという手もある。あるいはマスコミの指摘が正しいならばそれを受け入れてもいい。こっちがおとなしくなればそれ以上叩かれない可能性もある。

微妙なところですね。いずれも効果的ではないように見えます。それに挑発に乗らされていらぬ体力を使わせられている感もある。マスコミに迎合するのも美しくない。

マスコミをコントロールできないものとする判断には二つの価値観がキーになっているようです。

一つ目は美学。しょうもないバッシングにいちいち反応するのも迎合するのもカッコ悪い。

もう一つは経済学。マスコミをコントロールしようとして必死になっても、出来ることもその効果も限られているでしょう。それよりもひたすら自分が納得行くまで練習する方が効率的だし、体力の使い方としても有効です。

そろそろ本題に入りましょう。

システム障害がコントロール可能できるか。間違いなくコントロール不能です。コントロール不能という属性がシステム障害という概念に必然的に含まれていると言ってもよいでしょう。テストで意図的に障害を発生させる場合などを除けば、コントロール可能な障害という言葉がほとんど自己矛盾だからです。

システム障害は常に不意にこちらの意志とは無関係に発生するものです。そういう意味では天災に例えてもいいでしょう(世の中には富士山の噴火を止めるために日夜頑張っている人もいるそうですが)。

違和感を持つ人もいるかもしれませんね。システム障害は少なくとも人災であって天災ではない。コントロールできるのではないか。

では具体的に何ができるでしょうか。設計書を読み返す?プロジェクトのメンバーに「障害は許さない」とプレッシャーを掛ける?いずれも無意味です。

人は往々にして「将来をコントロールできる」と考えたくなるものです。そこには人間の本性に備わっている強い欲望がある。あるいは未来に対する不安を何とかしたい、という焦燥が隠されている。その不安に「原因と結果」というスキームを持ち込むとこうなります。

「今」は間違いなく次の瞬間につながっている。つまり、未来の原因は、今である。今が分かれば未来が分かる。

そもそも「原因と結果」というスキームが人間的な尺度に過ぎないことに気がつくべきです。あなたは自分がいつ死ぬのか、分かりますか?自分が死ぬ原因は今の自分に全て含まれている。本当でしょうか。それはただの人間的な解釈に過ぎないのではありませんか?

「原因と結果」は全て事後的な人間にとっての認識の枠組み=解釈に過ぎないのです。生きるために必要な誤謬としての真理。もし運が良ければ、前提が変らなければ因果の法則通りに物事が進むこともあるかもしれません。しかし必ずしも物事はそうはならないことの方が多い。

もう少し穏当な表現を使えば「原因と結果」という枠組みは慎重に適用されなければならないし、その内容ももっと慎重に検証されるべきなのです。あくまで人間が長い進化の過程で培ってきた概念に過ぎない。極めて有効に働くこともあるかもしれませんが、そうでない場合だって多い。
「原因と結果」というスキームは常に事後的であることに常に注意すべきです。未来への不安を覆い隠すために安直に時系列を逆転させてはなりません。

未来は原因と結果という時系列で理解できるよりももっと豊かで、もっと不確実なものなのです。

少し脱線しました。

しかし明らかにシステム障害は好ましくない。ではそれを防ぐために何をしたらよいのか。

答えは明らかです。同じようなことを繰り返しますが「入念にテストをしておくこと」「システムの状態を常に把握できるようにしておくこと」それから別の次元になりますが「システム障害を業務の障害に直結させないように、システム障害を折り込んだ業務を準備しておくこと」。

障害はコントロールできない。そこがプロジェクトの出発点だと私は思います。

2008年6月5日木曜日

Spring Web Flowを試してみる(2)

Spring Web Flowを試してみる(2)

eclipse3.3にswf-booking-mvc-2.0.1.RELEASE.warをインポートしてantのbuildファイルを作成しました。
あとはwarの中身の探検ですが、その前にbooking-mvcをdeployしてみました。動かないものをいじってもしょうがないので。
するとTomcat起動時にいきなりエラーが。まあ覚悟はしてましたけどね。DB関係の設定何もしてないし。
2008/06/05 10:45:55 org.apache.catalina.core.StandardContext listenerStart
致命的: クラス org.springframework.web.context.ContextLoaderListener のリスナインスタンスにコンテキスト初期化イベントを送信中の例外です
org.springframework.beans.factory.BeanCreationException: Error creating bean with name '_rememberMeServicesInjectionBeanPostProcessor': Initialization of bean failed; nested exception is org.springframework.beans.factory.BeanCreationException: ・・・

ログに出ていた[/WEB-INF/config/data-access-config.xml]をチェック。HSQLDBを使っているようなので、springサンプルで使ったDBのURLに編集しなおしました。
jdbc:hsqldb:mem:booking

jdbc:hsqldb:hsql://localhost

hsqlを起動して再チャレンジ。
当然のようにだめ。同じエラーがでます。DBが出来ていないことが原因だろうと推測。
hsqldbにテーブル群を作成してみます。(← 6/6追記:作成してはいけません。ダメです)
テーブルは insert.sql を参考にして適当に作りました。(割と嫌気がさしてきています。こんなのサンプルにつけておくべきでしょう)
(後記:動かなかったのは別の原因でした。ひょっとしたらdeployしたら自動で作ってくれるのかもしれません)
(6/6追記:自動で作ってくれます。しかも以下のテーブルを作ってしまうと動きません!)
CREATE TABLE Customer (
username varchar(127),
name varchar(127)
);
create table Hotel (
id INTEGER NOT NULL PRIMARY KEY,
price decimal(15,2),
name varchar(127),
address varchar(127),
city varchar(127),
state varchar(20),
zip varchar(10),
country varchar(20)
);


hsqlの管理画面から上記テーブルを作成。
java -classpath ../war/WEB-INF/lib/hsqldb.jar org.hsqldb.util.DatabaseManager

データのロードを実施。データロードにはantのタスク(spring mvcのチュートリアルからコピペ)を使いました。
    <target name="loadData">
<echo message="LOAD DATA USING: ${db.driver} ${db.url}"/>
<sql driver="${db.driver}"
url="${db.url}"
userid="${db.user}"
password="${db.pw}"
onerror="continue"
src="src/import.sql">
<classpath refid="master-classpath"/>
</sql>
</target>

結果はやはり失敗。
同じエラーが出ます。

googleで"No persistence units parsed from"で検索すると Error using JPAというページが引っかかりました。内容を読んでみると"WEB-INF/classes/META-INF/persistence.xml"がないとダメだ、とのこと。

eclipseにwarをインポートして、antでデプロイしていたのですが、"src/META-INF/persistence.xml"をtomcatにデプロイできていなかったことが判明しました。
上記をWEB-INF/classesにコピーする処理をbuild.xmlに追加。
    <target name="build" description="Compile main source tree java files">
<mkdir dir="${build.dir}"/>
<javac destdir="${build.dir}" source="1.5" target="1.5" debug="true"
deprecation="false" optimize="false" failonerror="true">
<src path="${src.dir}"/>
<classpath refid="master-classpath"/>
</javac>
<copy todir="${build.dir}" preservelastmodified="true">
<fileset dir="${src.dir}">
<include name="**/*.xml"/>
</fileset>
</copy>

</target>

無事にアクセスできました。
よかったよかった。

結論として、warファイルの中身はdata-access-config.xmlの"jdbc:hsqldb:mem:booking"を適切なものに変更し(本当は不要なのかもしれませんが)テーブルを作ればwarのデプロイに成功しました。
(6/6追記:テーブルは作成不要です。自動で作ってくれます)

日暮れて道遠しですが、とりあえずは一段落。

Spring Web Flowを試してみる(1)

Spring MVCは確かに優れたフレームワークなのですが、SimpleFormControllerとAbstractWizardFormControllerだけでは一般的に要求されるページ遷移を実装するのは難しいと思われます。
単に行ったり来たりするだけならば問題はありません。そうではなくブラウザの戻るボタンが押されたときに画面遷移を無効化したいとか、ブラウザリフレッシュ(リロード)を無効化したいという要件にSpring MVCで対応するのは厳しいようです。

SimpleFormControllerはformBackingObjectで画面出力し、画面入力をdoSubmitで受けて処理をし、redirectで返すという単発の処理に適したコントローラです。入力もセッションもその場限りで更新されるイメージです。
画面遷移を制御したい場合には、セッション管理やBeanを作りこみ、かつredirectではなくforwardで画面遷移を構成する必要がありますが、単にゴリゴリ書いて行くだけではSimpleFormControllerを実装したコントローラの肥大化を招くだけです。
AbstractWizardFormControllerは、Wizard形式の画面遷移を簡単に実装できる素晴らしいコントローラですが、同じく多数の画面(例えば50画面)をこれで管理するのはムリがあります。

ということで、Spring Web Flowがどれほどのものか試してみたいと思います。

【途中経過】

思った以上にドキュメントが貧弱で苦労しています。
eclipse 3.3へのプロジェクトのインポートもreadme通りには行きませんでした。

とりあえずspring-webflow-2.0.1.RELEASE\projects\spring-webflow-samples\booking-mvcで
ant jar 
ant publish

するとspring-webflow-2.0.1.RELEASE\projects\integration-repo\org.springframework.webflow.samples\swf-booking-mvc\2.0.1.RELEASE\swf-booking-mvc-2.0.1.RELEASE.war"が出来たようなので、ここから始めたいと思っています。

2008年6月4日水曜日

10年続けてみる

今朝ふと吉本隆明が言った「何事も10年続けるとモノになる」という言葉を思い出しました。
どういう文脈だったかは思い出せません。

いろいろつらいことも気に入らないこともあるだろうけど、まあとにかく10年続けてれば何かしら光明が見えてくる。体がそう成ってくる、という風に私は理解しました。

何事も10年続ければモノになる。

ありふれた言葉の要ですが、なかなか含蓄があります。

 確かにある種の技量は幼いうちに素養を積んでおかないと、歳をとってからは厳しいと思います。
例えば楽器の演奏です。小さい頃に耳が出来ていないと、例えばバイオリンの学習は難しいでしょう。
しかし、上手く行かなくて難しくて大変だからこそ、10年続けることに意義があるのかもしれません。

すこし脱線しますが、「耳が出来る」ということを、私は養老猛司風に「脳に回路が出来る」と解釈しています。耳が出来るということは、脳に音階に対応した回路が出来る。耳からのインプットを受けて指が勝手に動くような仕組みが脳に出来て行く。そう考えるとなんだか楽しくなるので。

私の人生をふり返ってみても、とにかく頑張って仕事をしてきて、いろいろ外部の環境の変化もあったけれど、でも何とか仕事がモノになってきた。幸福か?人のことがうらやましくないか?という問いには簡単には答えられませんし、先の不安もないわけではありません。それにしてもまあこれまで何とかなったもんだな、という感慨があります。

楽器など趣味の世界でもそうですね。確かに小さい頃からみっちり練習してきたプロや音大/芸大出の方にはかなわない。でも趣味も10年続けていれば、それなりに自己満足できるところまでは行けると思います。

10年。長いようで短い期間です。30歳から本気で何かをやり始めたとして、50歳まで20年しかありません。しかし短い期間といえども続けるということはそんな簡単ではない。何かを続ける10年間は長くも短くもなるわけです。

そう考えると今の時間と、今10年後を見定めて何を始めるのか、という問いが非常に重いものであることが分かります。またその問いには自分を何かに投げかけるという生き方が示唆されている。ただ漫然と日々を過ごすのではなく未来へ向かって自分を投げ入れるという生き方です。そして10年という単位で人生を見たときに今の時間が非常に貴重であることに気がつかされます。

これからも大変なことはいろいろあるでしょうが、頑張って生きて行こうと思います。

(6/6追記)
asahi.comに『「ほぼ日刊」実はホントに日刊 「イトイ新聞」10周年』という記事が出ていて、以下の文章がありました。(リンクが許可制なのでリンクなしです)
糸井さんは「10年続けること」を目標にしてきた。文芸評論家の吉本隆明氏に「とにかく毎日、10年続けたらものになる。ぼくが保証する」と言われたからだ。

微妙にシンクロしてうれしいです。こういうことってたまにある。

2008年6月3日火曜日

「障害を許さない」プロジェクトが破綻する理由(4)

■障害の予防にかけるコスト

障害が発生すれば当然コストがかかります。
障害によってはその損失額は膨大になるでしょう。また考えられる障害のケース毎に予想される損失額を加算して行くと、ほとんど無限とも言える額になると思います。
小は個々のコンポーネントの障害から、大きくは(昨今話題の銀行システムで言えば)ATMの障害や勘定系システムの障害、情報系システムの障害、あるいは顧客情報の漏洩、ブランドイメージの失墜などなど・・・

では、障害を予防するためにはどれだけコストをかけるべきか。
障害が発生した場合の推定損失額は無限だから無限にコストをかける?それはあり得ません。時間と金を無限にかけるシステムは決して完成することはないでしょう。

では適性なコストとは?

障害の根本的な原因は常に人だと私は思います。人が何か新しいことを始めればミスをする。そして運が悪ければそのミスの結果、障害が発生する。だから障害を発生させないためには、人に障害につながるようなミスをさせないような仕組みが必要です。

   素晴らしい。これで障害を根絶できます。まさにノーベル賞ものの理論ではないでしょうか。

でも残念でした。そんなに簡単には行きません。

仕組みといってもシステム開発作業は決まりきったルーチンワークではありません。ルーチンワークならそれこそシステムに任せればいい(そのシステムも障害からは逃れられませんが・・・)。むしろ少なからずクリエイティブな作業です。機械的に定義できるようなものではない。従って運用や仕組みによってその成果を一定の品質に保つことはできません。成果物の品質は個人の能力に依存してきます。しかしいくら優秀な人間が作業をしても当然勘違いやミスは発生する(世の中に完璧な人生を送る人がいるでしょうか?)。そして事前に潰せるミスも少なからずある。ではどのようにしてミスを潰すか。例えばミスのチェック体制を作ることによって潰すことができますね。

ここで私は「ミスを起こさない/起こさせないためにどれだけ努力するか」という命題を「人をどれだけ信頼するか」という命題に差し替えます。

人を疑えばコストが掛かります。ある人の行動や成果を徹底的にチェックしようとすると、もう一人の人がいる(実は一人では足りないんじゃないか、という気もします。人が増えるとそれだけで仕事が増えますから)。じゃあ、そのチェックする人を信用するのか、という話も出てきます。つまり疑い始めればきりがないのです。

かといってメンバーを全面的に信頼するわけには行かない。プロジェクトに関わったことがある人ならお分かりだと思います。ウソを報告したりまずいことを隠したりメンバーもいるものです。

逆説的ですが障害やミスを発生させないように強いプレッシャーをかけるとメンバーがウソをついたり隠したりする傾向が強まります(むしろ当然?)。そうならないためには、かえって障害やミスを報告したメンバーを誉めてやるべきです。

ですから、プロジェクトの運営を通じて信頼度と技術力が見極めたら、信頼できるメンバーには基本的に任せる。そして技術力が疑わしいメンバーのタスクを減らし、その成果物をチェックする。これがもっとも効率がいい。別にウソをついているのではないか。信頼できない、と疑うってかかるわけではありません。それは却って逆効果でしょう。オーバーワークになったメンバーはえてして品質の悪いものを作ります。しかしその人が悪いわけではなく、追い込んだリーダー/マネージャの問題です。
とにかくここが出発点だと思います。まずは信頼できる人間を信頼すること。そして信頼できる人間が前向きに作業できる時間を確保した上で、チェック体制を作ること。チェックばかりに体力が行ってしまうと、行きつく先は品質の劣化に他なりません。

システム開発メンバの本業は何か。障害を潰すためのチェックではありません。障害を洗い出し、品質を向上させるのが本業のはずです。それを見失ってはならない、と私は思います。

2008年6月2日月曜日

「障害を許さない」プロジェクトが破綻する理由(3)

■障害と不安
以前にも不安とプロジェクトの関係について述べましたことがありますが、今回は障害と不安との関係に焦点を当てて考えてみたいと思います。

障害は常に不安を煽るものです。なぜでしょうか。それは障害がわれわれを(未だ来ぬ)未来の可能性へと差し向けるからです。

普段われわれは特にその瞬間瞬間の可能性と直面することなく日常を過ごしています。先のことは全て予定表に入っている。つまり日常の時間にあるのは「未だ来ぬ」未来ではなく「将に来たるべき」将来です。これが普通日常に生きているわれわれの時間意識です。
具体的に言えば、例えば明日、いや次の瞬間にも大地震がおきてわれわれは死ぬかもしれない。あるいは車にぶつかって死ぬかもしれない。残念ながらそれは本当のことです。しかしそれを普段本気で考えている人はいません。健康な人であっても時折自分の死について考えることがあるかもしれません。しかし「次の瞬間死ぬかもしれない」と一瞬一瞬常に真剣に考えている人はほとんどノイローゼでしょう。

もう少し踏み込んで言えば、われわれは日常的な営みによって未来という巨大な不安を覆い隠しているのです。

そして障害は予期されない不愉快な事象として、われわれの不安を顕わにします。だからこそわれわれは障害を前もって、あらかじめ知りたいと思うのです。
しかし残念ながら障害の発生日は手帳には書いていません。予定通りに発生する障害というのはほとんど言葉の矛盾です。

ところが障害の原因が判明した事後からふり返ってみれば、その発生が必然であったことが分かる。
ここである種の人の中で時系列が逆転します。あのときこういう対応をしておけば障害は未然に防ぐことができた。なぜそれができなかったのか。

ある意味正しい反応です。好ましくない事象が発生したときにその原因を究明し次のアクションを取る。ヘビに噛まれて痛かったのならば次はヘビに噛まれないように用心する。しかし残念ながらシステム障害に対応するための方法論としてはいささか貧弱過ぎる。システムはあまりに複雑だし、人はあまりに不完全だからです。

障害に対応するためにはまずは人の不完全さを受け入れる必要があると私は思います。つまり事前に潰そうといたずらに体力をかけるのではなく(これは経済学の問題です)、事前に障害を出来るだけ出して直しておくこと。そして障害が発生したときの運用を確立しておくこと。それがもっとも効率的に障害に対処する方法だと私は思います。

2008年5月29日木曜日

Spring 2.5 AbstractWizardFormControllerを使ってみる(3)

Validatorを使ってみます。

【手順概要】
InputValidatorを追加します。

SimpleWizardControllerでprotected void validatePage(..)をオーバーライドします。

page1form.jsp、page2form.jspにform:errorsタグを入れ込みます。

springapp2-servlet.xmlにvalidatorの定義を追加します。

【手順詳細】
InputValidatorを追加します。
package springapp2.service;

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

public class InputValidator implements Validator {

public boolean supports(Class clazz) {
return AllValueForm.class.isAssignableFrom(clazz);
}

public void validate(Object obj, Errors errors) {
validateFirstPage((AllValueForm) obj, errors);
validateSecondPage((AllValueForm) obj, errors);

}

public void validateFirstPage(AllValueForm form, Errors errors) {
int id = form.getFirstId();
if (id == 0) {
errors.rejectValue("firstId", "", null,
"ID required (Other than 0).");
}
ValidationUtils.rejectIfEmpty(errors, "firstValue", "",
"Value required.");
}

public void validateSecondPage(AllValueForm form, Errors errors) {
ValidationUtils.rejectIfEmpty(errors, "nextValue", "",
"Value required.");
}

}

SimpleWizardControllerでprotected void validatePage(..)をオーバーライドします。
    @Override
protected void validatePage(Object command, Errors errors, int page,
boolean finish) {
AllValueForm form = (AllValueForm) command;
InputValidator validator = (InputValidator) getValidator();
// errors.setNestedPath("order");
switch (page) {
case 0:
validator.validateFirstPage(form, errors);
break;
case 1:
validator.validateSecondPage(form, errors);
}
}

page1form.jsp、page2form.jspにform:errorsタグを入れ込みます。(太字部分)
(page2form.jspは省略しています)
  <table width="95%" bgcolor="f8f8ff" border="0" cellspacing="0" cellpadding="5">
<tr>
<td align="right" width="20%">First id:</td>
<td width="20%">
<form:input path="firstId"/>
</td>
<td width="60%">
<form:errors path="firstId" cssClass="error"/>
</td>
</tr>
・・・略・・・
<form:errors path="firstValue" cssClass="error"/>


springapp2-servlet.xmlにvalidatorの定義を追加します。(太字部分)
・・・略・・・
<bean name="/page1form.htm"
class="springapp2.web.SimpleWizardController">
<property name="sessionForm" value="true" />
<property name="commandName" value="allValueForm" />
<property name="commandClass" value="springapp2.service.AllValueForm" />
<property name="validator">
<bean class="springapp2.service.InputValidator"/>
</property>

</bean>
<bean name="/page2form.htm"
class="springapp2.web.SimpleWizardController">
<property name="sessionForm" value="true" />
<property name="commandName" value="allValueForm" />
<property name="commandClass" value="springapp2.service.AllValueForm" />
<property name="validator">
<bean class="springapp2.service.InputValidator"/>
</property>

</bean>
・・・略・・・


以上。

「障害を許さない」プロジェクトが破綻する理由(2)

■「障害を許さない」スローガンの分析

ではなぜ障害は好ましくないと考えてしまうのでしょうか。今回は少し毛色を変えて「障害は許さない」という精神状態について分析してみましょう。

まず障害とは何でしょうか。

「常に予定外の出来事として発生するもの」
「通常の運用や業務を阻害するもの」
「不安を呼び覚ますもの」

「上司の思いつき」というのも一種の障害であることがよく分かりますね。
障害の原因という観点から整理してみましょう。

「ハードウェアの故障」
「ソフトウェアのバグ(時限系/タイミング系)」
「ソフトウェアのバグ(設計ミス)」
「ソフトウェアのバグ(ロジックの問題)」
「設計書のミス」

一次的にはそんなところでしょうか。

原因追求についてもう少しレベルを深くしてみましょう。
われわれ人間には実にさまざまな愛すべき欠陥があります。

「コミュニケーションミス」
「単純作業ミス」
「勘違い」
「思い込み」
「忘却」
「理解不足」

整理すると「人間の欠陥により」⇒「ハード/ソフトに欠陥が生じ」⇒「それによって予定外の通常業務を妨げる事象が発生し、不安を呼び覚まされる」ことが障害である、と言えます。

しかしこうしてみると「なあんだ。障害が起こるのなんてあったりまえのことじゃないか。所詮は人がやることでしょ」というふうに見えませんか?見えない?それは困った。

以上の文脈から言えば「障害が許せない」という人は「人間は確かに誤りを犯す。しかし障害は許せない」という考え方を持っていることになります。この考え方を二つに分けてみましょう。

(1)「自分のことはさておき他人の起こした障害は許せない」
(2)「自分の起こした障害だからこそ許せない」

(1)について。まあ確かに(例えば、ですよ)原子力発電所の運用をおろそかにされたりすると、これはちょっとけしからんと私でも思います。他人の安全を守るような仕事はきっちりやって頂きたい。
これを「他者糾弾タイプ」と定義します。

(2)は少し気の毒な考え方ですね。これを生真面目につきつめる人はうつ病まっしぐらかもしれません。適度な責任感を持って頂きたい。
こちらを「自己破滅タイプ」とします。

両者の資質を併せ持っているのが普通だと思います。後はそれぞれの傾向が強いかによってその人の性格を定義できるのではないでしょうか。

順列組み合わせによって4つのタイプが存在します。
タイプA【前者が強く、後者が弱い】
 ⇒ 自分のことは棚に上げて人のことをあれこれ言いつらうタイプ。
B【前者が強く、後者も強い】
 ⇒ 自分にも他人にも厳しいタイプ。
C【前者が弱く、後者も弱い】
 ⇒ テキトーな感じ。
D【前者が弱く、後者が強い】
 ⇒ 気の毒な感じですね。周りから利用されそう。

残念ながら悲惨なプロジェクトを推進する人には(また出世する人には)AやBのタイプが多いような気がします。いずれにせよある意味で正義感が強く、間違ったことが許せないタイプと言えるでしょう。こうしてみると、プロジェクトがどんどん悲惨になって行くのもむべなるかな、という気がしないではありません。恐らく短期的には「失敗が許せない」と胸を張っていうような人こそがはびこり「だって人間だもの」と弱々しい声でつぶやく人はどんどん駆逐されてゆくでしょうから。

とまあ今回はなんだか救いのない話になりましたが、実は私は希望を失っていません。短期的には他者糾弾傾向の強い人が幅をきかせるかもしれませんが、そのような人がプロジェクトを破壊する可能性もまた高い。長期的には妥当な判断が出来る人が成功すると私は信じています。

次回は「不安」という観点から「障害を許さない」というスローガンについて考えてみたいと思います。

Spring 2.5 AbstractWizardFormControllerを使ってみる(2)

「戻る」ボタンを実装します。

【手順概要】
page2form.jspにボタンを追加します。

confirmform.jspにボタンを追加します。

これだけ(!!)

【手順詳細】
page2form.jsp
formタグの中に以下を記載します。
<input type="submit" value="Previous" name="_target0">


confirmform.jsp
formタグの中に以下を記載します。
<input type="submit" value="Previous" name="_target1">


以上。素晴らしい。

#補足
前回投稿のspringapp2-servlet.xmlで、下記をコメントアウトしても問題なく動きました。
 <bean id="simpleWizardController"
class="springapp2.web.SimpleWizardController">
<!--
<property name="pages">
<list>
<value>page1form</value>
<value>page2form</value>
<value>confirmform</value>
</list>
</property>
-->

</bean>

逆にSimpleWizardControllerの以下をコメントアウトするとダメでした。
setPages(commands);

少し直感に反するなあ・・・

Spring 2.5 AbstractWizardFormControllerを使ってみる(1)

AbstractWizardFormController(以下AWFC)を使ってみます。
(情報がないので苦労しました・・・)
以下のようなフローを実現します。

入り口画面 ⇒ 設定画面1 ⇒ 設定画面2 ⇒ 設定確認画面 (⇒ 最初に戻る)

まずは小手調べに超基本的な動きのみを実装します。

【始めに】
SimpleFormController(以下SFC)は、画面を"formBackingObject"で描画し、"onSubmit"で送信されたデータを処理するという言わば単一画面でのインプット/アウトプット処理に特化したコントローラです。
それに対してAWFCは複数画面に渡るインプットを内容チェックしながら溜め込んで行く、文字通りウィザード形式の画面遷移をサポートするコントローラです。
「ウィザード形式」という抽象化が秀逸ですね。言われてみればショッピングカートもまさにウィザード形式になっています。
SFCでゴリゴリとウィザード形式の画面遷移を作成するよりも以下の点で優れています。

- 個々の画面にBeanを準備する必要がありません
 ⇒ 複数画面に渡って単一のBeanを共有できます。
- 「戻る」「進む」機能が単一のAWFCで処理できます。
 ⇒ この例でいえば「設定画面1」設定画面2」「設定確認画面」の遷移を単一のコントローラで制御できます。素晴らしい。

【手順概要】
入り口画面を作成します。

入り口画面用のコントローラをSFCで実装します。Beanは作成しません。

上記のSpring サーブレットXML定義ファイルを作成します。

AWFCで参照するBeanを作成します。

AWFCを継承したコントローラを作成します。

設定画面1~設定確認画面を作成します。

上記のSpring サーブレットXML定義ファイルを作成します。

補足「include.jsp」「web.xml」

【手順詳細】
入り口画面を作成します。
<%@ include file="/WEB-INF/jsp/include.jsp" %>

<html>
<head><title>Hello :: Wizard controller test</title></head>
<body>
<h1>Hello - Spring Application</h1>
<p>Message: <c:out value="${model.message}"/></p>
<br><a href="<c:url value="page1form.htm"/>">Input values</a>
</body>
</html>

入り口画面用のコントローラをSFCで実装します。Beanは作成しません。
package springapp2.web;

import java.util.HashMap;
import java.util.Map;

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

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;

public class EntranceController implements Controller {

protected final Log logger = LogFactory.getLog(getClass());
@Override
public ModelAndView handleRequest(HttpServletRequest arg0,
HttpServletResponse arg1) throws Exception {
String message = "WizardForm sample";
Map<String, Object> myModel = new HashMap<String, Object>();
myModel.put("message", message);
return new ModelAndView("entrance", "model", myModel);
}

}

上記のSpring サーブレットXML定義ファイル(springapp2-servlet.xml)を作成します。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">

<!-- the application context definition for the springapp DispatcherServlet -->

<bean name="/entrance.htm" class="springapp2.web.EntranceController" />

<bean id="viewResolver"
class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="viewClass"
value="org.springframework.web.servlet.view.JstlView">
</property>
<property name="prefix" value="/WEB-INF/jsp/"></property>
<property name="suffix" value=".jsp"></property>
</bean>

</beans>

AWFCで参照するBeanを作成します。(まさにNothing specialです)
package springapp2.service;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

public class AllValueForm {
protected final Log logger = LogFactory.getLog(getClass());
private int firstId;
private String firstValue;
private int nextId;
private String nextValue;

public String getNextValue() {
return nextValue;
}

public void setNextValue(String nextValue) {
this.nextValue = nextValue;
logger.info("nextValue set:" + nextValue);
}

public int getNextId() {
return nextId;
}

public void setNextId(int nextId) {
this.nextId = nextId;
logger.info("nextId set:" + nextId);
}

public int getFirstId() {
return firstId;
}

public void setFirstId(int firstId) {
this.firstId = firstId;
logger.info("firstId set:" + firstId);
}

public String getFirstValue() {
return firstValue;
}

public void setFirstValue(String firstValue) {
this.firstValue = firstValue;
logger.info("firstValue set:" + firstValue);
}
}

AWFCを継承したコントローラを作成します。
(本当にこれだけ!)
(5/30追記:processFinish(..)で返すのはforwardではなくredirectがよいです。forwardだとURLがpage1form画面になってしまい、リロードされると変なことになるので・・・)
package springapp2.web;

import java.util.HashMap;
import java.util.Map;

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

import org.springframework.validation.BindException;
import org.springframework.validation.Errors;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.AbstractWizardFormController;
import org.springframework.web.servlet.view.RedirectView;

import springapp2.service.AllValueForm;
import springapp2.service.InputValidator;

public class SimpleWizardController extends AbstractWizardFormController {

private static String[] commands = new String[] { "page1form", "page2form",
"confirmform" };

public SimpleWizardController() {
setPages(commands);
}

@Override
protected ModelAndView processFinish(HttpServletRequest arg0,
HttpServletResponse arg1, Object arg2, BindException arg3)
throws Exception {
String message = "Input done. (WizardForm sample)";
Map myModel = new HashMap();
myModel.put("message", message);
// return new ModelAndView("entrance", "model", myModel);
return new ModelAndView(new RedirectView("entrance.htm"), "model",
myModel);
}

@Override
protected void validatePage(Object command, Errors errors, int page,
boolean finish) {
AllValueForm form = (AllValueForm) command;
InputValidator validator = (InputValidator) getValidator();
// errors.setNestedPath("order");
switch (page) {
case 0:
validator.validateFirstPage(form, errors);
break;
case 1:
validator.validateSecondPage(form, errors);
}
}

}

設定画面1~設定確認画面を作成します。
それぞれのページのsubmitボタンにご注目ください。
<input type="submit" value="Next" name="_target1">
<input type="submit" value="Next" name="_target2">
<input type="submit" value="Finish" name="_finish">

'_targetN'のNが、SimpleWizardController.setPages(String[])した文字列配列の添え字を指します。
commands[0] ⇒ page1form
commands[1] ⇒ page2form
commands[2] ⇒ confirmform

また_finishボタンを押すとSimpleWizardController.processFinish(..)が呼ばれます。

page1form.jsp
<%@ include file="/WEB-INF/jsp/include.jsp" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>

<html>
<head>
<title>Page1</title>
<style>
.error { color: red; }
</style>
</head>
<body>
<h1>Page 1</h1>
<form:form method="post" commandName="allValueForm">
<table width="95%" bgcolor="f8f8ff" border="0" cellspacing="0" cellpadding="5">
<tr>
<td align="right" width="20%">First id:</td>
<td width="20%">
<form:input path="firstId"/>
</td>
<td width="60%">
<!-- form:errors path="percentage" cssClass="error"/ -->
</td>
</tr>
<tr>
<td align="right" width="20%">First value:</td>
<td width="20%">
<form:input path="firstValue"/>
</td>
<td width="60%">
<!-- form:errors path="percentage" cssClass="error"/ -->
</td>
</tr>
</table>
<br>
<input type="submit" value="Next" name="_target1">
</form:form>
<a href="<c:url value="entrance.htm"/>">Home</a>
</body>
</html>

page2form.jsp
<%@ include file="/WEB-INF/jsp/include.jsp" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>

<html>
<head>
<title>Page2</title>
<style>
.error { color: red; }
</style>
</head>
<body>
<h1>Page 2</h1>
<form:form method="post" commandName="allValueForm">
<table width="95%" bgcolor="f8f8ff" border="0" cellspacing="0" cellpadding="5">
<tr>
<td align="right" width="20%">Next id:</td>
<td width="20%">
<form:input path="nextId"/>
</td>
<td width="60%">
<!-- form:errors path="percentage" cssClass="error"/ -->
</td>
</tr>
<tr>
<td align="right" width="20%">Next value:</td>
<td width="20%">
<form:input path="nextValue"/>
</td>
<td width="60%">
<!-- form:errors path="percentage" cssClass="error"/ -->
</td>
</tr>
</table>
<br>
<input type="submit" value="Next" name="_target2">
</form:form>
<a href="<c:url value="entrance.htm"/>">Home</a>
</body>
</html>

confirmform.jsp
<%@ include file="/WEB-INF/jsp/include.jsp" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>

<html>
<head>
<title>Confirm</title>
<style>
.error { color: red; }
</style>
</head>
<body>
<h1>Confirm</h1>
<form:form method="post" commandName="allValueForm">
<table width="95%" bgcolor="f8f8ff" border="0" cellspacing="0" cellpadding="5">
<tr>
<td align="right" width="20%">First id:</td>
<td width="20%">
<c:out value="${allValueForm.firstId}"/>
</td>
<td width="60%">
<!-- form:errors path="percentage" cssClass="error"/ -->
</td>
</tr>
<tr>
<td align="right" width="20%">First value:</td>
<td width="20%">
<c:out value="${allValueForm.firstValue}"/>
</td>
<td width="60%">
<!-- form:errors path="percentage" cssClass="error"/ -->
</td>
</tr>
<tr>
<td align="right" width="20%">Next id:</td>
<td width="20%">
<c:out value="${allValueForm.nextId}"/>
</td>
<td width="60%">
<!-- form:errors path="percentage" cssClass="error"/ -->
</td>
</tr>
<tr>
<td align="right" width="20%">Next value:</td>
<td width="20%">
<c:out value="${allValueForm.nextValue}"/>
</td>
<td width="60%">
<!-- form:errors path="percentage" cssClass="error"/ -->
</td>
</tr>
</table>
<br>
<input type="submit" value="Finish" name="_finish">
</form:form>
<a href="<c:url value="entrance.htm"/>">Home</a>
</body>
</html>

上記のspringapp2-servlet.xmlに定義を追加します。(太字の部分です)
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">

<!-- the application context definition for the springapp DispatcherServlet -->

<bean name="/entrance.htm" class="springapp2.web.EntranceController" />

<bean id="simpleWizardController"
class="springapp2.web.SimpleWizardController">
<property name="pages">
<list>
<value>page1form</value>
<value>page2form</value>
<value>confirmform</value>
</list>
</property>
</bean>

<bean name="/page1form.htm"
class="springapp2.web.SimpleWizardController">
<property name="sessionForm" value="true" />
<property name="commandName" value="allValueForm" />
<property name="commandClass" value="springapp2.service.AllValueForm" />
</bean>
<bean name="/page2form.htm"
class="springapp2.web.SimpleWizardController">
<property name="sessionForm" value="true" />
<property name="commandName" value="allValueForm" />
<property name="commandClass" value="springapp2.service.AllValueForm" />
</bean>
<bean name="/confirmform.htm"
class="springapp2.web.SimpleWizardController">
<property name="sessionForm" value="true" />
<property name="commandName" value="allValueForm" />
<property name="commandClass" value="springapp2.service.AllValueForm" />
</bean>

<bean id="viewResolver"
class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="viewClass"
value="org.springframework.web.servlet.view.JstlView">
</property>
<property name="prefix" value="/WEB-INF/jsp/"></property>
<property name="suffix" value=".jsp"></property>
</bean>

</beans>

補足「include.jsp」
<%@ page session="false"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>

補足「web.xml」
<?xml version="1.0" encoding="UTF-8"?>

<web-app version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
<servlet>
<servlet-name>springapp2</servlet-name>
<servlet-class>
org.springframework.web.servlet.DispatcherServlet
</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>

<servlet-mapping>
<servlet-name>springapp2</servlet-name>
<url-pattern>*.htm</url-pattern>
</servlet-mapping>

<welcome-file-list>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>

</web-app>

以上。

【参考】
Springに付属のJpetstoreサンプルから、OrderFormController.javaのソースと設定ファイル
The Spring Framework (Alan P Sexton)

2008年5月28日水曜日

「障害を許さない」プロジェクトが破綻する理由(1)

障害を許さないプロジェクトは簡単に破綻します。

現場経験の長い方なら、これだけでも何となく同意して頂けるかもしれませんね。

逆に経験のない方や、マネジメントの経験(特にユーザー寄り)が長い方は「何を言ってるんだ」と思われるかもしれません。障害を出さないのが当たり前だろう、と。

なぜ「障害を許さない」プロジェクトが破綻するのか。これから何回かに分けてその理由を述べて行きたいと思います。

■「障害を許さない」プロジェクトの特徴

障害は本当に好ましくないものでしょうか。「障害なんか起こらない方がいいに決まってるじゃないか」と思われた方、少し待ってください。確かに本番運用に入って起こる障害は好ましくない。エンドユーザーさんにも迷惑を掛けますし、現場の体力もかかる。しかしプロジェクトの途中で起こる障害はどうでしょうか。「プロジェクトだって同じだ。障害なんか起こらないでスムースに行くのが一番」という声が聞こえてきそうですね。

でも、私に言わせれば「障害が起こらない」というのは「開発者が仕事をしていない」のと同義なのです。言い換えれば開発者が熱心に仕事をすればするほど、障害が発生するものです。障害が発生しないプロジェクトの方がよほど怖い。それはどこかで開発/テストのプロセスが機能不全に陥っている証拠です。

テストは潜在的な欠陥を発見するためのプロセスです。問題がないことを確認するプロセスではない。ソフトウェアには必ず欠陥があります。欠陥を発見し、修正するためにテストサイクルがあります。
障害が出ることを好ましく思わないプロジェクトではこれが逆転します。本来どんどん障害を出すべきテストフェーズが、障害が起きないことを確認する為のテストフェーズにすり代わってしまうのです。

その結果、障害を許さないプロジェクトでは障害への対応方法が全く違ってきます。

障害の発生を推奨するプロジェクトでは、障害を修正した後に速やかに次のテストケースに移ることができます。目的は次の潜在障害の検知です。潜在障害を検知するためにテストですから、テストをどんどん実施するのが望ましい。テストフェーズが上手く回っているプロジェクトで仕事をするのは気持ちのよいものです。障害がどんどん直り、品質が向上していることが体感できます。

ところが、障害を許さないプロジェクトでは発生した障害が望ましくないものとして捉えられます。障害は発生してはならない。あってはならないことが起こってしまった。今後二度とこのようなことを起こさないために、何をすべきなのか。そして執拗な原因追求が始まります。
つまり障害がほとんど不祥事として扱われる。そして単純ミス、思い込み、レビュー漏れが不祥事の原因と考えられてしまいます。
その結果、本来の目的であるはずの潜在バグの洗い出し作業が後回しになり、現状のプロセスや開発者に対する粗探しが始まります。
コミュニケーションをしっかり取っていればこの障害は発生しなかった。では会議の数を増やそう。一見筋が通っているようにも見えます。しかし、はっきり言って手遅れかつ時間のムダと言えましょう。会議を増やすよりもむしろ再テストを行ったりテストケースを増やすほうがバグが効率的に潰せるのは明らかです。
「障害=不祥事」と捉えるプロジェクトではそのことに気がつかない。将来起こりうる障害を洗いだすことよりも、現状の粗探しに囚われてしまうのです。

繰り返しますが将来起こりうる障害を少なくするためには=ソフトウェアの品質を上げるためにはどんどん障害を出して修正して行くべきです。いくら設計書を読み返しても、ソースコードをチェックしても、会議を増やしても、障害はなくなりません

もちろん会議を増やすことによって実際に別の問題が見つかることがあるかもしれません。でもテストケースを増やしたり、テストを行うほうがよほど効率がよい。テストケースの検討を通じて障害が発見されることはめずらしくありません。またテストを行えば少なくともそのテストは上手く行ったという結果が必ず残ります。
障害の発生を許さないプロジェクトでは、調査や机上検証を執拗に繰り返したり、安心したいがためだけに一時的な文書をベンダーに作成させがちです。しかしこれらは潜在障害を洗い出すためには極めて非効率です。このような作業にリソースをつぎ込んでしまうとプロジェクトが疲弊し、結局品質の向上が後回しになってしまうのです。

(続く)

2008年5月27日火曜日

Springサンプルを拡張した感想

「Springを使うと、たかだかcreateとdeleteのためにあわせて20個以上のファイルを変更しなければならないんでしょ?何がうれしいの?」

正しいご指摘です。
確かにcreateもdeleteも極端を言えばServletひとつで出来てしまいます。単純に比較するとSpring上で開発する方が作業量が多い。
しかしながら実際に作業して頂けると分かると思うのですが、ServletやBeanでゴリゴリコーディングするよりも、開発の負担はかなり軽いように見えました。

「本当?わざわざ体力かけて勉強したから負け惜しみ言ってるんじゃないの?それとも勉強した成果がもったいないから無意識にSpringをかばってる?」

もちろん初期の学習体力はそれなりにかかります。それを乗り越えるとSpringが非常に風通しのよいフレームワークであることが分かります。

やはり各コンポーネントがきれいにオブジェクト指向的に分かれているのが大きいです。その結果、
 - DB系(ProductDAOとその実装)
 - ロジック系(ProductManagerとその実装)
 - Bean系
 - コントローラとJSPの描画系(特にtaglibですね)
 - MVCを関連付けるXML設定ファイル(ここも学習が必要です)
上記それぞれを独立して考えることが可能です。つまり、コンポーネントごとの関係とか副作用を机上で検討する必要がありません。また、JUnitで個々のコンポーネントをテストすることもできます。大規模なプロジェクトでも安心して開発できるデザインと言えます。
実際、Springのお作法が分かってからはほとんど悩むことがなく、サクサクと開発できました。

本当によく出来たフレームワークだと思います。
(喧伝されすぎてるなあ、というのはありますけどね)

Springサンプルに削除機能を作成する

続いてProductの削除機能です。
今度はサクサク行きました。注意点はcheckboxからのデータ取得だけです。
spring:bindでDeleteProducts.product.idをbindするとcheckboxで選択した値が取れません(選択されたものだけでなく、全てとれてしまいます)。
そのため"targets"というint配列をBeanに追加し、jsp上でid="targets"、name="targets"というタグを書き込んでいます。

【手順概要】

ProductDAOにインターフェースdeleteProductを追加します。

JdbcProductDaoでdeleteProductを実装します。

JdbcProductDaoTestsにテストメソッドを追加します。

ProductManagerにdeleteProductインターフェースを追加します。

SimpleProductManagerでdeleteProductメソッドを実装します。

DeleteProducts Beanを追加します。

DelteProductValidatorを追加します。

DeleteProductFormControllerを追加します。

deleteProduct画面を追加します。

springapp-servlet.xml、メッセージ、リンクにエントリーを追加します。

【手順詳細】
ProductDAOにインターフェースdeleteProductを追加します。
public void deleteProduct(Product prod);

JdbcProductDaoでdeleteProductを実装します。
    @Override
public void deleteProduct(Product prod) {
logger.info("Delete product: " + prod.getDescription());
int count = getSimpleJdbcTemplate().update(
"delete from products where id = :id",
new MapSqlParameterSource().addValue("id", prod.getId()));
logger.info("Deleted: " + count);
}

JdbcProductDaoTestsにテストメソッドを追加します。
    public void testDeleteProduct() {
List<Product> products = new ArrayList<Product>();
Product p1 = createProduct(200, "dummy10", 100.10);
Product p2 = createProduct(201, "dummy11", 100.20);
products.add(p1);
products.add(p2);
for (Product p : products) {
productDao.insertProduct(p);
}
List<Product> insertedProducts = productDao.getProductList();
boolean checked1 = false;
boolean checked2 = false;
for (Product p : insertedProducts) {
if (p.getId() == p1.getId()) {
dumpProduct(p);
dumpProduct(p1);
assertEquals(true, p.equals(p1));
checked1 = true;
}
if (p.getId() == p2.getId()) {
dumpProduct(p);
dumpProduct(p2);
assertEquals(true, p.equals(p2));
checked2 = true;
}
}
if (!checked1 || !checked2) {
fail("Product not inserted.");
}
productDao.deleteProduct(p1);
productDao.deleteProduct(p2);
List<Product> afterDelete = productDao.getProductList();
for (Product p : afterDelete) {
if (p.getId() == p1.getId()) {
fail("Product p1 not deleted.");
}
if (p.getId() == p2.getId()) {
fail("Product p2 not deleted.");
}
}
}

ProductManagerにdeleteProductインターフェースを追加します。
 public void deleteProduct(Product product);

SimpleProductManagerでdeleteProductメソッドを実装します。
 @Override
public void deleteProduct(Product product) {
productDao.deleteProduct(product);
}

DeleteProducts Beanを追加します。
package springapp.service;

import java.util.List;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import springapp.domain.Product;

public class DeleteProducts {
/** Logger for this class and subclasses */
protected final Log logger = LogFactory.getLog(getClass());
private List<Product> products = null;
int [] targets = null; // ポイント

public void setProducts(List<Product> products) {
this.products = products;
}

public List<Product> getProducts() {
return products;
}

public int[] getTargets() {
return targets;
}

public void setTargets(int[] targets) {
this.targets = targets;
StringBuffer log = new StringBuffer();
log.append("Targets set:");
for(int i = 0; i < targets.length; i++) {
log.append(targets[i]);
log.append(":");
}
logger.info(log);
}
}

DelteProductValidatorを追加します。
package springapp.service;

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

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

public class DeleteProductsValidator implements Validator {

/** Logger for this class and subclasses */
protected final Log logger = LogFactory.getLog(getClass());

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

public void validate(Object obj, Errors errors) {
DeleteProducts dp = (DeleteProducts) obj;
logger.info("Validating ...");
int[] targets = dp.getTargets();
if (targets == null || targets.length == 0) {
logger.info("Delete target not selected.");
errors.rejectValue("products", "error.target-not-specified", null,
"Value required.");
}
logger.info("Validation end.");
}
}

DeleteProductFormControllerを追加します。
package springapp.web;

import java.util.List;

import org.springframework.web.servlet.mvc.SimpleFormController;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.view.RedirectView;

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

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import springapp.domain.Product;
import springapp.service.DeleteProducts;
import springapp.service.ProductManager;

public class DeleteProductsFormController extends SimpleFormController {
/** Logger for this class and subclasses */
protected final Log logger = LogFactory.getLog(getClass());

private ProductManager productManager;

public ModelAndView onSubmit(Object command) throws ServletException {
DeleteProducts dp = (DeleteProducts) command;
List<Product> products = dp.getProducts();
int[] targets = dp.getTargets();
for (int i = 0; i < targets.length; i++) {
for (Product product : products) {
if (targets[i] == product.getId()) {
productManager.deleteProduct(product);
logger.info("Deleted:" + product);
continue;
}
}
}
return new ModelAndView(new RedirectView(getSuccessView()));
}

protected Object formBackingObject(HttpServletRequest request)
throws ServletException {
DeleteProducts dp = new DeleteProducts();
dp.setProducts(productManager.getProducts());
return dp;
}

public void setProductManager(ProductManager productManager) {
this.productManager = productManager;
}

public ProductManager getProductManager() {
return productManager;
}

}

deleteProduct画面を追加します。
<%@ include file="/WEB-INF/jsp/include.jsp" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>
<html>
<head>
<title><fmt:message key="title"/></title>
<style>
.error { color: red; }
</style>
</head>
<body>
<h1><fmt:message key="deleteproducts.heading"/></h1>
<form:form method="post" commandName="deleteProducts">
<c:forEach items="${deleteProducts.products}" varStatus="row">
<spring:bind path="deleteProducts.products[${row.index}].id">
<input type="checkbox"
<!-- "id"ではチェックボックスの値が取得できません -->
name="targets"
id="targets"
value="<c:out value="${status.value}"/>" />
</spring:bind>
<spring:bind path="deleteProducts.products[${row.index}].description">
<c:out value="${status.value}"/>
</spring:bind>
<spring:bind path="deleteProducts.products[${row.index}].price">
<i>$<c:out value="${status.value}"/></i>
</spring:bind>
<br>
</c:forEach>
<form:errors path="products" cssClass="error"/>
<br>
<input type="submit" align="center" value="Execute">
</form:form>
<a href="<c:url value="hello.htm"/>">Home</a>
</body>
</html>

springapp-servlet.xml
    <bean name="/deleteproducts.htm" class="springapp.web.DeleteProductsFormController">
<property name="sessionForm" value="true"/>
<property name="commandName" value="deleteProducts"/>
<property name="commandClass" value="springapp.service.DeleteProducts"/>
<property name="validator">
<bean class="springapp.service.DeleteProductsValidator"/>
</property>
<property name="formView" value="deleteproducts"/>
<property name="successView" value="hello.htm"/>
<property name="productManager" ref="productManager"/>
</bean>

メッセージ
deleteproducts.heading=Delete products :: SpringApp
error.target-not-specified=Target not specified!!!

hello.jspからのリンク
    <br><a href="<c:url value="deleteproducts.htm"/>">Delete products</a>

以上

Springサンプルに新規作成機能を作成する

Spring 2.5 付属のチュートリアル「Developing a Spring Framework MVC application step-by-step」に、Productの新規作成機能を追加します。

【手順概要】

ProductDAOにインターフェースinsertProductを追加します。

JdbcProductDaoでinsertProductを実装します。

Productにequalsメソッドを追加します。(テスト用)

JdbcProductDaoTestsにテストメソッドを追加します。

ProductManagerにinsertProductインターフェースを追加します。

SimpleProductManagerでinsertProductメソッドを実装します。

InsertProduct Beanを追加します。

InsertProductValidatoを追加します。

InsertProductFormControllerを追加します。

InsertProduct画面を追加します。

springapp-servlet.xml、メッセージ、リンクにエントリーを追加します。

【手順詳細】

ProductDAOにインターフェースinsertProductを追加します。
 public void insertProduct(Product prod);

JdbcProductDaoでinsertProductを実装します。
    public void insertProduct(Product prod) {
logger.info("Insert product: " + prod.getDescription());
int count = getSimpleJdbcTemplate().update(
"insert into products(id, description, price) values(:id, :description, :price)",
new MapSqlParameterSource().addValue("id", prod.getId())
.addValue("description", prod.getDescription())
.addValue("price", prod.getPrice()));
logger.info("Inserted: " + count);
}

Productにequalsメソッドを追加します。(テスト用です)
※ Doubleの比較には'=='ではなくequalsを使います。当たり前か。しかしTigerからdoubleとDoubleが等価的に使えるようになったので油断してしまいました。
 @Override
public boolean equals(Object obj) {
if (obj instanceof Product) {
Product prod = (Product)obj;
if (id != prod.getId()) {
logger.info("ID unmatch.");
return false;
}
if (!description.equals(prod.getDescription())){
logger.info("Description unmatch.");
return false;
}
if (!price.equals(prod.getPrice())) {
// '==' では比較できない
logger.info("Price unmatch.");
return false;
}
return true;
} else {
logger.info("Not Product instance.");
return false;
}
}

JdbcProductDaoTestsにテストメソッドを追加します。
やっててよかったJUnit。後で役に立ちました。
    private Product createProduct(int id, String desc, double price) {
Product ret = new Product();
ret.setId(id);
ret.setDescription(desc);
ret.setPrice(price);
return ret;
}
private void dumpProduct(Product p) {
logger.info(p);
}

public void testInsertProduct() {
List<Product> products = new ArrayList<Product>();
Product p1 = createProduct(100, "dummy1", 100.10);
Product p2 = createProduct(101, "dummy2", 100.20);
products.add(p1);
products.add(p2);
for (Product p : products) {
productDao.insertProduct(p);
}
List<Product> insertedProducts = productDao.getProductList();
boolean checked1 = false;
boolean checked2 = false;
for (Product p : insertedProducts) {
if (p.getId() == p1.getId()) {
dumpProduct(p);
dumpProduct(p1);
assertEquals(true, p.equals(p1));
checked1 = true;
}
if (p.getId() == p2.getId()) {
dumpProduct(p);
dumpProduct(p2);
assertEquals(true, p.equals(p2));
checked2 = true;
}
}
if (!checked1 || !checked2) {
fail("Product not inserted.");
}
}

ProductManagerにinsertProductインターフェースを追加します。
 public void insertProduct(Product product);

SimpleProductManagerでinsertProductメソッドを実装します。
 @Override
public void insertProduct(Product product) {
List<Product> products = getProducts();
int ids[] = new int[products.size()];
int i = 0;
// Uniqueキーをゴリゴリと作成
for (Product prod: products) {
ids[i] = prod.getId();
i++;
}
Arrays.sort(ids);
int last = ids[ids.length-1];
last++;
product.setId(last);
productDao.insertProduct(product);
}

InsertProduct Beanを追加します。Nothing specialというやつですね。
package springapp.service;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

public class InsertProduct {
/** Logger for this class and subclasses */
protected final Log logger = LogFactory.getLog(getClass());
private String description;
private double price;
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
logger.info("description:"+description+" set.");
}
public double getPrice() {
return price;
}
public void setPrice(double price) {
this.price = price;
logger.info("price:"+price+" set.");
}
}

InsertProductValidatorを追加します。
ちゃんと個々の項目にエラーがでます。
package springapp.service;

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

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import springapp.domain.Product;

public class InsertProductValidator implements Validator {

private int DEFAULT_MIN_PRICE = 1;
private int minPrice = DEFAULT_MIN_PRICE;

/** Logger for this class and subclasses */
protected final Log logger = LogFactory.getLog(getClass());

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

public void validate(Object obj, Errors errors) {
InsertProduct is = (InsertProduct) obj;
logger.info("Validating ...");
String desc = is.getDescription();
if (desc == null || desc.equals("")) {
logger.info("Description is null or empty string.");
errors.rejectValue("description", "error.description-not-specified", null,
"Value required.");
}
if (is.getPrice() <= minPrice) {
errors.rejectValue("price", "error.too-cheap",
new Object[] { new Integer(minPrice) },
"Value too low.");
}
logger.info("Validation end.");
}

public void setMinPrice(int i) {
minPrice = i;
}

public int getMinPrice() {
return minPrice;
}

}


InsertProductFormControllerを追加します。
package springapp.web;

import org.springframework.web.servlet.mvc.SimpleFormController;
import org.springframework.web.servlet.view.RedirectView;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.ServletException;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import springapp.service.ProductManager;
import springapp.service.InsertProduct;
import springapp.domain.Product;

public class InsertProductFormController extends SimpleFormController {
/** Logger for this class and subclasses */
protected final Log logger = LogFactory.getLog(getClass());
private ProductManager productManager;

public ModelAndView onSubmit(Object command)
throws ServletException {
InsertProduct is = (InsertProduct) command;
logger.info("Will insert:" + is);
Product product = new Product();
product.setDescription(is.getDescription());
product.setPrice(is.getPrice());
productManager.insertProduct(product);
logger.info(
"returning from PriceIncreaseForm view to " + getSuccessView());
return new ModelAndView(new RedirectView(getSuccessView()));
}

public void setProductManager(ProductManager productManager) {
this.productManager = productManager;
}
}

InsertProduct画面を追加します。
<%@ include file="/WEB-INF/jsp/include.jsp" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>

<html>
<head>
<title><fmt:message key="title"/></title>
<style>
.error { color: red; }
</style>
</head>
<body>
<h1><fmt:message key="insertproduct.heading"/></h1>
<form:form method="post" commandName="insertProduct">
<table width="95%" bgcolor="f8f8ff" border="0" cellspacing="0" cellpadding="5">
<tr>
<td align="right" width="20%">Description</td>
<td width="30%">
<form:input path="description"/>
</td>
<td width="50%">
<form:errors path="description" cssClass="error"/>
</td>
</tr>
<tr>
<td align="right">Price</td>
<td>
<form:input path="price"/>
</td>
<td>
<form:errors path="price" cssClass="error"/>
</td>
</tr>
</table>
<br>
<input type="submit" align="center" value="Execute">
</form:form>
<a href="<c:url value="hello.htm"/>">Home</a>
</body>
</html>

springapp-servlet.xml、メッセージ、リンクにエントリーを追加します。
    <bean name="/insertproduct.htm" class="springapp.web.InsertProductFormController">
<property name="sessionForm" value="true"/>
<property name="commandName" value="insertProduct"/>
<property name="commandClass" value="springapp.service.InsertProduct"/>
<property name="validator">
<bean class="springapp.service.InsertProductValidator"/>
</property>
<property name="formView" value="insertproduct"/>
<property name="successView" value="hello.htm"/>
<property name="productManager" ref="productManager"/>
</bean>

error.description-not-specified=Description not specified!!!

<br><a href="<c:url value="insertproduct.htm"/>">Insert product</a>

以上。少し慣れてきました。

2008年5月26日月曜日

Spring MVCでチェックボックスを使う(2)~解説

前の投稿のままではあまりに不親切なのでポイントを絞って解説します。

【SelectProducts.java】
 private int[] ids = null;

チェックボックスは配列に格納されるハズだという直感に従い int[] ids を定義しました。
(全体を精読できてはいませんがドキュメントにはこのような記載はなかったはず・・・)

【SelectInventorysFormController.java】
 protected ModelAndView onSubmit(HttpServletRequest request,
HttpServletResponse response, Object command, BindException errors)
throws Exception {
SelectProducts sp = (SelectProducts) command;
HttpSession session = request.getSession();
session.setAttribute("SelectProducts", sp);
return new ModelAndView(new RedirectView(getSuccessView()));
}

選択された"ids"を次画面に引き渡すため、オブジェクトをSessionに登録しておきます。

【selectproducts.jsp】
    <form:form method="post" commandName="selectProducts">
<c:forEach items="${selectProducts.products}" var="prod" varStatus="row">
<input type="checkbox" id="ids" name="ids" value="<c:out value="${prod.id}"/>">
<c:out value="${prod.description}"/>
<i>$<c:out value="${prod.price}"/></i><br><br>
</c:forEach>
<input type="submit" align="center" value="Execute">
</form:form>

ゴリゴリとチェックボックスを生成しています。
出来上がりは以下のようになります。
    <form id="selectProducts" action="/springapp/selectproducts.htm" method="post">
<input type="checkbox" id="ids" name="ids" value="1">
Lamp
<i>$10.0</i><br><br>
<input type="checkbox" id="ids" name="ids" value="2">
Table
<i>$10.0</i><br><br>
<input type="checkbox" id="ids" name="ids" value="3">
Chair
<i>$10.0</i><br><br>
<input type="submit" align="center" value="Execute">
</form>


【SetPrices.java】
 private List<Product> products = new ArrayList<Product>();
private int[] ids = null;
private String[] descriptions = null;
private double[] prices = null;
private static final int DEFALUT_NUM = 3;
private int position = 0;

後日検証する予定ですが、ids, descriptions, prices, positionは不要と思われます。

【SetPricesValidator.java】
 public void validate(Object obj, Errors errors) {
SetPrices ps = (SetPrices) obj;
for (Product product : ps.getProducts()) {
logger.info("Validating ...");
if (product.getPrice() == null) {
logger.info("Price is null.");
errors.rejectValue("prices", "error.price-not-specified", null,
"Value required.");
} else {
logger.info(product);
if (product.getPrice() <= minPrice) {
errors.rejectValue("prices", "error.too-cheap",
new Object[] { new Integer(minPrice) },
"Value too low.");
}
}
logger.info("Validation end.");
}
}

直感的に納得できるコーディングに落ち着きました。さすがSpringと思っています。

【SetPricesFormController】
 protected Object formBackingObject(HttpServletRequest request)
throws ServletException {
SetPrices ret = new SetPrices();
SelectProducts sp = (SelectProducts) request.getSession().getAttribute(
"SelectProducts");
int[] i = sp.getIds();
for (Product product : productManager.getProducts()) {
for (int j = 0; j < i.length; j++) {
if (product.getId() == i[j]) {
ret.addProduct(product);
break;
}
}
}
return ret;
}

(List)SetPrices.getProducts()で取るための事前の仕込みです。
このようにしておけば後述の"<c:forEach items="${setPrices.products}" varStatus="row">"でJSPからアクセスできます。(これはJSTLのデザインが素晴らしいですね)
 public ModelAndView onSubmit(Object command) throws ServletException {
List<Product> products = ((SetPrices) command).getProducts();

for (Product product : products) {
productManager.setPrice(product.getId(), product.getPrice());
}

return new ModelAndView(new RedirectView(getSuccessView()));
}

絶対上手く行かないだろうと思っていたのでList<Product> products = ((SetPrices) command).getProducts(); でproductsが取れるのには驚きました。素晴らしい。

【setprices.jsp】
<form:form method="post" commandName="setPrices">
<c:forEach items="${setPrices.products}" varStatus="row">
<spring:bind path="setPrices.products[${row.index}].id">
<input type="hidden"
name="<c:out value="${status.expression}"/>"
id="<c:out value="${status.expression}"/>"
value="<c:out value="${status.value}"/>" />
</spring:bind>
<spring:bind path="setPrices.products[${row.index}].description">
<c:out value="${status.value}"/>
</spring:bind>
<spring:bind path="setPrices.products[${row.index}].price">
<input type="text"
name="<c:out value="${status.expression}"/>"
id="<c:out value="${status.expression}"/>"
value="<c:out value="${status.value}"/>" />
</spring:bind>
<br>
</c:forEach>
<form:errors path="prices" cssClass="error"/>

上記タグによって、以下のHTMLが生成されました。(全てにチェックを入れたパターン。もちろんチェックされ具合によって動的にリストが変わります。)
<form id="setPrices" action="/springapp/setprices.htm" method="post">
<input type="hidden"
name="products[0].id"
id="products[0].id"
value="1" />
Lamp
<input type="text"
name="products[0].price"
id="products[0].price"
value="10.0" />
<br>
<input type="hidden"
name="products[1].id"
id="products[1].id"
value="2" />
Table
<input type="text"
name="products[1].price"
id="products[1].price"
value="10.0" />
<br>
<input type="hidden"
name="products[2].id"
id="products[2].id"
value="3" />
Chair
<input type="text"
name="products[2].price"
id="products[2].price"
value="10.0" />
<br>
<br>
<input type="submit" align="center" value="Execute">
</form>

以上。

Spring MVCでチェックボックスを使う

前回はradioボタンでしたが、今回はチェックボックスを使ってみます。
コントローラはSimpleFormControllerを使います。
チェックボックスで受け付けるため、配列が入力されることが前提となります。
また件数可変の一覧表示画面から、spring:bindタグを使って動的にデータをBeanに入れています。

【手順概要】

変更対象選択用Bean(SelectProducts)を追加します。

SelectInventorysControllerを追加します。

変更対象複数選択画面を追加します。

springapp-servlet.xmlにエントリーを追加します。

メッセージを追加します。

リンクを追加します。

価格設定用Bean(SetPrices)を追加します。

バリデータ(SetPricesValidator)を追加します。

SetPricesFormControllerを追加します。

価格設定用画面を追加します。

メッセージを追加します。

springapp-servlet.xmlにエントリーを追加します。

【手順詳細】

SelectProducts
チェックボックスなので配列を取ります。
package springapp.service;

import java.util.List;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import springapp.domain.Product;

public class SelectProducts {
/** Logger for this class and subclasses */
protected final Log logger = LogFactory.getLog(getClass());

private List<Product> products = null;

private int[] ids = null;

public List<Product> getProducts() {
return products;
}

public void setProducts(List<Product> products) {
this.products = products;
}

public int[] getIds() {
return ids;
}

public void setIds(int[] ids) {
this.ids = ids;
StringBuffer log = new StringBuffer();
for(int i = 0; i < ids.length; i++) {
log.append("id[");
log.append(i);
log.append("]=");
log.append(ids[i]);
log.append("; ");
}
logger.info(log);
}
}

SelectInventorysFormControllerを追加します。
package springapp.web;

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

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

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import springapp.service.ProductManager;
import springapp.service.SelectProducts;

public class SelectInventorysFormController extends SimpleFormController {
/** Logger for this class and subclasses */
protected final Log logger = LogFactory.getLog(getClass());
private ProductManager productManager;

protected Object formBackingObject(HttpServletRequest request)
throws ServletException {
SelectProducts selectProducts = new SelectProducts();
logger.info("productManager:" + productManager.getProducts().size());
selectProducts.setProducts(productManager.getProducts());
logger.info("selectProd:" + selectProducts.getProducts().size());
return selectProducts;
}

@Override
protected ModelAndView onSubmit(HttpServletRequest request,
HttpServletResponse response, Object command, BindException errors)
throws Exception {
logger.info("Executed.");
SelectProducts sp = (SelectProducts) command;
StringBuffer log = new StringBuffer();
log.append("SelectProduct.getIds():");
int[] i = sp.getIds();
for (int j = 0; j < i.length; j++) {
log.append("id[");
log.append(j);
log.append("]=");
log.append(i[j]);
log.append("; ");
}
logger.info(log);
HttpSession session = request.getSession();
session.setAttribute("SelectProducts", sp);
return new ModelAndView(new RedirectView(getSuccessView()));
}

public void setProductManager(ProductManager productManager) {
this.productManager = productManager;
}

public ProductManager getProductManager() {
return productManager;
}

}

変更対象複数選択画面を追加します。
<%@ include file="/WEB-INF/jsp/include.jsp" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>
<html>
<head><title><fmt:message key="title"/></title></head>
<body>
<h1><fmt:message key="selectproducts.heading"/></h1>
<h3>Products</h3>
<form:form method="post" commandName="selectProducts">
<c:forEach items="${selectProducts.products}" var="prod" varStatus="row">
<input type="checkbox" id="ids" name="ids" value="<c:out value="${prod.id}"/>">
<c:out value="${prod.description}"/>
<i>$<c:out value="${prod.price}"/></i><br><br>
</c:forEach>
<input type="submit" align="center" value="Execute">
</form:form>
<br>
<a href="<c:url value="hello.htm"/>">back</a><br>
<br>
</body>
</html>

springapp-servlet.xmlにエントリーを追加します。
    <bean name="/selectproduct.htm" class="springapp.web.SelectInventoryFormController">
<property name="sessionForm" value="true"/>
<property name="commandName" value="selectProduct"/>
<property name="commandClass" value="springapp.service.SelectProduct"/>
<property name="formView" value="selectproduct"/>
<property name="successView" value="setprice.htm"/>
<property name="productManager" ref="productManager"/>
</bean>

メッセージを追加します。
selectproducts.heading=Select Products :: SpringApp

トップページ(hello.jsp)にリンクを追加します。
<br><a href="<c:url value="selectproducts.htm"/>">Select products</a>

価格設定用Bean(SetPrices)を追加します。
妙にリッチな作りになっていますが(結果的に無意味だった)試行錯誤の結果です。
恐らく(面倒くさいので検証していませんが)setProducts(List<Product> products) だけでよいのではないかと思います。(あるべき姿は後日調査予定)
package springapp.service;

import java.util.ArrayList;
import java.util.List;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import springapp.domain.Product;

public class SetPrices {
/** Logger for this class and subclasses */
protected final Log logger = LogFactory.getLog(getClass());
private List<Product> products = new ArrayList<Product>();
private int[] ids = null;
private String[] descriptions = null;
private double[] prices = null;
private static final int DEFALUT_NUM = 3;
private int position = 0;

public SetPrices(int num) {
initialize(num);
}

public SetPrices() {
initialize(DEFALUT_NUM);
}

private void initialize(int number) {
ids = new int[number];
descriptions = new String[number];
prices = new double[number];
}

public void setProducts(List<Product> products) {
this.products = products;
}

public void addProduct(Product prod) {
products.add(prod);
ids[position] = prod.getId();
descriptions[position] = prod.getDescription();
prices[position] = prod.getPrice();
position++;
}

public List<Product> getProducts() {
return products;
}

public int[] getIds() {
return ids;
}

public void setIds(int[] ids) {
this.ids = ids;
}

public String[] getDescriptions() {
return descriptions;
}

public void setDescriptions(String[] descriptions) {
this.descriptions = descriptions;
}

public double[] getPrices() {
return prices;
}

public void setPrices(double[] prices) {
this.prices = prices;
}
}

バリデータ(SetPricesValidator)を追加します。
package springapp.service;

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

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import springapp.domain.Product;

public class SetPricesValidator implements Validator {

private int DEFAULT_MIN_PRICE = 1;
private int minPrice = DEFAULT_MIN_PRICE;

/** Logger for this class and subclasses */
protected final Log logger = LogFactory.getLog(getClass());

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

public void validate(Object obj, Errors errors) {
SetPrices ps = (SetPrices) obj;
for (Product product : ps.getProducts()) {
logger.info("Validating ...");
if (product.getPrice() == null) {
logger.info("Price is null.");
errors.rejectValue("prices", "error.price-not-specified", null,
"Value required.");
} else {
logger.info(product);
if (product.getPrice() <= minPrice) {
errors.rejectValue("prices", "error.too-cheap",
new Object[] { new Integer(minPrice) },
"Value too low.");
}
}
logger.info("Validation end.");
}
}

public void setMinPrice(int i) {
minPrice = i;
}

public int getMinPercentage() {
return minPrice;
}

public void setMaxPercentage(int i) {
minPrice = i;
}

public int getMaxPercentage() {
return minPrice;
}

}

SetPricesFormControllerを追加します。
package springapp.web;

import java.util.List;

import org.springframework.web.servlet.mvc.SimpleFormController;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.view.RedirectView;

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

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import springapp.domain.Product;
import springapp.service.ProductManager;
import springapp.service.SelectProducts;
import springapp.service.SetPrices;

public class SetPricesFormController extends SimpleFormController {
/** Logger for this class and subclasses */
protected final Log logger = LogFactory.getLog(getClass());

private ProductManager productManager;

public ModelAndView onSubmit(Object command) throws ServletException {
List<Product> products = ((SetPrices) command).getProducts();

for (Product product : products) {
productManager.setPrice(product.getId(), product.getPrice());
}

return new ModelAndView(new RedirectView(getSuccessView()));
}

protected Object formBackingObject(HttpServletRequest request)
throws ServletException {
SetPrices ret = new SetPrices();
SelectProducts sp = (SelectProducts) request.getSession().getAttribute(
"SelectProducts");
int[] i = sp.getIds();
for (Product product : productManager.getProducts()) {
for (int j = 0; j < i.length; j++) {
if (product.getId() == i[j]) {
ret.addProduct(product);
break;
}
}
}
return ret;
}

public void setProductManager(ProductManager productManager) {
this.productManager = productManager;
}

public ProductManager getProductManager() {
return productManager;
}

}

価格設定用画面を追加します。
恐らくはここがキモになります。
spring:bindタグを使っています。
参考サイト:Dynamic list binding in Spring MVC
<%@ include file="/WEB-INF/jsp/include.jsp" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>
<html>
<head>
<title><fmt:message key="title"/></title>
<style>
.error { color: red; }
</style>
</head>
<body>
<h1><fmt:message key="setprices.heading"/></h1>
<form:form method="post" commandName="setPrices">
<c:forEach items="${setPrices.products}" varStatus="row">
<spring:bind path="setPrices.products[${row.index}].id">
<input type="hidden"
name="<c:out value="${status.expression}"/>"
id="<c:out value="${status.expression}"/>"
value="<c:out value="${status.value}"/>" />
</spring:bind>
<spring:bind path="setPrices.products[${row.index}].description">
<c:out value="${status.value}"/>
</spring:bind>
<spring:bind path="setPrices.products[${row.index}].price">
<input type="text"
name="<c:out value="${status.expression}"/>"
id="<c:out value="${status.expression}"/>"
value="<c:out value="${status.value}"/>" />
</spring:bind>
<br>
</c:forEach>
<form:errors path="prices" cssClass="error"/>
<br>
<input type="submit" align="center" value="Execute">
</form:form>
<a href="<c:url value="hello.htm"/>">Home</a>
</body>
</html>

メッセージを追加します。
error.price-not-specified=Price not specified!!!
setprices.heading=Set Prices :: SpringApp

springapp-servlet.xmlにエントリーを追加します。
    <bean name="/setprices.htm" class="springapp.web.SetPricesFormController">
<property name="sessionForm" value="true"/>
<property name="commandName" value="setPrices"/>
<property name="commandClass" value="springapp.service.SetPrices"/>
<property name="validator">
<bean class="springapp.service.SetPricesValidator"/>
</property>
<property name="formView" value="setprices"/>
<property name="successView" value="hello.htm"/>
<property name="productManager" ref="productManager"/>
</bean>

以上。疲れた・・・

2008年5月23日金曜日

Springフレームワークの続き(リベンジ)

前回失敗したrequestデータの受け渡しを(ベタですが)HttpSessionを使って実装し、とりあえずリベンジしました。

いろいろ調べていると"SimpleFormController"はformBackingObjectで画面を描画し"onSubmit"でその画面に入力された処理を行うという、ある特定の一画面を処理するのに特化したコントローラのようです。

画面間でデータを受け渡すには以下の方法があって、
1.DBやHttpSessionを使う
2.AbstractWizardFormControllerとSpring WebFlowを使う
ということのようです。

(参考ページ)getSuccessView() and formBackingObject() [Archive] dsklyut氏の投稿より

前回は「Postされたデータが取れない」と嘆きましたが、どうやらSimpleFormControllerはFormも(恐らくはRequestも)次画面に遷移する時はクリアしてしまうらしいですね。納得できる説明です。

以下が変更した内容です。

SelectProduct.java(太字部分が追加)
Requestから直でIDを取るからいらないか、と思ってBeanから抜いてしまっていたプロパティを追加しました。(本来入れておくべきだったかも)
package springapp.service;

import java.util.List;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import springapp.domain.Product;

public class SelectProduct {
/** Logger for this class and subclasses */
protected final Log logger = LogFactory.getLog(getClass());

private List products = null;

private int id = 0;

public int getId() {
return id;
}

public void setId(int id) {
logger.info("id:" + id + " set.");
this.id = id;
}

public List getProducts() {
return products;
}

public void setProducts(List products) {
this.products = products;
}
}

SelectInventoryFormController
HttpServletRequest引数を持つonSubmitに変え、SessionにObjectを詰めています。
package springapp.web;

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

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

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import springapp.service.ProductManager;
import springapp.service.SelectProduct;

public class SelectInventoryFormController extends SimpleFormController {
/** Logger for this class and subclasses */
protected final Log logger = LogFactory.getLog(getClass());
private ProductManager productManager;

protected Object formBackingObject(HttpServletRequest request)
throws ServletException {
SelectProduct selectProduct = new SelectProduct();
logger.info("productManager:" + productManager.getProducts().size());
selectProduct.setProducts(productManager.getProducts());
logger.info("selectProd:" + selectProduct.getProducts().size());
return selectProduct;
}

@Override
protected ModelAndView onSubmit(HttpServletRequest request,
HttpServletResponse response, Object command, BindException errors)
throws Exception {
logger.info("Executed.");
SelectProduct sp = (SelectProduct) command;
logger.info("SelectProduct.getId():" + sp.getId());
HttpSession session = request.getSession();
session.setAttribute("SelectProduct", sp);
return new ModelAndView(new RedirectView(getSuccessView()));
}

public void setProductManager(ProductManager productManager) {
this.productManager = productManager;
}

public ProductManager getProductManager() {
return productManager;
}

}

SetPriceFormController
formBackingObjectの中でSessionを参照し、前画面の結果を貰っています。
package springapp.web;

import org.springframework.web.servlet.mvc.SimpleFormController;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.view.RedirectView;

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

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import springapp.domain.Product;
import springapp.service.ProductManager;
import springapp.service.SelectProduct;
import springapp.service.SetPrice;

public class SetPriceFormController extends SimpleFormController {
/** Logger for this class and subclasses */
protected final Log logger = LogFactory.getLog(getClass());

private ProductManager productManager;

public ModelAndView onSubmit(Object command) throws ServletException {
int id = ((SetPrice) command).getId();
double price = ((SetPrice) command).getPrice();

productManager.setPrice(id, price);
logger.info("Update prices by " + price + ". Id is " + id + ".");
logger.info("returning from PriceIncreaseForm view to "
+ getSuccessView());

return new ModelAndView(new RedirectView(getSuccessView()));
}

protected Object formBackingObject(HttpServletRequest request)
throws ServletException {
SetPrice ret = new SetPrice();
SelectProduct sp = (SelectProduct) request.getSession().getAttribute(
"SelectProduct");
int i = sp.getId();

for (Product product : productManager.getProducts()) {
if (product.getId() == i) {
ret.setId(i);
ret.setDescription(product.getDescription());
ret.setPrice(product.getPrice());
break;
}
}
return ret;
}

public void setProductManager(ProductManager productManager) {
this.productManager = productManager;
}

public ProductManager getProductManager() {
return productManager;
}

}


まずは一段落。Springの評価が少し上がりました(私の誤解が解けただけですけどね)。

2008年5月22日木曜日

「同じ」と「違う」(5)~ 本質を見極める

抽象化とアナロジー、そして「同じ」によって人が物事を理解します。しかし、その理解は具体的な差異(「違う」ということ)を捨象して得られたものです。つまりその理解からは具体的な何かが失われている。ありのままの「物事それ自体」は理解されていないのです。

(補足)ここでは別にカントの「物自体」のような大げさなことを言っている訳ではありません。多分に牽強付会ではありますが、むしろフッサールの言う(「自然的態度」と対比させた上での)事象それ自体に近いという思いはあります。

誤解なのか、それとも本質を見抜いた理解なのか。どのようにして人は本質を見抜くことができるのか。

そもそも本質ってなんだよ、と思ったあなたは鋭いですね。ですが、ここでは「本質とは」という議論には踏み込みません。誤解ではなく、妥当な理解という定義に留めます。

では、抽象化しアナロジーによって把握した上でなおかつ誤解に至らないようにするには、どのようにしたらよいのでしょうか。

別にあっと驚くような意外な方法論があるわけではありません。本質に近づくためには、まず抽象化して削ぎ落とす前に、十分に具象を把握することが必要なのです。それからアナロジーによって把握すると同時に、そのアナロジーが取りこぼした差異をできるだけ把握しておくことです。

その努力を怠れば、粗雑な抽象化による乱暴な認識に留まってしまいます。

当たり前のことですね。

例を上げればきりがありません。
ブラックバスだろうがフナだろうが魚は魚。同じだろう。在来種だろうが外来種だろうが木は木だ。草は草だ。その認識が生態系の破壊をもたらします。自然界は微妙なバランスの上に成り立っている。放っておけば(あるいは粗雑な理解で介入すれば)荒れるばかりです。

あるいはシステムの理解もそうですね。OracleもDB2も同じだろう。要は同じRDBMSだ。安ければいい。ある意味では本質的な意見ではあります。例えば経営層などであれば、SQLの方言だとか細かい使い勝手の差異を気にする必要はありません。しかし「OracleもDB2も同じだ」という認識ではプロジェクトは回りません。発生する障害の種類がまるで違う。互換性が違う。サポートが違う。個々の機能の名前と実装が違う。

そのように同じ物を別のレベルで抽象化して見てしまっている両者が話をする時は、お互いに垣根を乗り越える必要があります。残念ながらその垣根を越えさせられるのは、大抵弱い立場の人間(ベンダーや現場の人間)であるわけですが。

偉い人も含めて、われわれが普段周りの世界を乱暴に認識していることにも気がつくべきでしょう。世の中はそんなに単純ではない。自然にしてもシステムにしても、単純に抽象化して割り切れるようなものではないのです。

(一旦終了)