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も同じだ」という認識ではプロジェクトは回りません。発生する障害の種類がまるで違う。互換性が違う。サポートが違う。個々の機能の名前と実装が違う。

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

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

(一旦終了)

2008年5月20日火曜日

「同じ」と「違う」(4)~ 抽象化

前回の相対性理論に関する記述を少し補足します。
相対性理論に含まれる数式は何語に翻訳されようとも当然同じです(というか翻訳の対象ではないですよね)。これは「記号」の配列だからです。しかし相対性理論の「解釈」は異なる可能性がある。量子力学を理解している人と、講談社ブルーバックスを斜め読みした人とは当然解釈がことなる。そういうことです。

では「似ている」について。

通常「同じ」と混同されている言葉に「似ている」があります。

例えば「Javaの参照とC言語のポインタ」は似ている」⇒「要するにJavaの参照ってのはポインタだよね」。このようにして人は新しい物事を理解します。つまり「似ている」というのは物事を把握するための重要なステップなのです。

(余談)「Javaの参照とC言語のポインタを混同するな!」と怒る人もいそうですが、Javaで普通に開発するのであればその程度の理解で十分だと思います。

しかし、「似ている」(以降はそれっぽく「アナロジー」と呼びます)とするためにはその前段階である特徴なり構造なりを取り出す必要があります。Javaの参照とポインタの例で言えば、前者はクラスの実体ではなくその(比喩的には)場所を指しています。ポインタは実メモリの番地を指している。まず、それぞれが「場所を指している」ということを取り出す必要があります。これを抽象化と言います。

事物を抽象化して構造なり特徴なりを取り出し、別のものとアナロジーによって理解して「同じ」だと判断する。ここには二つの「切り捨て」(捨象)が含まれています。

まず抽象化によって細部(具象)が切り捨てられる。例えば虫について言えば、ゴキブリだろうが蝿だろうが「足を6本持った存在」まで切り捨てられます。これが一つ目の捨象。次にその足がどのような足かに関わらず「足が6本なら、同じだ」とされます。これが二つ目の捨象。

抽象化とアナロジーによって世の中は単純に見えます。
虫に何かいちいち構っていられない。自分が乗る電車にいちいちこだわってはいられない。虫は虫だろう。電車は電車だ。そう断ずることによって、考える手間がずいぶん省けます。

しかし当然副作用もあります。つまり「同じ」と思い込んでいるけれども実は全てが違う。つまり誤解をしている可能性だって大いにある。もちろん、抽象化して「同じ」だと断じた結果、本質を取り出すこともできます。

抽象化した結果、無理解に陥ったのか。それとも本質を見抜いたのか。次にそれが重要となります。

(続く)

Springフレームワークの印象

ざくっといじってみた結果の、現時点での印象を書きます。

【良い点】
・とにかくデザインが素晴らしい
Spring上で開発されたアプリは、コンポーネント間の結合粒度が粗くなります。その結果↓
 ⇒ インターフェースがきちんと分かれる
 ⇒ 各コンポーネント(クラス)が独立して開発可能です。変更の影響範囲も分かり易い。つまり Spring で開発していると「UTやっておけば大丈夫」という「おおむね安心という心証」を得る機会が多くなります。これは素晴らしいことです。

Struts はシンプルで機能を絞り込んでいるが故に、開発側の自由度が上がります。つまりいろいろなことが出来てしまいます。しかし、Spring 上でアプリを開発した場合は、当面「Springのお作法に従わなければならない」という不自由さはあるものの、結果としてコードがキレイに分割されることになります。
自身のデザインではなく、自身の上で稼働するアプリのデザインを実に美しく規定していますこれはすごいことです。

XML地獄は健在だと以前に書きましたが、コンポーネント(クラス)同士を疎結合するためのXMLであるとすれば、その欠点を補って余りあるデザインになっていると思います。

【中立】
POJO(Pure Old Java Object)を標榜していますが、私見ではギリギリのところで踏みとどまっている印象があります。

私の感覚では「ツールによって開発すると便利だが、いざとなったら自分でXMLやJavaソースをゴリゴリ編集してメンテナンスできる」というのが最低限の基準です。
この基準からすると、例えばXML定義ファイルが完全に正となってそこから全て生成するようなツールはアウトです(アウトプットに手を入れられなくなるため)。他にもベンダーが提供する開発ツールを離れると事実上何もできなくなるような技術もダメ(例えばEJB。WebSphere と Rational Application Developer の連携がよい例ですね)。

Ant と エディタ(あるいはeclipseの標準機能)、百歩譲って xDoclet が限界で、それ以上は勘弁して欲しいというのが個人的な基準ですが、Spring は何とかその基準をクリアしているようです(おまえの勝手な基準だろ?と言われればその通りですが)。

「何とかクリア」というのは、やはりブラックボックス的な要素(Daoなど)があるからです。しかし、そこはいざとなったらソースを読めば何とかなるはずということは分かっていて、しかしそのソースを読む時間も体力もないな、というあたりの迷いが「中立」という立場につながっています。

それから学習体力もバカになりません。結構 Spring を理解して使うのは大変だと思います。(Web で Spring を試していらっしゃる方は皆さん優秀です!びっくりします)

