2008/01/24からのアクセス回数 12313
Agile Web Development with Railsの例題と同じ問題をSpringを使って実装を試みたときの メモです。
もう一つの目的は、Spring-MVCプラグインがどの程度実際の問題解決に役立つかを検証することです。
mavenを使ってプロジェクトを生成します。
とします。ecliseでプロジェクトを管理できるようにeclipseプラグインも起動します。
mvn archetype:create \ -DgroupId=example.cart \ -DartifactId=cart \ -DarchetypeArtifactId=spring-mvc-archetype \ -DarchetypeGroupId=jp.co.pwv.spring-mvc-archetype \ -DarchetypeVersion=1.1.1 cd cart mvn eclipse:eclipse -DdownloadSources=true
データベースは、HsqlDBのサーバを使用するため、db.propertiesの内容を修正します。
db.url=jdbc:hsqldb:hsql://localhost
最後にeclipseでcartプロジェクトをimportし、CVSに登録します。
長くなったので別タイトルにしました。
最初にProductを管理するページを作成します。
さしあたり、管理機能として
最初にProductのドメインモデルを作成します。 ドメインモデルは、example.cart.domainパッケージ内に定義します。
eclipseで以下のように入力した後、getter/setterを自動生成してください。
package example.cart.domain; public class Product { private Integer id; private String title; private String description; private String image_url; }
GenMVCプラグインのscaffoldゴールを指定して、ProductのDao、 Controller、 View、データベーステーブル を自動生成します。
その前に、GenMVCプラグインは、Productのクラスファイルを見に行くので、mavenのpackageを実行します。
mvn package mvn GenMVC:scaffold
このコマンドで、
[INFO] ------------------------------------------------------------------------ [INFO] Building Unnamed - example.cart:cart:war:1.0-SNAPSHOT [INFO] task-segment: [GenMVC:scaffold] [INFO] ------------------------------------------------------------------------ [INFO] [GenMVC:scaffold] [INFO] pkgName:example.cart [INFO] runtime.classpath:/Users/take/Documents/workspace/cart/target/classes [INFO] cls: example.cart.domain.Member [INFO] template fullpath:velocity/IDao.vm [INFO] template fullpath:velocity/Dao.vm [INFO] template fullpath:velocity/edit_stub.vm [INFO] template fullpath:velocity/list_stub.vm [INFO] template fullpath:velocity/hbm.vm [INFO] cls: example.cart.domain.Product [INFO] template fullpath:velocity/IDao.vm [INFO] template fullpath:velocity/Dao.vm [INFO] template fullpath:velocity/Manager.vm [INFO] template fullpath:velocity/EditController.vm [INFO] template fullpath:velocity/OpsController.vm [INFO] template fullpath:velocity/edit.vm [INFO] template fullpath:velocity/edit_stub.vm [INFO] template fullpath:velocity/list.vm [INFO] template fullpath:velocity/list_stub.vm [INFO] template fullpath:velocity/hbm.vm [INFO] template fullpath:velocity/servlet-stub.vm [INFO] template fullpath:velocity/sql.vm [INFO] template fullpath:velocity/applicationContext.vm [INFO] template fullpath:velocity/form-messages.vm [INFO] template fullpath:velocity/validation.vm
と出力され、必要なファイルがすべて生成されます。 再度、mvn packageを実行してtarget/cart.warをtomcatのwebappsにコピーします。
これだけで、Productのリスト表示、編集の画面が生成されます。
scaffoldの後にProductに属性を追加したくなることはよくあります。
Product の属性を変更したときの手順は以下の通りです。
通常は、これで十分ですが、以下のファイルを修正した場合にはバックアップを取ってください。
今回は、自動生成されたファイルを全く変更していないので、テーブルの削除だけを行います。
開発の途中ではデータベースのテーブルを変更したり、値を参照します。このような用途に便利なのが Ecl,ipseのプラグインDbEditです。 DbEditのインストール方法はhttp://www.pwv.co.jp/take_public_html/DevTool/DevTool_c9.html#doc1_589 を参照してください。
DbEditのTableタグを開くと以下のようにT_MEMBERとT_PRODUCTの2つのテーブルが作られています。
GenMVCプラグインでは、クラス名の前にT_を付けたテーブルが作成されます。 T_PRODUCTを削除するには、 T_PRODUCTで右マウスクリックから削除を選択してください。
Productに価格(price)を追加します。
以下のように属性priceを追加し、getter/setterを自動生成するだけです。
private Double price;
日本では価格に小数点はないのですが、ここでは例としてDouble型を使いました。
それでは、先ほどと同様にGenMVCプラグインを起動します。
mvn package mvn GenMVC:scaffold
GenMVCプラグインが生成する画面は、属性の出力順がProductクラスの定義順に並んでいないので、実際には手で修正する必要があります。
ProductのVelocityテンプレートは、main/webapp/WEB-INF/velocity/productops/以下にあります。
が一覧を表示するテンプレートです。
list.vmを見ると
parse ( "productops/list_stub.vm" )
だけです。 これは、GenMVCプラグインがlist.vmを変更しないようするためにlist_stub.vmをインクルードする 2段階で処理しています。
従ってユーザvelocityテンプレートを変更する場合には、list_stub.vmをlist.vmにコピーして編集します。 以下に順序を入れ替えたlist.vmを示します。
<html> <head> <title>Product</title> </head> <body> <h1>Listing product</h1> <table> <tr> <td>id</td> <td>title</td> <td>description</td> <td>image_url</td> <td>price</td> </tr> #foreach (${product} in ${productList}) <tr> <td>${product.id}</td> <td>${product.title}</td> <td>${product.description}</td> <td>${product.image_url}</td> <td>${product.price}</td> #set( $editLink = "/editproduct.htm?id=${product.id}" ) <td><a href="#springUrl(${editLink})">[edit]</a></td> #set( $deleteLink = "/productops/delete.htm?id=${product.id}" ) <td><a href="#springUrl(${deleteLink})">[delete]</a></td> </tr> #end </table> <a href='#springUrl("/editproduct.htm")'>add</a> </body> </html>
同様に編集画面も順序を変え、Descriptionをtextareaに変えてます。
<html> <head> <title>Products</title> </head> <body> Edit Product <form method="post" action="#springUrl("/editproduct.htm")"> #springFormHiddenInput( "product.id" "" ) <table> <tr> <td>title:</td> <td>#springFormInput( "product.title" "size='35'" )</td> <td> #springBind("product.title") <font color="red">${status.errorMessage}</font> </td> </tr> <tr> <td>description:</td> <td>#springFormTextarea( "product.description" "rows='4' cols='40'" )</td> <td> #springBind("product.description") <font color="red">${status.errorMessage}</font> </td> </tr> <tr> <td>image_url:</td> <td>#springFormInput( "product.image_url" "size='35'" )</td> <td> #springBind("product.image_url") <font color="red">${status.errorMessage}</font> </td> </tr> <tr> <td>price:</td> <td>#springFormInput( "product.price" "size='10'" )</td> <td> #springBind("product.price") <font color="red">${status.errorMessage}</font> </td> </tr> <tr> <td colspan="3"> <input type="submit" value="Save Changes"/> </td> </tr> </table> </form> </html> </body>
現在の入力フォームは、各フィールドが必須だけのチェックしかしていません。 priceに文字を入力して、Save Chageボタンを押すと
が出力されます。
これでは、エラーの原因が分かりづらいので、validation.xmlを修正してpriceをdoubleとしてふさわしい値になるようにしようと、
<field property="price" depends="required"> <arg0 key="product.price" /> </field>
を
<field property="price" depends="required,double"> <arg0 key="product.price" /> </field>
としたが、ダメでした。
原因は、Validationが行われる前に、Productの値がHTTPのパラメータからセットされるためです。
ソースをトレースした結果、bindAndValidationを使用する場合、最初にHTTPリクエストから値をセットするcommandオブジェクトへのbind操作が先行します、ここでStringからDoubleへの変換に失敗するため、その後のValidationのエラーチェックでは値がセットされていないので、requiredのエラーが追加されますが、表示されません。
対応策としては、ProductionのPriceをDoubleからStringに変えるという方法しかありません。 これでは、GenMVCプラグインを起動すると間違ったCreate table文が生成されてしまいます。
そこで、domainクラスに対応するcommandクラスをGenMVCプラグインで生成し、その属性をすべてString型としました。
更に、EditProductControllerでcomanndオブジェクトとdomainオブジェクトの値を変換するメソッドcommandToDomain, domainToCommandを追加しました。こんなに簡単にデータの設定ができるのは、CustomDataBinderの威力です。
protected void bind(Object target, Object source) throws Exception { CustomDataBinder binder = new CustomDataBinder(target, source); this.prepareBinder(binder); binder.bind(); } protected Object commandToDomain(Object source) throws Exception { Object object = new Product(); bind(object, source); return (object); } protected Object domainToCommand(Object source) throws Exception { Object object = new ProductCommand(); bind(object, source); return (object); }
これでようやく、priceのValidationが正常に行えるようになりました。
最後にスタイルシート使って衣装替えをします。 スタイルシートについては、詳しくないのでAgile Web Development with Railsの例題のスタイルを借用します。
スタイルシートを追加したlist.vmは、以下の通りです。
<html> <head> <link rel="stylesheet" href="#springUrl('/css/cart.css')" type="text/css"> <title>Product</title> </head> <body> <div id="product-list"> <h1>Listing product</h1> <table cellpadding="5" cellspacing="0"> #foreach (${product} in ${productList}) #if($velocityCount %2 == 1) <tr valign="top" class="list-line-odd"> #else <tr valign="top" class="list-line-even"> #end <td> <img class="list-image" src="#springUrl(${product.image_url})" /> </td> <td width="60%"> <span class="list-title">${product.title}</span><br/> #if ($product.description.length() > 80) #set($size = 80) #else #set($size = $product.description.length()) #end $product.description.substring(0,$size) </td> <td class="list-action"> #set( $editLink = "/editproduct.htm?id=${product.id}" ) <a href="#springUrl(${editLink})">[edit]</a><br/> #set( $deleteLink = "/productops/delete.htm?id=${product.id}" ) <a href="#springUrl(${deleteLink})">[delete]</a> </td> </tr> #end </table> <a href='#springUrl("/editproduct.htm")'>New product</a> </div> </body> </html>
ここで、スタイルシートの指定を
<link rel="stylesheet" href="#springUrl('/css/cart.css')" type="text/css">
でしているところと、説明文を一部カットするためにStringのsubstringメソッドをテンプレートから呼び出しているところに注意してください。このようにVelocityを使うとテンプレートからjavaのメソッド呼び出しができます。
スタイルシートの出力結果は、以下の通りです。
次にカタログ表示ページを作成します。Agile Web Development with Railsではカタログの表示にstoreという新しいコントローラを作成していますが、ここではProductOpsControllerを借用します。 その理由は、ProductOpsControllerがProductを扱うコントローラであり、MultiActionControllerのサブクラスなのでメソッドと同じテンプレートを作成するだけでカタログページが作れるとメリットを示すためです。
手順は以下の通りです。
public ModelAndView catalog(HttpServletRequest request, HttpServletResponse response) throws Exception { return new ModelAndView().addObject(manager.findAll()); }
catalog.vmは以下の通りです。
<html> <head> <link rel="stylesheet" href="#springUrl('/css/cart.css')" type="text/css"> <title>Store</title> </head> <body id="store"> <h1>Your wine catalog</h1> #foreach (${product} in ${productList}) <div class="entry"> <img src="#springUrl(${product.image_url})" /> <h3>${product.title}</h3> $product.description <br> <span class="price">$numberTool.format("##0", $product.price)</span> </div> #end </body> </html>
カタログページは、次のように表示されます。
最後にカートへの追加ボタンを入れます。
価格の後に次の行を挿入します。
<form method="post" action="#springUrl("/cartops/add.htm")"> #springFormHiddenInput( "product.id" "" ) <input type="submit" value="Add to Cart"/> </form>
画面では次のように表示されます。
カートの処理に進む前に注文項目を作成します。
ここでのポイントはカートの注文項目がそのまま注文にリンクされるようにすることです。
それでは、注文項目のdomainクラスを作りましょう。
package example.cart.domain; public class LineItem { private Integer id; private Integer quantity = new Integer(1); private Integer productId; private Product product; private Integer orderId; public void addQuantity(Integer quantity) { this.quantity += quantity; } public Double getPrice() { return (new Double(quantity*product.getPrice())); } }
として、getter/setterを自動生成してください。
この後は、いつものようにGenMVC:scaffoldを実行します。
mvn package mvn GenMVC:scaffold
カートの処理を行う、CartServiceとカートに対する要求を処理するCartOpsControllerを追加します。
最初にCartServiceを追加します。CartServiceでは
を行います。
package example.cart.service; import java.util.Iterator; import java.util.Map; import java.util.TreeMap; import example.cart.domain.LineItem; public class CartService { private Map<Integer, LineItem> lineItemMap = new TreeMap<Integer, LineItem>(); public void addLineItem(LineItem item) { Integer key = item.getProductId(); LineItem lineItem = (LineItem)lineItemMap.get(key); if (lineItem == null) lineItemMap.put(key, lineItem); else lineItem.addQuantity(item.getQuantity()); } public Double getTotal() { double total = 0; Iterator itr = lineItemMap.values().iterator(); while (itr.hasNext()) { LineItem lineItem = (LineItem)itr.next(); total += lineItem.getPrice(); } return (new Double(total)); } public Map getLineItemMap() { return lineItemMap; } }
ProductOpsContollerをコピーしてCarOptsControllerを作成します。
private CartService cartService; private ProductManager manager; private void setupCartService(HttpServletRequest request) { ApplicationContext co = WebApplicationContextUtils. getRequiredWebApplicationContext( request.getSession().getServletContext()); cartService = (CartService)co.getBean("cartService"); }
listでは、
public ModelAndView list(HttpServletRequest request, HttpServletResponse response) throws Exception { setupCartService(request); return new ModelAndView().addObject("lineItemList", cartService.getLineItemMap().values()); }
addでは、
public ModelAndView add(HttpServletRequest request, HttpServletResponse response) throws Exception { Integer productId = new Integer(ServletRequestUtils.getRequiredIntParameter(request, "id")); setupCartService(request); Product product = manager.findById(productId); LineItem item = new LineItem(); item.setProductId(productId); item.setProduct(product); cartService.addLineItem(item); ModelAndView modelAndView = new ModelAndView(); modelAndView.addObject("lineItemList", cartService.getLineItemMap().values()); modelAndView.setViewName("cartops/list"); return modelAndView; }
cartopsフォルダをsrc/main/webapp/WEB-INF/velocityに作成し、list.vmを追加します。
簡単な項目一覧をlist.vmで出力します。
<html> <head> <title>Cart</title> </head> <body> <h1>Your Wine Cart</h1> <ul> #foreach ($lineItem in $lineItemList) <li> $lineItem.quantity × $lineItem.product.title </li> #end </ul> </body> </html>
CartOpsControllerは、GenMVCプラグインの影響を受けないようにserver-def.xmlに定義します。
<bean id="cartOpsController" class="example.cart.web.CartOpsController" parent="baseProductController"/>
ブラウザーからカタログ画面を表示(http://localhost:8080/cart/productops/catalog.htm)し、Add to Cartボタンを押すと以下のような画面に遷移します。
次に小計と合計の表示とEmpty cartボタンを追加し、ひとまずcartの完成としましょう。
modelAndView.addObject("total", cartService.getTotal());
public ModelAndView emptyCart(HttpServletRequest request, HttpServletResponse response) throws Exception { setupCartService(request); cartService.getLineItemMap().clear(); ModelAndView modelAndView = new ModelAndView(); modelAndView.setViewName("redirect:/productops/catalog.htm"); return modelAndView; }
最終的な画面は、以下のようになります。