【現時点での要注意事項】
・ドキュメントが足りない
過渡的なものでしょうが情報が足りません。前回、チュートリアルを拡張しようと頑張ってみたのですが、結局失敗しました。その経験から(あつかましくも)言ってます。(5/23追記:成功しました。「Springフレームワークの続き(リベンジ)」
「おまえの力が足りない」と言われればその通りです。それは認めましょう。しかし「一覧表から項目を選択して、編集のために次画面に渡す」というのはどのWebアプリにも含まれるような、極一般的な作りだと思います。それが簡単に出来ないのはやはり問題だと思われました。

具体的には
-radioボタンを Spring的に動的に生成する方法が分かりませんでした。(ゴリゴリとコーディングしてしまいました)
-前画面からデータを受け取って次の画面に描画することができませんでした。(formBackingObjectのrequestからpostデータが取れませんでした)

恐らくは「私の Spring に対する無理解」「当初のデザインが根本的に失敗している」「他に有効なメソッドがあるのを見落としている」といった原因があるのでしょうが、英語のページ含めてどんなに調べても達成できなかったのは残念なところです。

【総評】
確かに素晴らしいアプリケーションです。しかし例によって(この業界でありがちなことですが)喧伝され過ぎているきらいがあります。
(ある種の人からはバカにされるかもしれませんが)Strutsと生のJDBCでゴリゴリ書くのと比べてどちらが良いか。迷って迷ってやはり Spring を選んだ方がよいような気がしないでもないということはないと言い切るにはいささかやぶさかである、といったところでしょうか。

2008年5月16日金曜日

Springフレームワークの続き(画面追加~挫折)

Springフレームワークのチュートリアルをベースに少し機能を追加してみます。
(注意:最後の一歩で頓挫しています。)
(5/23追記:Springフレームワークの続き(リベンジ)で一応成功しました)

チュートリアルでは価格を割増しするだけのビジネスロジックを作成しましたが、初期の価格を設定するロジックを追加します。
すでに存在するプロダクトのリストを取得してフォームとして画面に表示し、編集/入力された新規の価格をDBに反映するロジックです。

二つの画面が増えることになります。
1.最初の画面から変更対象選択画面への遷移
 編集画面に現在登録されているプロダクトを表示。
 変更対象選択画面では、チェックボックスで変更対象を選択することができます。
2.価格設定画面へ遷移
 '1'で選択された項目が表示されます。価格を設定することができます。

【手順概要】
Productsインターフェースに以下のメソッドを追加します。
public void setPrice(int id, int price);

SimpleProductManagerでsetPrice(int id, int price)を実装します。

SimpleProductManagerTestでsetPriceのテストを行います。

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

SelectInventoryコントローラを追加します。

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

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

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

リンクを追加します。

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

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

SetPriceFormControllerを追加します。

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

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

【手順詳細】
Productsインターフェースに以下を追加します。
public void setPrice(int id, int price);

SimpleProductManagerを実装します。
 public void setPrice(int id, double price) {
List<Product> products = productDao.getProductList();
if (products != null) {
for (Product product : products) {
if (product.getId() == id) {
product.setPrice(price);
productDao.saveProduct(product);
break;
}
}
}
}

次にSimpleProductManagerTestです。
    private static int CHAIR_ID = 1;
private static int TABLE_ID = 2;
private static int POSITIVE_PRICE_INCREASE = 10;
private static int NEGATIVE_PRICE_INCREASE = 10;
(中略)
public void testSetPrice() {
productManager.setPrice(TABLE_ID, TABLE_PRICE_UPDATED);
productManager.setPrice(CHAIR_ID, CHAIR_PRICE_UPDATED);

List<Product> products = productManager.getProducts();

for (Product product: products) {
if (product.getId() == TABLE_ID) {
assertEquals(TABLE_PRICE_UPDATED, product.getPrice());
} else if (product.getId() == CHAIR_ID) {
assertEquals(CHAIR_PRICE_UPDATED, product.getPrice());
}
}
}

次に変更対象選択用Bean(SelectProduct)を追加します。
以下のサイトを参考にしました。LazyListは使っていません。(LazyListでは動きませんでした。なぜかは未調査)
<a href="http://mattfleming.com/node/134">Dynamic list binding in Spring MVC</a>
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());
//logger.info("Descriptions set to "+strs);
private List<Product> products = null;
/*
LazyList.decorate(new ArrayList(),
FactoryUtils.instantiateFactory(Product.class));
*/

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

SelectInventoryFormControllerを追加します。
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.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;
}
public ModelAndView onSubmit(Object command)
throws ServletException {

logger.info("Executed.");

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

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

public ProductManager getProductManager() {
return productManager;
}
}

変更対象選択画面(selectproduct.htm)を追加します。
<%@ 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></head>
<body>
<h1><fmt:message key="selectproduct.heading"/></h1>
<h3>Products</h3>
<form:form method="post" commandName="setPrice">
<c:forEach items="${selectProduct.products}" var="prod" varStatus="row">
<input type="radio" id="id" name="id" 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>

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

hello.jspにリンクを追加します。
<a href="<c:url value="selectproduct.htm"/>">Select product</a>

価格設定用Bean(SetPrice)を追加します。
package springapp.service;

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

public class SetPrice {
/** Logger for this class and subclasses */
protected final Log logger = LogFactory.getLog(getClass());
private int id;
private String description;
private double price;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
logger.info("id:"+id+" set.");
}
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("description:"+description+" set.");
}

}

バリデータ(SetPriceValidator)を追加します。
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 SetPriceValidator 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 SetPrice.class.equals(clazz);
}

public void validate(Object obj, Errors errors) {
SetPrice ps = (SetPrice) obj;
if (ps == null) {
errors.rejectValue("price", "error.not-specified", null, "Value required.");
}
else {
logger.info("Validating with " + ps + ": " + ps.getPrice());
double price = ps.getPrice();
if (price <= minPrice) {
errors.rejectValue("price", "error.too-cheape",
new Object[] {new Integer(minPrice)}, "Value too low.");
}
}
}

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

public int getMinPercentage() {
return minPrice;
}

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

public int getMaxPercentage() {
return minPrice;
}

}

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

import java.util.Enumeration;

import org.springframework.beans.factory.parsing.ParseState.Entry;
import org.springframework.web.bind.ServletRequestUtils;
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.PriceIncrease;
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();
Enumeration<String> enume = request.getAttributeNames();
while(enume.hasMoreElements()) {
String key = enume.nextElement();
logger.info(key+":"+request.getAttribute(key));
}
*/

logger.info(getFormSessionAttributeName(request));

String str = ServletRequestUtils
.getStringParameter(request, "id");

SetPrice ret = new SetPrice();
// 最後は決め打ちのハードコード(泣)
int i = 1;
for (Product product : productManager.getProducts()) {
if (product.getId() == i) {
ret.setId(product.getId());
ret.setDescription(product.getDescription());
ret.setPrice(product.getPrice());
break;
}
}
return ret;
}

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

public ProductManager getProductManager() {
return productManager;
}

}

価格設定用画面(setprice.jsp)を追加します。
<%@ 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="setprice.heading"/></h1>
<form:form method="post" commandName="setPrice">
<c:out value="${setPrice.description}"/>
<form:input path="price"/>
<form:errors path="price" 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="/setprice.htm" class="springapp.web.SetPriceFormController">
<property name="sessionForm" value="true"/>
<property name="commandName" value="setPrice"/>
<property name="commandClass" value="springapp.service.SetPrice"/>
<property name="validator">
<bean class="springapp.service.SetPriceValidator"/>
</property>
<property name="formView" value="setprice"/>
<property name="successView" value="hello.htm"/>
<property name="productManager" ref="productManager"/>
</bean>


以上ですが、残念ながら途中で頓挫してしまいました。

DAOと連携したDBの更新は成功するのですが(IDハードコードで確認済み)
一覧から更新対象を選択し、次の画面にIDを渡すことがどうしても出来ませんでした。
formBackingObject(HttpServletRequest request)で、requestからradioの値を引っ張ることができません。悔しいのですが二日間チャレンジして出来なかったので、これで諦めます。

#なんで "String str = ServletRequestUtils.getStringParameter(request, "id");"でPOSTされたidが取れないんだ!!!

(5/23追記)SimpleFormControllerはrequestをクリアしてしまうようですね。
"id"は前の画面のFormにちゃんと入っていました。これを生のHttpSessionに登録し、次画面で受け取ることができます。Spring的にキレイではないとは思いますが。

以上。

2008年5月14日水曜日

「同じ」と「違う」(3)~「同じ」から「似ている」へ

対象を事物に絞れば「同じ」モノは存在しない、と言いました。

違和感ありますよね。「同じ」という言葉は非常にリアリティを持っている。実は同じものが存在しないことを認めると、人間にとって世界の根本的な成り立ちが怪しくなるのです。

確かに、「同じ」ものが存在するような気がする。では何が「同じ」なのでしょうか。

養老猛司氏は、テープに録音された音声としてのテキストと文書に書かれたテキストが同じであるという例を上げ、「同じ」なのは情報である。と述べました。情報それ自体は媒体に関係なく「同じ」である、と。

氏の意見に同意してもよいのですが、私としてはもう少し踏みたい。
「情報」という言葉にすると「同じ」かどうかは怪しいと思うからです。理由は簡単。例えば(何でもいいのですが)夏目漱石の小説を読んだとします。私が読むのと他の人が読むのとでは当然感想が違います。確かに文字の並びは同じといってよいでしょう。しかし、同じ本を読んだとしても同じものを見ていることにはならないのです。あるいは聖書。キリスト教者が読むのとそうではない人が読むのとでは、全く受け止め方が違ってくるでしょう。

私は、同じなのは「記号の配列」であると思います。厳密に言えば。

でも情報だって同じだろう。翻訳されたら記号の配列は変るが、相対性理論は日本でもアメリカでも同じ相対性理論だ。その通りです。しかし、この議論の前に私は「同じ」という言葉をもう少し緩めて「似ている」という範囲に広げたいと思っています。

アメリカの相対性理論も日本の相対性理論も、究極的には同じだと思います。

しかし、やはり人によって理解のレベルも着眼点も解釈も異なるはずです。ですから現実的には「相対性理論は相対性理論である」というよりも、人の解釈が「似ている」から「限りなく同じ」のグラデーションのどこかに分布する、というのが妥当だと思います。私と現役の科学者が相対性理論について語り合ったって、どこにもたどり着かないでしょうから。

次に「似ている」について、もう少し踏み込んでみましょう。

(続く)

2008年5月13日火曜日

「同じ」と「違う」(2)~「同じ」は存在しない

自分は自分である
ゴキブリはゴキブリだ

以上は「AはAである」という構造を持っています。言い換えればAとAは同じだ、ということです。

では

自分は自分ではない
ゴキブリはゴキブリではない

はどうでしょう。AとAが違う、ということでこれはほとんど論理的には矛盾していますね。

以上より、A=Aは真だし、A≠Aは偽だということが一般に言えます。

(何をつまらんことを言っているんだ、という声が聞こえてきそうです。もう少々お付き合い下さい)

しかし、実はここに大きな問題があるのです。

具体的に考えて見ましょう。具体的に、というのは実際に存在する事物についてです。ここでは事物を空間的に位置を占め、重さを持つものとします。

すると、どうなるか。
目の前に缶コーヒーが二本あるとします。同じメーカーの、同じ種類の缶コーヒーです。同じ自動販売機で、午前と午後に一本ずつ購入したものです。
この二本の缶コーヒーは「同じ」なのでしょうか。

ブランドが同じ。種類が同じ。価格が同じ。同じに決まっている。

果たしてそうでしょうか。
「同じ」としている主語に注目して下さい。「同じ」としているのは缶コーヒーではありませんね。すべて缶コーヒーの言わば属性とも言うべきものです。缶コーヒーそれ自体ではない。もう少し言えばその属性も先ほど定義した「事物」ではない。

缶コーヒーそれぞれは、同じではないのです。なぜなら空間的に別の位置を占めているからです。座標が違うものは同一ではありえません。つまり、世の中に存在する事物に、同じ物は一つとして存在しないのです。

「同じ」は実体=事物的な存在ではありません。「同じ」とは価値評価に過ぎません。

言い換えれば「同じ」は人間の頭に存在する概念でしかありません。

ところがあたりを見れば「同じ」事物があふれているように見える。それは頭が周りの事物を「同じだ」「同じだ」と強制的にラベリングしているからなのです。

(続く)

東京三菱のシステム障害に思うこと

原因はカタカナで転送すべきだったデータを漢字で送っていたためだったそうです。

早速「テストが甘いかったんじゃないか」などという結果だけを見て偉そうなことを言うひとがちらほら出てきています。この手の原因と結果を転倒させる人は本当に始末に悪いですね。

確かにテストが甘かったのは事実でしょう。外部システムとの正常系テスト(記帳の回数オーバーのハンドリングはほとんど正常系です)をしていれば、事前に発見できたはずですから。しかし、部外者がそのレベルのことをしたり顔で言うのは、プロ野球の監督の采配に文句をつける酔っ払いオヤジとかわりません。

安易に時系列(原因⇒結果)を逆転させてはなりません。なぜなら、それは安直な分析につながるからです。今にしてみればあまりに自明だったことが、必ずしも現在自明であるとは限りません。むしろ原因と結果という図式を単純に信奉して「ああすればこうなる」式に(これも養老節ですな)手を打つ方が危険です。原因と結果という図式は、本来は不可能であるはずの未来の予測を、いとも容易に見せてしまうまやかしの図式でもあるからです。

送信データミスが原因で、結果として障害が発生した、という解釈は正しいです。それを逆転して「送信データミスがなければ障害は起こらなかったのに」と考えるのも、まあ妥当な感想でしょう。でも、そこから「テストが甘かった。私の成功体験では・・・」と断ずるのは早計です。

システム構築中は、取り返しのつかない意思決定の連続です。そして意思決定には常にリスクが伴う。人間ですからどこかで誤ることは十分ありえるからです。そのリスクをいかに取るかが大事なのです。

今回について言えば(結果を知っている部外者からは)明らかに必須と思えるテストの実施がなぜ漏れたのかが問題です。
外部システムとのインターフェースがある以上、結合テストはほとんど必然です。漏れようがない。それから今回の問題は正常系のテストでカバーできるはずです。異常系・障害系のテストなら時間との兼ね合いで割り切る判断もあるかもしれませんが、正常系のテストが行われなかった理由はほとんど謎です。

私には以下の要因が思い当たります(どれも当たり前のことですが・・・)。どちらも「ピーク6000人を投入した」「失敗が絶対に許されない」と言われるからこそ、気になる点です。

1.作業に優先順位が定められていたか
2.現場レベルのエンジニアに無用なプレッシャーが掛かっていなかったか

人が多くても、適切にリソースが配分されていなければ意味がありません。その人員は、何の仕事をしていたのでしょうか。システムに無理解な上層部(残念ながら昨今はリーダークラスにも多いですね)の思いつきに対応するために使われたのではないでしょうか?本当に現場に必要とされるタスクに、回されたのでしょうか。そもそも、何が重要か、適切に判断されていたのでしょうか?

それから「失敗は許されない」というスローガンは、前にも書きましたが実に百害あって一利なしなのです。このスローガンによって引き起こされる副作用には、恐らくは瑣末事大主義と優先順位付けの不能でしょう。失敗をできるだけ起こさないためには、リスクをきちんと評価して、重要な作業を優先しなければなりません。しかし「失敗は許されない」無思考に陥ると、作業の割り切りやリスクテイクが出来なくなるのです。

今回発生した障害の裏に何があったのかは知りませんが、同じエンジニアとしてはこれ以上何事も起こらないことを祈っています。

2008年5月12日月曜日

「同じ」と「違う」(1)

埴谷雄高氏の「死霊」に出てくる有名な言葉に「自同律の不快」というのがあります。私はこの言葉を、カントの「同一律」と物自体、ニーチェの力への意志、それから養老猛司の「同じ」ってやつだな、うんうん、と勝手に解釈し、それなりに納得したつもりでいました。

ある日、もう半年以上前でしょうか、日経新聞の夕刊で「死霊」に関する記事を読みました。そこでは「自同律の不快」のことを『「私は私である」と言い切るのは不快だという感覚』であると解説していました。
さすが日経、浅はかな解釈であることよ、と鼻で笑っていたのですが、Webで見る限りどうも一定のお墨付きを得た解釈のようです。むしろ埴谷雄高氏がこの言葉を使って自ら語ったことがあるような様子すら見受けられました。(気楽なエッセーなので言質を取ったりしち面倒くさいことはしませんが)

改めて読めば、確かに大きく的を外した解釈ではなさそうです。「私は私だ」という経験上自明である言明に不快感を示す、一体どういうことだ?と興味を引きますし、少なくともA=Aが不快だ、と言うよりは分かり易い。

しかし私は文字通り「A=A」が不快である、という意味で「自同律の不快」という言葉を受け止めたいと思います。

「私が私である」はもちろん(養老猛司風に言えば)「虫は虫だ」というのも自同律です。A=Aであるという形式を元に世界を構成する理性の乱暴さ。それに対抗する「自同律の不快」は普遍的な共感を得ることができると思うのです。

(続く)

Springフレームワークを使ってみた

Springフレームワークに含まれる、以下のチュートリアル文書に従ってSpringフレームワークを試してみました。
spring-framework-2.5.3/docs/MVC-step-by-step/html_single/index.html

まだ第一印象レベルですが、その感想です。

■XML地獄は健在
Strutsと同様、SpringでもXML設定ファイル地獄が存在します。
単純に比較するのも酷ですが、多機能なだけSpringの方がとっつきにくいという感想です。

XDocletを使えば少しはマシになるのでしょうか。

■DAOはイケてる?
DAOは結構イケてる気がします。
Torque(だったかな?)よりも簡単そうですね。(チュートリアルが優れているせいかも?)

■Spring's JDBC framework
神経を使う connect / close をしないで済むのが素晴らしい。
WebSphere(v5以降?)ではUserTransactionを使うのですが、JNDIを使わないだけシンプルで良いと思います。(WASで同じことをやる場合、JNDIの登録作業が発生して面倒くさいです。WASの実装に依存するところもブラックボックス的で気に入りませんし)

■総評
目的は違うのでしょうが、Strutsよりマシかどうかは分かりません。
Springで提供している機能がバッチリ要件にハマれば採用の価値はあると思います。
しかし、それにしても相応の学習負荷と開発ルールの策定負荷があります。

例によって喧伝されている技術ではありますが、難解さ/取っ付きの悪さが目立ちます。

Strutsと同様Springそれ自体は優れたアプリケーションなので、デザインを勉強するにはよいと思います。

以上。

2008年5月7日水曜日

「醜い親子」??

『トランヴェール - Train Vert』という無料雑誌があります。JR東日本の新幹線の座席ポケットに、通販雑誌と共に配られています。こぢんまりとまとまっていて広告臭も薄く、ぼーっと目を通すには悪くない雑誌です。

このGW、子供を連れて新幹線に乗る機会がありました。新幹線に乗ってしばらくして、子供たちがおもちゃやお菓子に目を奪われておとなしくなっている隙に、缶ビールの栓を空けてこの雑誌の5月号を手に取りました。何気なく読み始めたのですが、冒頭に掲載されていた内館牧子氏のエッセイにいきなりいやな気分にさせられました。

内容は、コンビニのサンドイッチとおにぎりを泣きながら拒否する未就学児と、それをなだめる親についてのエッセイです。
この未就学児は内館氏の足元に「ゴミ」などと言ってサンドイッチを投げつけ、親は彼(彼女)を叱りもせず海苔巻を与えて一緒に食べたそうです。エッセイは、日本の歴史上の少年兵士と目の前の親子を比較して親子を醜いと断罪するものでした。曰く昔の少年は屋外ではモノを食べてはならないと教えられてきた、飢えを我慢して戦いに臨んだ少年に比べ、この親子の醜さはなんだ、云々

言わんとすることは分かるのですが、一言でいえば「大人気ないなあ」という感想です。その場で「食べ物を粗末にさせてはならない」と親を注意するのならともかく、当の本人たちには何も言わずに言わば公共の場であるところの雑誌で糾弾するというのはどうも卑怯ではないかな、という気がしました。

子供を持つ親の立場からすれば、内館氏に断罪された彼らの気持ちも分からないではありません。
・不機嫌な子供は実に、実に、実に扱いづらい
・子供を怒る親を不愉快に思う人は多い(泣く子供よりも子供を真剣に怒る親の方が醜い)
 ⇒ 子供はさっさとなだめたい(内館氏のような人がいるから!)
(もう一つ言えば)
・コンビニのサンドイッチはかなりいまいちである

だから、私にとってすれば、彼らが公共の場で貶められるのは残念な気がしてなりません。一歩間違えば、つまり家族の気分や疲れ次第では、容易に起こり得る状況だからです。(ついでに言えば仮に私が断罪された親の立場だったとして、面と向かって文句を言われたら素直に謝りますし、別にしかられたことにクヨクヨしません。)

いやはや。

2008年4月28日月曜日

ドラえもん 緑の巨人なんとか

期待せずに見に行ったのですが、にもかかわらず、残念ながらかなりの駄作でした。

安直な「自然との共生万歳」的なサクサクストーリーであることは覚悟していました。しかし物語以前に展開やセリフ回し、人物描写がお話にならないくらい乱暴で脈絡がないです。ラピュタのパクリもイタい。シャレにもパロディにもなってないし、何で??と違和感のみが残る感じでした。
途中から「こりゃひどい」と思って目と耳をシャットダウンして耐えていましたが、辛かった・・・

子供も「いつまで経っても宇宙船から出られなくてつまらない」と言ったので、途中で出てきました。

映画版プリキュアの方がよほど面白かったです。

2008年4月24日木曜日

これは強力!XDoclet

非常に便利そうに見えるのですが、EJBやStrutsと共に使われるため、この人単体で何ができるのかが見えづらいXDocletです。
実際に試してみましたが、かなりマニアックですが、相当使えるツールとみました。
(そもそも目的が違うかもしれませんが、JJTreeとかjavaccとかで苦労したことがサクっとできる感じです)

かなりシンプルに試してみました。

【概要】
XDocletはAntと連携して動きます。
今回、Ant1.7、XDoclet1.23を使いました。

(1)ant buildファイルにXDocletタスクを定義します。
タスクにはejbとかStrugsとかSpring用にすでにカスタマイズされたものがあるようですが、今回はシンプルに実行するため「xdoclet.DocletTask」を使います。

antを実行すると、ソースディレクトリに含まれるJavaソースを解析してくれます。クラス名や、JavaDocコメント形式のコメントそれぞれにアクセスできるようになります。

(2)次に.xdtファイルを準備します。
特にXMLフォーマットにこだわる必要はなさそうです。CSVなどにも出力できますし、当然JavaソースをジェネるのもOK(なんと素晴らしい)
<XDtClass:forAllClasses>タグなどを使って、クラス名やコメントにアクセスします。

(3)Javaを準備します。
JavaDocコメント形式
 /**
  * @mytest.tag3
  * name="foo"
  */
で書いておくと、XDocletが解析してくれます(ただのコメントでは駄目です。)

(4)Antタスクを実行すると、xdtに解析結果を埋め込んだ形でアウトプットが作成されます。

EJBやStruts、SpringのXML定義ファイルや、Setter, Getterしか持たない単純なBeanクラスなどをジェネるのに最適!素晴らしいツールです。

【詳細】
ディレクトリ構造はこんな感じ。


まずはAnt build fileです。
<?xml version="1.0" encoding="UTF-8"?>
<project basedir="./" default="build" name="xdoclet-test">
<!-- Properties -->
<property name="xdoclet.home" value="F:/libraries/xdoclet-1.23"/>
<property name="src.dir" value="src"/>
<property name="xdoclet.template.dir" value="xdt"/>
<property name="build.xml.dir" value="xml"/>
<property name="build.dir" value="bin"/>

<!-- Path definitions -->
<path id="xdoclet.classpath">
<fileset dir="${xdoclet.home}/lib">
<include name="*.jar"/>
</fileset>
</path>

<!-- Targets -->
<target name="compile">
<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="xdoclet.classpath"/>
</javac>
</target>

<target name="build" depends="compile">
<taskdef classname="xdoclet.DocletTask" name="doclet"
classpathref="xdoclet.classpath" />
<doclet destdir="${src.dir}"
excludedtags="@version, @author, @todo" force="true"
verbose="false">
<fileset dir="${src.dir}">
<include name="**/*.java" />
</fileset>
<template destDir="${build.xml.dir}"
destinationFile="test.xml"
templateFile="${xdoclet.template.dir}/test_xml.xdt"
subTaskName="Generate XML file" />
<template destDir="${build.xml.dir}"
destinationFile="test.txt"
templateFile="${xdoclet.template.dir}/test_print.xdt"
subTaskName="Generate Class info" />

</doclet>
</target>
</project>

次に'*.xdt'です。二通り準備しました。
■test_print.xdt
<XDtClass:forAllClasses>==================================================
Class name :
<XDtClass:fullClassName/>
Class tag values :
<XDtClass:forAllClassTags tagName="mytest.tag1">
<XDtClass:classTagValue
tagName="mytest.tag1" paramName="value"/>
Field tag values :
<XDtField:forAllFields>
<XDtField:fieldTagValue
tagName="mytest.tag3" paramName="name"/>
</XDtField:forAllFields>
Method tag values :
<XDtMethod:forAllMethods>
<XDtMethod:ifHasMethodTag tagName="mytest.tag2">
<XDtMethod:methodTagValue
tagName="mytest.tag2" paramName="name"/>
</XDtMethod:ifHasMethodTag></XDtMethod:forAllMethods>
</XDtClass:forAllClassTags>
</XDtClass:forAllClasses>

■test_xml.xdt
<?XML version="1.0" encoding="UTF-8"?>
<XDtClass:forAllClasses>
<class name="<XDtClass:className/>">
<variables>
<XDtClass:forAllClassTags tagName="mytest.tag1">
<tag value="<XDtClass:classTagValue
tagName="mytest.tag1" paramName="value"/>"/>
</XDtClass:forAllClassTags>
</variables>
<fields>
<XDtField:forAllFields>
<tag value="<XDtField:fieldTagValue
tagName="mytest.tag3" paramName="name"/>"/>
</XDtField:forAllFields>
</fields>
<methods>
<XDtMethod:forAllMethods>
<XDtMethod:ifHasMethodTag tagName="mytest.tag2">
<tag value="<XDtMethod:methodTagValue
tagName="mytest.tag2" paramName="name"/>"/>
</XDtMethod:ifHasMethodTag>
</XDtMethod:forAllMethods>
</methods>
</class>
</XDtClass:forAllClasses>

次にJavaソース(1本/2本)
package testpack;

/**
* @mytest.tag1
* value="hoge1"
*/
public class XDocletTest0 {

/**
* @mytest.tag3
* name="foo"
*/
public static String STATIC_VAR = "hoge";
int i = 0;

/**
* @mytest.tag2
* name="piyo2"
*/
public static void method1() {
return;
}

private String method2(String arg) {
return arg;
}

}

(2本/2本)
package testpack;

/**
* @mytest.tag1
* value="hoge1-2"
*/
public class XDocletTest1 {

/**
* @mytest.tag3
* name="foo-2"
*/
public static String STATIC_VAR = "hoge";
int i = 0;

/**
* @mytest.tag2
* name="piyo2-2"
*/
public static void method1() {
return;
}

private String method2(String arg) {
return arg;
}

}


実行すると、以下の二つのファイルが作成されます。
う~ん。強力。(って不親切過ぎかな)
==================================================
Class name :
testpack.XDocletTest1
Class tag values :
hoge1-2
Field tag values :
foo-2
Method tag values :
piyo2-2

==================================================
Class name :
testpack.XDocletTest0
Class tag values :
hoge1
Field tag values :
foo
Method tag values :
piyo2

<?XML version="1.0" encoding="UTF-8"?>

<class name="XDocletTest1">
<variables>
<tag value="hoge1-2"/>
</variables>
<fields>
<tag value="foo-2"/>
<tag value=""/>
</fields>
<methods>
<tag value="piyo2-2"/>
</methods>
</class>

<class name="XDocletTest0">
<variables>
<tag value="hoge1"/>
</variables>
<fields>
<tag value="foo"/>
<tag value=""/>
</fields>
<methods>
<tag value="piyo2"/>
</methods>
</class>


余裕が出たら、もう少し細かく解説します。

参考)
春のXDoclet中心生活

2008年4月17日木曜日

プロジェクトにおける不安について

プロジェクトに参画した人は、普通さまざまな不安を抱きます。

「このコードをリリースして大丈夫だろうか」(開発者)
「無事リリースできるのだろうか」「明日はどんな障害が発生するだろうか」(マネージャー)
「障害が発生して業務を止めたりしないだろうか」(顧客)

人は不安からは逃げられません。プライベートであろうが仕事であろうが、あらゆることが不安の対象となります。

むしろ不安を抱かない方が却って問題です。何も不安に思わなければ、問題意識も生まれません。漠然とした不安をきっかけとして、具体的で有効なアクションを取れることもあるのです。

不安には冷静に対処する必要があります。なぜ自分が不安に思うのか。まずはその対象を明確にしなければなりません。また、不安は対象が明確になればほとんど消えてしまうものなのです。
いろいろ考えてみると不安の原因が単に自分の無知だった、ということも少なくありません。その場合も不安はさっぱりと消えてしまいます。
例えば、あるプログラムが本当に動くかどうか不安に思って調べたところ、開発環境でずっと問題なく動いていることを知ったために一切の不安がなくなった、などということは開発者ならば珍しくないでしょう。

もちろん、対処しようがない不安もあります。そういう場合は覚悟を決める他はありません。ですが、プロジェクトに参画することで生じる不安は、ほとんど全てが具体化すれば消えてしまうものです。ですから、具体化の努力を怠ってはなりません。

まずは漠然とした不安を感じたとして、その不安の原因をできるだけ具体化するのです。例えばある機能がちゃんと定義されているかどうか。具体的にはどのような定義が、どこに設定してあればよいのか。期待通りに定義されていることを確認すれば、不安は消えます。
あるいは無知から生じる不安については、自分が何を知らないから不安なのかをはっきりさせ、その具体的な事実なり知識なりを仕入れさえすれば、同じく不安は解消します。

それを怠り、いたずらに不安の対象を広げるのは好ましくありません。特に下手に不安を抽象化するとその対象は際限なく広がることになります。
例えば、先ほどの「設定漏れ」が不安の原因ではなく、「人によるチェックミス」が原因だと考えてしまったとしましょう。ところが人為的なミスはどこでも常に発生しうるものです。ですから、そのように不安の原因を定義してしまうと、不安はいつまで立っても解消しないことになります。人によるチェックミスは「あってはならないもの」ではなく「当然ありうべき」言わば制約なのです。不安の原因がチェックミスであると考えてしまうと「チェックミスが発生する(し得る)」限り不安は解消しません。すると「今後チェックミスは許さない」という無茶なスローガンが生じたり、「他にチェックミスはないのか」という愚問が生まれたりします。
もちろん今後同じようなミスが発生しないように、「設定漏れ」というリスクに対して「設定項目が正しいか」チェックする運用を作る、という対処は正しいと言えます。そうではなく、顧客やマネージャーの漠然とした不安を「全部チェックする」「あらゆる設定の妥当性を確認する」ことによって解消しようとすると、行きつく先はプロジェクトの破綻です。

顧客やマネージャの不安を上手にハンドリングできるSE、つまり「信頼感」を与えるSEこそが、優れたSEではないか、と私は思います。

DB2ストアドプロシージャ メモ

Stored Procedure のページが簡潔で良いです。
この通りにやれば出来ますが、一部ハマった点があったのでメモしておきます。

まず、接続は「デフォルトコネクション」を使います。
Connection con = DriverManager.getConnection("jdbc:default:connection");

最初、'jdbc:db2://hostname:port/dbname'でやったところ見事にハマりました。

まず「resultset closed invalid operation」とか何とか出て、気持ち悪いなと思ったもののconnectionをクローズせずにやったところ「java.sql.CallableStatement.executeQuery() was called but no result set was returned.」だのなんだの。

デフォルトコネクションを使ったところ、connectionをcloseしても問題なく使えました。

次に
db2 "call sqlj.refresh_classes ( void )"

です。これを実行すると、SQL0444N rc=4 が出ました。
SQL0444N  ルーチン "*_classes" (特定名 "SQL080417102058370")
が、アクセスできないライブラリーまたはパス "...refresh_classes"、関数
"sqlj.refresh_classes" のコードで実行されています。 理由コード: "4"
SQLSTATE=42724

これも大分ハマったのですが、
以下のように'void'を取ると問題なく実行できました。
db2 "call sqlj.refresh_classes()"

以上です。

2008年4月15日火曜日

コマンドラインからCVSを使う

コマンドラインからCVSを使ってみます。
Eclipse - CVS連携はよく出来ていますが、やはり内部の動きが見えないと不安ですね。(そんなことはないですか?)

情報提供しているページは山ほどありますが、自分のために整理してみました。

■ログイン■

$CVSROOTを確認します。サーバがセットアップされているのが前提となります。
# echo $CVSROOT
:pserver:user@hostname:/cvsroot
【CVSにログイン】
# cvs login
Logging in to :pserver:user@hostname:2401/cvsroot
CVS password: ********[Enter]

■プロジェクト追加■
# ls sample_project
a.c b.java
# cvs import -m "初期登録" sample_project vendor start
N sample_project/a.c
N sample_project/b.java

■バイナリファイルの指定■
# cvs co CVSROOT
# cd CVSROOT
# vi cvswrappers
【ファイルを編集し、以下の行を追加します】
*.gif -k 'b'
*.GIF -k 'b'
*.bmp -k 'b'
*.BMP -k 'b'
*.xls -k 'b'
*.XLS -k 'b'
*.Xls -k 'b'
*.pdf -k 'b'
*.PDF -k 'b'
*.class -k 'b'
# cvs update
cvs update: Updating .
M cvswrappers
# cvs commit
cvs commit: Examining .
【viが開くので"バイナリファイル指定を追加'というコメントを入力し':wq'でviを終了します。】
CVS: ----------------------------------------------------------------------
CVS: Enter Log. Lines beginning with `CVS:' are removed automatically
CVS:
CVS: Committing in .
CVS:
CVS: Modified Files:
CVS: cvswrappers
CVS: ----------------------------------------------------------------------
cvswrappersを編集
~
~
"/tmp/cvsNrkSUb" 9 行、314 文字
Checking in cvswrappers;
/cvsroot/CVSROOT/cvswrappers,v <-- cvswrappers
new revision: 1.3; previous revision: 1.2
done
cvs commit: Rebuilding administrative file database

■CVS関連作業の終了■

cvsからチェックアウトしたファイルをリリースします('-d'オプションでディレクトリも削除されます)。一旦作業を止めるイメージです。
# cd .. # <- 削除対象ディレクトリを指定するため上位に移動
# cvs release -d CVSROOT
You have [0] altered files in this repository.
Are you sure you want to release (and delete) directory `CVSROOT': yes
#

■タグをつける■

タグをつける前に、以下のを確認しましょう。
 バージョンを構成する全てのファイルがコミットされていること
 リリースしないファイルやディレクトリがないこと
  # ls *.log *tmp* *test* *.org *.orig *.bak *.back *.[0-9]* *hoge* *piyo* *foo* *bar* # などなど
 classなどをCVSで管理している場合は、タグをつける前にコンパイルをしましょう
# ls
sample_project
# cvs tag rel20080326 sample_project
cvs tag: Tagging sample_project
(略)

■rdiffの使い方■

タグ=バージョン名をつけると、タグを使ってもバージョン同士/バージョンと現状のdiff/rdiffを見ることができます。(もちろん-r [リビジョン]でもOKです)
# cvs commit -m'rdiffテスト' a.c
# cd ..
# cvs rdiff -s -r rel20080326 sample_project
cvs rdiff: Diffing sample_project
File sample_project/a.c changed from revision 1.2 to 1.3

■cvs statやcvs logコマンドでタグの状況を確認■
# cvs stat -v a.c
# cvs log a.c

■タグを削除する■
# cvs stat -v | grep -p Existing
【削除したいタグを確認する】
# cd ..
# cvs tag -d rel20080326 sample_project

■エクスポートする■

バージョンを指定してエクスポートします。
エクスポートによって"CVS"管理ディレクトリを含めないでソースを抽出することができます。
タイムスタンプも、レポジトリ上の最終更新日に揃えられます。
# mkdir export
# cd export
# cvs export -r rel20080326 sample_project

■コメントを編集する■
# cvs admin -m1.3:'変更後コメント' a.c

■パーミッションを変更する■

やむを得ず(go+xやパg+wなど)パーミッションを変更する場合は、レポジトリのファイルパーミッションを直接変更することになります。
レポジトリのパーミッション+umaskで最終的なパーミッションが決まります。
動かなくならないよう、常識的な範囲で変更して下さい。

■ログインしてモジュールをチェックアウトする■
# cvs login
Logging in to :pserver:cvs@hostname:2401/cvsroot
CVS password: ********
# #【重要】必要に応じてumaskを変更します。
# # 755, 644系のパーミッションを期待するなら、umaskは022とします。
# umask 022
# cvs co sample_project # <- co は checkoutの略

■ファイル追加■

ファイル'addtest.ksh'を追加します。
# cvs add addtest.ksh
# cvs commit -m"新規登録" addtest.ksh

■バイナリファイルの追加■
# cvs add -kb binry.o

■ディレクトリ追加■

ファイルが存在しないディレクトリは存在しないのと同じです。
# mkdir adddir
# echo "test" > adddir/adddir.ksh
# cvs add adddir
# cvs add adddir/*.ksh
# cvs commit -m "dir以下のファイル追加"

他の人が追加したディレクトリを自分の作業ディレクトリに表示させるときは
cvs update -d -P を実行します。

■ファイル編集■

viでも何でも使って適宜編集した後、update, commitします。
'cvs commit ファイル名' とすると指定したファイルのみをcommitします。
# cvs update
# cvs commit -m"コメント" fileName.txt

■ファイル削除■

実際にはファイルは削除されません。レポジトリから見えなくなるだけで、いつでも復活させることができます
# rm filename
# cvs remove filename
# cvs commit -m "Removed unneeded files"

cvs remove -f とすれば、rmコマンドは不要です。
# cvs remove -f filename

# 削除後でもログは参照可能です。
# cvs log filename

■ファイルの削除を中止する■

手順概要(1)remove 実行前
# rm filename
# cvs update

手順概要(2)remove 実行後(commit未済)
$ rm filename
$ cvs remove filename
$ cvs add filename
$ cvs commit

手順概要(3)remove/commit後
$ cvs add filename
$ cvs commit

■ディレクトリ削除■

実際には削除されません。リポジトリには空のディレクトリが残ります。
# rm adddir/*.ksh
# cvs remove adddir/*.ksh
# cvs remove adddir
# cvs commit -m "adddirを削除"
# cvs up -d -P

■ファイルの名前を変更する■■
# mv addtest.ksh renametest.ksh
# cvs remove addtest.ksh
# cvs add renametest.ksh
# cvs commit -m "addtestをrenametestに変更"

■ディレクトリの名前を変更する■
特別な手順はありません。新規のディレクトリを作成し、ディレクトリ中のファイルを作成したディレクトリに移動させます。

■古いファイルとのDIFF■
# cvs diff -r[rev] [ファイル名]

■コメント/履歴確認■
# cvs log [ファイル名]

■バージョン確認■
# cvs stat [-v] [ファイル名]

'-v'を追加するとタグ(リリースバージョン)情報を出力します。

■最新バージョンに書き戻す■
$ rm ファイル
$ cvs update ファイル
U ファイル

【あるいは】

$ cvs update -C ファイル

■古いバージョンに書き戻す■
# cvs log ファイル
# cvs diff -r[rev] ファイル # 戻したいファイルとのDIFFを確認する
# cvs update -p -r [rev] ファイル > ファイル
# cvs update
M ファイル
# cvs commit ファイル

■追加を途中でやめる■
# cvs remove -f ファイル

commitは不要です。

■作業ディレクトリがいつチェックアウトしたものか分からなくなった■
作業ディレクトリで'cvs update'するか、一旦作業ディレクトリを削除してcheck outします。
(当たり前か)

■終了する(release)■

cvs releaseコマンドを使います。
'-d'オプションを使うとディレクトリまで削除してくれます。
# cvs release -d sample_project


【参考】
CVS--Concurrent Versions System (in Japanese) - CVS のコマンド便覧 必要十分なリファレンス
CVS使用法メモ(Hishidama's CVS usage Memo) 実践的なコマンドメモ
リリースブランチ(cvs) ちょっと毛色は違いますがブランチの意義について

2008年4月4日金曜日

DIFF出力結果の見方

申し訳ないほど基本的なことですみません。
普段ぼおっと見ているもので、今回意識的に整理してみました。
【凡例】
$ diff File1 File2

Lines Affected in File1 Action Lines Affected in File2

1. Number1 a Number2[,Number3]
2. Number1[,Number2] d Number3
3. Number1[,Number2] c Number3[,Number4]

読み方
1. 'File1'の'Number1'行に、'File2'の'Number2'(から'Number3')の行を追加
2. 'File1'の'Number1'(からNumber2')を('File2'の'Number3'から)削除
3. 'File1'の'Number1'(からNumber2')を'File2'の'Number3'(から'Number4')の記載へと変更

[tmp/] $ cat a.txt
aaa
bbb

ccc

ddd

[tmp/] $ cat b.txt
xxx
yyy

iii
jjj


[tmp/] $ diff a.txt b.txt
1,2c1,2 (1)
< aaa .
< bbb .
--- .
> xxx .
> yyy .
4c4,5 (2)
< ccc .
--- .
> iii .
> jjj .
6d6 (3)
< ddd .


(1) a.txtの1~2行目の(aaa, bbb)を、b.txtの1~2行目(xxx,yyy)に変更し、
かつ
(2) a.txtの4行目(ccc)を、b.txtの4~5行目(iii,jjj)に変更し、
かつ
(3) a.txtの6行目(ddd)を削除すれば、
a.txtとb.txtは同じファイル

といった読み方になりましょうか。

2008年3月18日火曜日

EclipseからCVSを使う

ソースコードが増えてきたのでCVSを使ってバージョン管理をしたいと思います。

■手順あらまし
(1)AIX環境にCVSサーバを立てる
(2)Eclipse(Lomboz)からCVSを利用する

■手順詳細
(1)AIX環境にCVSサーバを立てる

a. 下記URLからcvsとzlib(CVSの前提)をダウンロードします
AIX Toolbox for Linux Applications
3/18時点では、下記のrpmが最新でした。
cvs-1.11.17-3.aix5.1.ppc.rpm
zlib-1.2.3-4.aix5.2.ppc.rpm

b. インストールします
# rpm -i zlib-1.2.3-4.aix5.2.ppc.rpm
# rpm -i cvs-1.11.17-3.aix5.1.ppc.rpm
#

c. 各種設定
1:ユーザの作成
smitty mkuser

などでCVSユーザ(ここではcvsuser)を作成します。
パスワードも忘れずに付けておきましょう。(rootで"passwd cvuser")
(手順は割愛)

2:CVSレポジトリの作成とinit
ディレクトリを作成し、cvs initを実行します。
# su - cvsuser
$ mkdir cvsroot
$ export CVSROOT=/full/path/to/cvsroot # .profileなどにいれてしまいましょう
$ cvs init
$

3:/etc/servicesに以下のエントリーを追加
/etc/servicesに以下のエントリーが存在しない場合(存在しないと思いますが)追加します。
cvspserver      2401/tcp # CVS client/server
cvspserver 2401/udp # CVS client/server

4:/etc/inetd.confに以下のエントリーを追加
cvspserver stream tcp nowait root /usr/bin/cvs cvs -f --allow-root=/full/path/to/cvsroot pserver

5:inetdを再起動
# stopsrc -s inetd
0513-044 /usr/sbin/inetd サブシステムは停止を要求されました。
# startsrc -s inetd
0513-059 inetd サブシステムは始動しました。サブシステム PID は 1536230 です。

この状態で'cvsuser'でEclipseから接続し、使うことができます。

6:(オプション)cvsのpasswdを作成
/full/path/to/cvsroot/CVSROOT
以下にpasswdファイルを使います。書式は
userid:encrypt_password:valid_userid
です。パスワードが空で、cvsuserと同じ権限でアクセスできる'cvs'ユーザを作成する場合は、
cvs::cvsuser

というエントリーを追加すれば良いです。
ファイルのエンクリプトにはApacheなどに付属する'htpasswd'ユーティリティを使えばよいようですが、個人的な環境なのでそこまではしません。

(2)Eclipse(Lomboz)からCVSを利用

Eclipse(Lomboz)を起動し、'Window' → 'Open Perspective' → 'Other' から、'CVS Repository Exploring'を選択します。

'CVS Repository'ビューを右クリックし、'New' → 'Repository Location'を選択します。

'Add CVS Repository'で以下の項目を入力し、'Finish'をクリックします。
Host: CVSサーバのホスト名
Repository Path: /full/path/tocvsroot
User: cvsuser
Password: ******* (cvsuserのOSパスワード)

これでCVS Repositoryが表示され、使えるようになります。