はじめに
今ではJavaコミュニティのほとんどだれもがJakarta Commonsのことを知っています。Jakarta CommonsはJakartaのさまざまなプロジェクトで使われている再利用可能なクラスの集まりです。しかし、これらのクラスを独立したコンポーネントとして自分のJavaプロジェクトの中でも利用できることは案外知られていないのではないでしょうか。本稿はJakarta Commonsのさまざまなコンポーネントを紹介する3回シリーズの第1弾です。このシリーズでは実際のサンプルアプリケーションを通じてJakarta Commonsコンポーネントの使い方を説明していきます。これらのサンプルはJakarta Commonsコンポーネントを例示するだけのものではなく、典型的なJavaプロジェクトで再利用できる有用な機能を盛り込んだ完全なアプリケーションです。
本稿では次のコンポーネントを取り上げます。
- Validator
- Collections
- Chain
- Functors
- Lang
本稿には完全なソースコードが付属しており、各サンプルのテストケースをJUnitで起動することで実行できます。
著者注
Commonsコンポーネントのアーキテクチャと用法を理解するためには、オブジェクト指向プログラミング(OOP)とGang of Fourのデザインパターン(Command、Decorator、Singleton、Factory、Chain of Responsibility、Composite)についての基本的知識が非常に役立ちます。
Validator
Commons Validatorは、StrutsでWebアプリケーションのフォームの妥当性検査に使用するコンポーネントです。このコンポーネントは任意のJava Beanの妥当性を検査するように作られていますが、ドメインレイヤオブジェクトや、Swingアプリケーションへのユーザー入力、あるいはWebサービスに送られるXML形式のデータなどの妥当性検査にも使用できます。
私が見てきた大部分のプロジェクトでは、妥当性検査が次の3つの方法のいずれかで書かれています。
- ドメインオブジェクト内で
- ユーティリティクラス内で
- カスタムフレームワークとして
最初の2つのアプローチを採用した場合は、結果的にデザインが好ましくないものになります。なぜなら、開発者がそれぞれ独自に妥当性検査メソッドを書く傾向にあるからです。たとえ単にエラーを記録したり、例外をスローしたり、それをユーザーに表示したりするだけの妥当性検査コードであっても、それがアプリケーションのあちこちに点在するのは望ましくありません。もう1つのデメリットは、せっかくエラーメッセージをリソースバンドルから読み取って表示する仕組みを実現しても、そのためのコードがあちこちに重複して存在することになる、という点です。たとえ妥当性検査のためのコードであっても、コードの重複は望ましくありません。
今回は、試しにCommons Validatorを使ってJava Swingアプリケーションへのユーザー入力の妥当性検査を行います。図1は、ユーザーが登録情報を入力するGUIのスナップショットです。
図1 Swingアプリケーションのスナップショット
このアプリケーションでCommons Validatorをどのように利用しているかを詳しく説明する前に、このデザインで重要な役割を果たすクラスを紹介しておきます(ソースコードを見ると理解しやすいでしょう)。以下のクラスはパッケージ「in.co.narayanan.commons.validator」の一部です。
| RegistrationScreen | 他のGUIコンポーネントを含むメインフレーム。Singletonパターンのクラスです。 |
| Registration | ユーザー登録情報を表現するドメインオブジェクト。 |
| Listeners | GUIイベントを処理して、それらを適切なコマンドに委譲するためのアダプタクラス。 |
| SubmitCommand | Commandパターンを使って送信操作を表現します(妥当性検査はこのクラスから開始されます)。 |
| RegistrationContext | Mediatorパターンを使ってイベントリスナおよびコマンドクラスに必要なコンテキスト情報を渡します。 |
| ValidatorEngine | Commons Validatorを使ってRegistration Beanの妥当性を検査し、結果を処理します。 |
このサンプルの目的は、登録時にユーザーが入力したログイン名(proposedLoginName)とメールアドレス(mailAddress)の妥当性を検査することです。SubmitCommandの中で、ユーザーが入力した値がRegistrationドメインオブジェクトに格納されます。
Commons Validatorの使用手順
Commons Validatorコンポーネントを作成するには、通常は次の手順を行います。
1. POJO妥当性検査クラスを書くか再利用する
Commons Validatorを使用するための最初のステップは、一群のデータに対して妥当性検査を行うクラスを書くことです。このサンプルでは、proposedLoginNameとmailAddressの妥当性検査を行うためにGenericBeanValidatorというクラスを書きました。GenericBeanValidatorクラスの構造は次のようになっています。
public class GenericBeanValidator {
public static boolean validateRequired(Object bean, Field field)
{
String value =
ValidatorUtils.getValueAsString(bean, field.getProperty());
return !GenericValidator.isBlankOrNull(value);
}
public static boolean validateEmail(Object bean, Field field)
{
String value =
ValidatorUtils.getValueAsString(bean, field.getProperty());
return GenericValidator.isEmail(value);
}
}
この妥当性検査クラスはどんなAPIにも依存しない、まったくのPOJOクラスなので、単体テストが簡単にできます。この目標を達成するために、今回はValidatorUtilsクラスとGenericValidatorクラスを使用しました。これらのクラスはCommons Validatorの一部です。getValueAsStringメソッドはRegistration Beanからプロパティの値を取り出し、文字列を返します。GenericBeanValidatorクラスはGenericValidatorのラッパーのように機能してBeanの妥当性を検査します。GenericValidatorクラスには他にも種々の妥当性検査を行うためのユーティリティメソッドが数多く含まれています(Commons Validatorを最大限に利用するには、javadocを参照してください)。
2. マッピングXMLファイルを定義する
次のステップでは、妥当性検査クラス(前のステップで書いたものと同じようなクラス)を宣言して、それらを妥当性検査すべきBeanに結び付けるためのXMLファイルを定義します。ValidatorエンジンはこのXMLファイルを使用します(後ほど詳しく説明します)。このサンプルの「registration.xml」ファイルは次のようになります。
<form-validation>
<global>
<validator name="required"
classname=
"in.co.narayanan.commons.validator.GenericBeanValidator"
method="validateRequired"
methodParams=
"java.lang.Object, org.apache.commons.validator.Field"
msg="required.field"/>
<validator name="email"
classname=
"in.co.narayanan.commons.validator.GenericBeanValidator"
method="validateEmail"
methodParams=
"java.lang.Object, org.apache.commons.validator.Field"
msg="invalid.email"/>
</global>
<formset>
<form name="userRegistration">
<field property="proposedLoginName" depends="required">
<arg0 key=
"userRegistration.proposedLoginName.displayname"/>
</field>
<field property="mailAddress" depends="email">
<arg0 key="userRegistration.mailAddress.displayname"/>
</field>
</form>
</formset>
</form-validation>
妥当性検査クラスは<validation>タグ内で宣言する必要があります。ここで重要なプロパティは、論理名(name)、クラス名(classname)、メソッド名(method)、メソッド引数(methodParams)、およびメッセージキー(msg)で、これは妥当性検査が失敗した場合にエラーメッセージを取得するのに使われます。このサンプルでは、proposedLoginNameとmailAddressの妥当性検査を行うために2つのエントリが定義されています。
<form>タグは<validator>定義をJava Beanプロパティにマップするために使われています。このサンプルの<arg0>タグはJava Beanプロパティの外部化されたフィールド名を取得するために使われています。これはロケール固有の表示用フィールド名をユーザーに表示するときに便利です。表示用フィールド名とエラーメッセージは外部化され、ファイル「applicationResource.properties」の中で利用できるようになっています。<field>定義内では任意の数の<argX>タグを使用できます。
3. Validatorを初期化して実行する
Commons Validatorエンジンを実行すると、プロパティに関する妥当性検査がXMLファイルマッピングで定義されているかどうかがチェックされます。定義されている場合は、該当する妥当性検査クラスメソッドが呼び出され、必要な引数が渡されます。すべてのプロパティについてこのプロセスが繰り返され、結果がエラーメッセージとともに蓄積されます(結果の処理方法については後で説明します)。
次のコードは、SubmitCommandが使用するValidatorEngineクラスから抜粋したものです。
InputStream definitionStream =
reg.getClass().getResourceAsStream(definitionXml);
....
resources = new ValidatorResources(definitionStream);
....
Validator validator = new Validator(resources, Registration.NAME);
validator.setParameter(Validator.BEAN_PARAM, reg);
....
results = validator.validate();
....
return parseResults(formName, reg, results, resources);
ValidationResourcesクラスはXMLファイル内の定義を表しています。このクラスを初期化するにはdefinitionXmlストリームを渡します。その後、このインスタンスを妥当性検査の対象となるBeanの名前と一緒にValidatorに渡します。妥当性検査クラスで必要な追加パラメータがある場合は、setParameterメソッドを使ってValidatorに渡すことができます。GenericBeanValidatorクラスのvalidateXXXXメソッドには、java.lang.Objectとorg.apache.commons.validator.Fieldという2つの引数があったことを思い出してください。Validatorは、実行時にこの最初のパラメータにBeanのインスタンスを渡し、2番目のパラメータに適切なフィールド参照を渡します。
ところで、Validatorは他のオブジェクト(たとえばHttpServletRequestオブジェクト)の参照をどうやって妥当性検査メソッドに渡すのでしょうか。実は完全修飾クラス名とHttpServletRequestクラスのインスタンスへの参照を指定して、ValidatorのsetParameterメソッドを呼び出すのです。Validator内の妥当性検査メソッドを呼び出せば妥当性検査を開始することができます。
4. 結果を処理する
Validatorは実行時にValidatorResultsのインスタンスを返します。このインスタンスには、実行される妥当性検査についての情報がすべて格納されます。結果の視覚的な構造は次のようになります。
[
[property1]
[action1]
[action2]
[property2]
[action1]
]
すべてのBeanプロパティの結果はValidatorActionインスタンスの形で結び付けられます。これらのプロパティを反復処理し、ValidatorResultクラスのisValidメソッドを呼び出すことにより(パラメータにはアクション名を指定)、妥当性検査が成功したか失敗したかを判断します。具体的な解析ロジックについては、ValidatorEngineクラスのparseResultsメソッドのコードを見てください。
その他の用途
このサンプルでは、Commons ValidatorをJava Beanアプリケーションに対するユーザー入力の妥当性検査に利用してみましたが、Commons Validatorは他にもさまざまなアプリケーションで使用でき、モジュール型設計、高い生産性、容易なメンテナンスといった利点が得られます。たとえば次のような使い方が考えられます。
- XML形式でWebサービスレイヤに送られる注文情報に妥当性検査を適用する
- ある時点におけるアプリケーション内のクラスの状態に妥当性検査を適用する(クラスの状態が要求を満たすものでなければ適切な例外をスローするなど)
- JMXと併用してアプリケーションを監視する
- 完全にJavaで実装されたルールエンジンの中で使用する(妥当性検査クラスを複雑なビジネスアプリケーション内のドメインオブジェクトに作用する個別ルールとして使用する)
このフレームワークには、Webアプリケーションにおける電子メールやURLなどの情報をクライアント側で妥当性検査するためのJavaScript関数もいくつか含まれています。
Collections
Commons Collectionsは、これだけを単独の記事として取り上げてもよいほど大きなトピックです。本稿ではAPIについて概説するにとどめ、最も一般的な実例を示します。
JDKにJava Collections Frameworkがあるのに、なぜCommons Collectionsが必要なのかと疑問を持つ人もいるかもしれません。Collectionsは、Java Collections Frameworkのクラスとインターフェイスを拡張し、機能強化するものです。実のところ、私はこれをJDKの一部にすべきだとさえ思っています。
私が特に好んで使用している機能をいくつか挙げておきます。
- バッグインターフェイス
- 固定サイズ、デュアル、およびLRUのマップ
- オブジェクト配列およびマップのイテレータ
- マップのMultiKey
- APIを使いやすくする多数のユーティリティクラス
- 大部分のクラスの動作をカスタマイズできるデコレータ
Collectionsのクラスは次のようなパッケージに編成されています。
- org.apache.commons.collections
- org.apache.commons.collections.bag
- org.apache.commons.collections.bidimap
- org.apache.commons.collections.buffer
- org.apache.commons.collections.collection
- org.apache.commons.collections.comparators
- org.apache.commons.collections.functors
- org.apache.commons.collections.iterators
- org.apache.commons.collections.keyvalue
- org.apache.commons.collections.list
- org.apache.commons.collections.map
- org.apache.commons.collections.set
org.apache.commons.collections
「org.apache.commons.collections」パッケージには、他のパッケージで実装されているインターフェイスと、コレクションクラスまたはコレクションクラスのデコレートバージョンをインスタンス化するためのファクトリとして機能するユーティリティクラスが含まれています。ArrayStack、BeanMap、ExtendedProperties、FastArrayList、FastHashMap、FastTreeMapといった注目すべきクラスが含まれています。これらのクラスの詳細情報はjavadocに網羅されていますが、表1で各クラスの実際の用法を簡単に紹介しておきます。
表1 「org.apache.commons.collections」パッケージ内の注目すべきクラス
| コレクションクラス | 実際の用法 |
ArrayStack | ArrayStackはシングルスレッド環境で使用するためにArrayListをスタック実装にしたものです。たとえば、特定のメソッドの中でだけ何かの処理にスタックを使用したいという場合は、この実装を使用した方がJDK-1.4.2のVectorを使ったスタック実装よりも有利です。 |
BeanMap | Swing GUIウィジェットのJButtonと同様、このクラスを使用すると、マップさえもJava Beanとして扱うことができます。アプリケーションのデータソースや一連の設定プロパティを表現するGUIをデザインする際に、マップを画面上でドラッグアンドドロップすることができます。 |
ExtendedProperties | このパッケージで私が最も気に入っているクラスです。設定プロパティをロードするためのloadメソッドを持つjava.util.Propertiesに似ていますが、こちらの実装には次のような利点があります。 ・同じプロパティキーに複数の値を持たせることができます。 ・値を異なる行に分割することができます。 ・非文字列値を取得するための便利なヘルパーメソッドがあります。 たとえば、getFloatメソッドはfloat型のプロパティを取得します。そのため、設定プロパティ値を解析するためにラッパーメソッドを書く必要はありません。 |
FastArrayList、
FastHashMap、
FastTreeMap | 大部分の操作が読み取り専用ならば、これらのクラスをマルチスレッド環境で使用できます。これらのクラスは、それぞれArrayList、HashMap、TreeMapをベースにしています。 |
org.apache.commons.collections.bag
org.apache.commons.collections.bagクラスは、オブジェクトの複数のコピーをListに追加するという要件を備えたアプリケーションでとても役に立ちます。こうした状況では、通常は、オブジェクトをArrayListに追加し、特定の型の追加オブジェクトの数を判定する際には毎回反復処理を行うという方法が使われています。この要件に該当する好例はオンラインショッピングカートです。このアプローチの欠点は、メモリと速度の面でパフォーマンスが劣ることです。
この要件に対処するには、オブジェクトのコピーを1つだけ保持し、同じ型のエントリが新たに追加されるたびにカウントを増やしていくというやり方が好ましいでしょう。それにはCommons CollectionsのHashBagクラスとTreeBagクラスが適しています(それぞれHashMapとTreeMapをベースにしています)。
ソースコードの「in.co.narayanan.commons.collections.bag」パッケージを見ると、バッグの実際の用法がわかります。このサンプルでは、さまざまなオペレーティングシステムのソフトウェアライセンスの注文を処理し、バッグ内の注文された製品を表現しています。
これらのクラスでは、もっと具体的なバージョンやデコレータが利用できます。詳細についてはjavadocを参照してください。
org.apache.commons.collections.bidimap
たいていのJava開発者は、キー/値のキーを取り出したい場合には、2つのHashMapを用意し、1つ目のHashMapの値を2つ目のHashMapにキーとして渡す、という方法を使用してきました。通常、名前と値を平等に扱いたければ、このテクニックを使用する必要があります。このケースでは値さえもキーになり得ます。
org.apache.commons.collections.bidimapは、PeopleSoftとSiebelのコマンド処理エンジンを統合するプロトタイプアダプタです(ただし、双方に同等のコマンドが存在することが前提となります)。このアダプタの関連クラスは「in.co.narayanan.commons.collections.bidimap」パッケージに含まれています。まず、サンプル内のクラスSiebelPeopleSoftConnectorのコードをひととおり見てみてください。このクラスはアダプタとして働き、コマンドマッピングをBidiMap内に保持します。Siebelコマンドを処理する要求が到着すると、BidiMapから対応するPeopleSoftコマンドが取得され、PeopleSoftコマンドエンジンに渡されます。PeopleSoftコマンドでは逆の処理が行われます。このサンプルに含まれているのはアプリケーションのスケルトンだけです。
請負で設計をする場合は、このクラスの修正不能バージョン(開発者が内容を変更できないもの)を使用すると便利です。これにより、開発者が内容を処理しているときに誤りを犯す危険性を大幅に減らすことができます。
org.apache.commons.collections.buffer
コレクションから特定の順序でオブジェクトを取り除きたい場合は、バッファインターフェイスの実装を利用することができます。注目すべきクラスとして、CircularFifoBuffer、PriorityBuffer、BoundedFifoBuffer、BlockingBufferなどを挙げることができます。いずれにもデコレートバージョンがあります。
パッケージ「in.co.narayanan.commons.collections.buffer」にバッファサンプルがあります。これはCircularFifoBufferクラスの例を示しています。このクラスは移動ウィンドウと考えることができます。コレクションのサイズは固定しており、満杯になるとオブジェクトが先入れ先出し(FIFO)方式で削除されます。
このアプリケーションの目的は、リモートサーバーにパフォーマンスデータを報告することです。パフォーマンスデータは監視に使用されるだけなので、データが失われてもそれほど問題にはなりません。パフォーマンスデータを報告するクライアントアプリケーションは、ReportPerformanceDataクラスをインスタンス化し、reportPerformanceメソッドを呼び出す必要があります。パフォーマンスデータは循環バッファに追加されます。
ReportTaskクラスはパフォーマンスデータオブジェクトを削除し、それをパフォーマンスサーバーに送ります。報告タスクを非同期に開始するためにJava 1.5の並行処理ユーティリティクラスThreadPoolExecutorが使用されるので、バッファ内に報告すべきデータがあればデータが報告されます。
org.apache.commons.collections.collection
「org.apache.commons.collections.collection」パッケージにはデコレータクラスが含まれています。これらのクラスはjava.util.Collectionインターフェイスを直接実装しています。そのため、Collectionインターフェイスを実装しているクラスならどのクラスでも、これらのデコレータを利用することができます。PredicatedCollection、CompositeCollection、SynchronizedCollection、TransformedCollection、TypedCollection、UnmodifiableCollectionなどがよく使われるクラスです。表2に、これらのデコレータをどのような場合に使用するかを示します。
表2 「org.apache.commons.collections.collection」パッケージ内のよく使われるクラス
| デコレータクラス | 使用する状況 |
PredicatedCollection | コレクションへのオブジェクトの追加を"amount >= $15000"などの条件で制限したいとき。このクラスをインスタンス化するには、条件を別オブジェクトとして定義します。これはプレディケイトとして呼び出され、decorateファクトリメソッドに引数として渡されます。 |
CompositeCollection | コレクションのコレクションを作成し、オブジェクトの追加または削除を統一的な方法で行いたいとき。 |
SynchronizedCollection | 既存のコレクションをスレッドセーフにしたいとき。 |
TransformedCollection | コレクションへの追加時に常にオブジェクトの形式を変換したいとき(たとえばStringからIntegerに変換するなど)。 |
TypedCollection | コレクションに追加するオブジェクトの型を制限したいとき。これはJava 1.5のgenericsに似ています。 |
UnmodifiableCollection | 既存のコレクション参照への修正を制限したいとき。 |
org.apache.commons.collections.comparators
このパッケージにはさまざまな再利用可能クラスが含まれており、コレクション内でオブジェクトをソートするときに役立ちます。特に注目したいのがNullComparatorクラスとFixedOrderComparatorクラスです(表3を参照)。
表3 NullComparatorクラスとFixedOrderComparatorクラス
| 再利用可能なクラス | 機能 |
NullComparator | 配列またはリストの中でエントリをソートする際にnullエントリを最後部に移動します。 |
FixedOrderComparator | コレクション内での順序をあらかじめ定義されたリストに戻します。 |
org.apache.commons.collections.functors
「org.apache.commons.collections.functors」パッケージについては、後のセクションで詳しく説明します。
org.apache.commons.collections.iterators
「org.apache.commons.collections.iterators」パッケージ内の多くのクラスはjava.util.Iteratorインターフェイスを実装しています。MapIterator、ArrayIterator、CollatingIterator、LoopingIterator、IteratorUtilsなどが最も役に立つクラスです。このパッケージ内のクラスを利用するには、IteratorUtilsクラス内のメソッドをよく調べる必要があるでしょう。
org.apache.commons.collections.keyvalue
「org.apache.commons.collections.keyvalue」パッケージ内のMultiKeyクラスは非常に有用です。アプリケーション内でドメインオブジェクトを作成し、それを複合主キーと対にしてマップに格納したい場合は、対象レコードの主キーを構成する値を渡してMultiKeyのインスタンスを作成します。その後、このインスタンスをドメインオブジェクトを格納するためのマップに渡します。
このパッケージのその他の一般的な用法として、ロケール固有のエントリをマップに格納するという例が挙げられます。この場合は、実際のキーとロケール名からキーが構成されます。
org.apache.commons.collections.list
「org.apache.commons.collections.list」パッケージ内の注目すべきクラスは、TreeList、FixedSizeList、NodeCachingLinkedList、CursorableLinkedList、TransformedList、PredicatedListです。これらのクラスのことは、javadocを見ればすぐに分かります。
org.apache.commons.collections.map
「org.apache.commons.collections.map」パッケージ内の注目すべきクラスは、CaseInsensitiveMap、CompositeMap、FixedSizeMap、Flat3Map、LazyMap、LinkedMap、LRUMap、MultiKeyMap、PredicatedMap、SingletonMap、StaticBucketMapです。これらのクラスのことは、javadocを見ればすぐにわかります。
org.apache.commons.collections.set
「org.apache.commons.collections.set」パッケージ内の注目すべきクラスは、CompositeSet、ListOrderedSet、MapBackedSet、PredicatedSet、TransformedSet、TypedSet、UnmodifiableSetです。これらのクラスのことは、javadocを見ればすぐにわかります。
Chain
Gang of FourのCOR(Chain of Responsibility)デザインパターンは、一群の操作をモデル化するのによく使われます。各操作をコマンドオブジェクトとしてモデル化し、他のオブジェクトとリンクしてチェーンにする必要があります。これらのコマンドオブジェクトはデータに作用し、チェーンの次のコマンドに制御を渡すべきかどうかを示します。コードに変更を加えなくても操作を簡単に追加/削除できるのが利点です。実行が成功するためには、コマンドの実行時にコマンドラインから渡された引数とサブコマンドを処理コードで利用できなければなりません。
Commons Chainコンポーネントは、アプリケーション内でこのパターンを使用するのに必要なインターフェイスと実装を提供します。APIは明快で、理解するのも難しくはないでしょう。以降では、このコンポーネントを現実の問題に適用することで用法を説明していきます。CLI(Command Line Interface)アプリケーションを開発したり使用したりした経験のある開発者は少なくないと思います。本稿のCommons Chainのサンプルは、HTTP、FTP、PINGという基本的なネットワークプロトコルを使用するためのCLIアプリケーションです(ソースコードについては「in.co.narayanan.commons.chain」パッケージを見てください)。次のリストはいくつかのコマンドを抜粋したものです。
java CommandProcessor -user admin -password manager -ping {host}
java CommandProcessor -user admin -password manager
-ftp {host} -get {path_to_file}
java CommandProcessor -user admin -password manager
-http -get {path_to_file}
今回のサンプルでは、-user引数と-password引数を使って、ネットワークコマンドではなくローカルデータベースに対してユーザーの妥当性検査を行います。-get引数は、-ftpと-httpという両マスタコマンドについて、指定のURLのファイルを取得するためのサブコマンドです。ここではCommons Chainの使用例を示すことが狙いなので、この実装にはスケルトンコードしか入っていません。そのため、-pingコマンドの実行時にはコンソールにログメッセージが表示されるだけで、実際にターゲットに接続することはありません。あとは各自で完全なアプリケーションに仕上げてください。その際はCommons Net APIを使用するようお勧めします。
この問題に対してCORパターンを使用すると、デザインが大幅に簡素化されてモジュール性が向上します。
CLIコンテキスト
CLIコマンドの実行時に操作が正常に行われるためには、処理コードで引数とサブコマンドを利用できる必要があります。CommandlineContextクラスとCLICommandクラスはドメインオブジェクトで、これらはコマンドとサブコマンドと引数を表現します。
CLICommandは自分自身への参照を持っています。これはサブコマンドを表現するためのCompositeデザインパターン実装であり、これにより任意の数のコマンドをネストすることが可能になります。CLICommandのインスタンスはuserとpasswordと一緒にCommandlineContextに入れられるので、実行されるCLI操作の全情報を表現します。コンテキストは実行時にチェーン内のコマンドに渡されます。コンテキスト情報を利用したり制御を次のコマンドに渡したりするのは、チェーン内の各コマンドが行います。
このサンプルではCLIの引数をプライベート変数として表現し、ゲッターメソッドを使ってアクセスします。代替策としてContextBase実装を使用する方法もあります。この方法ではJava Beanイントロスペクションを使って属性値を取得し、Mapビューを提供します。したがって、getUserやgetPasswordのような専用ゲッターメソッドをget("user")やget("password")で置き換えることができます。
チェーン内のコマンド
コマンドに必要なコンテキスト情報を表現したら、次にコマンドとチェーンとサブチェーンをモデル化します(後ほどCommons Chain APIでコマンドとチェーンをどのように実行するかを示します)。Pingクラスは-pingのために実行されるコマンドです。このクラスはexecuteメソッドに渡されたコンテキストを使って、コマンドタイプが-pingかどうかをチェックします。コマンドタイプが他のものなら、このメソッドはfalseを返して、チェーン内の次のコマンドに制御を渡す必要があることをChainフレームワークに知らせます。
Ftpクラスはマスタコマンド-ftpを表現します。したがって、ftpのサブコマンドである-getと-lsを処理するために、これはChainBaseのサブクラスとしてモデル化されます。実行メソッドはコマンドタイプをチェックします。それが-ftpなら、このチェーン内のFtpGetコマンドとFtpLsコマンドを実行するために基本クラスのexecuteが呼び出されます。このアプリケーションでは、コマンドに-http -getなどのサブコマンドが含まれていれば、それはChainとしてモデル化され、該当するコマンド制御が委ねられます。
チェーン
このアプリケーションで使われるチェーンとコマンドを視覚的に表現すると、次のようになります。
Main Chain
Authenticator (Verifies user and password) --> Ping (Command) -->
Ftp (Sub-chain A) --> Default (Prints invalid command message)
Sub-chain A
FtpGet (Command) --> FtpLs (Command)
チェーンはChainBaseクラスまたはCatalogBaseクラスを使って実行時にプログラム内から作成できます。代替策として、XMLファイル内でコマンドとチェーンを指定し、ConfigParserを使ってチェーンを構築するという方法もあります。今回のサンプルで使用する「catalog.xml」ファイルは以下のとおりです。
<?xml version="1.0" ?>
<catalog>
<chain name="CommandProcessor">
<command name="authenticate"
className="in.co.narayanan.commons.chain.Authenticator"/>
<command name="ping"
className="in.co.narayanan.commons.chain.Ping"/>
<chain name="ftp"
className="in.co.narayanan.commons.chain.Ftp">
<command name="get"
className="in.co.narayanan.commons.chain.FtpGet"/>
<command name="ls"
className="in.co.narayanan.commons.chain.FtpLs"/>
</chain>
<command name="default"
className="in.co.narayanan.commons.chain.DefaultCLICommand"/>
</chain>
</catalog>
フレームワークのセットアップと実行
次のリストはCommons Chainフレームワークをセットアップして実行するためのコードです。
ConfigParser parser = new ConfigParser();
parser.parse(getClass().getResource("catalog.xml"));
Catalog catalog = CatalogFactoryBase.getInstance().getCatalog();
....
Command command = catalog.getCommand("CommandProcessor");
command.execute(getPingCommand());
command.execute(getFtpGetCommand());
command.execute(getFtpLsCommand());
command.execute(getInvalidCommand());
command.execute(getInvalidCredentials());
「catalog.xml」ファイルが解析され、結果としてCatalogの参照がCatalogFactoryBaseから取得されます。これで各CLIコマンドの呼び出しの「モックアップ」ができたことになります。完全なコードはテストケースクラスTestChainに収められています。なお、別個のクラスでコマンドライン入力を解析してコンテキストオブジェクトを作成し、それらをチェーンに渡すのが理想的です。その際はCommons CLIを使用することができます。
Functors
Functors APIの目的は、手続き型プログラム要素をオブジェクトとしてモデル化することです。たとえば、何かの条件に関してVector内またはArrayList内のオブジェクトを評価し、その結果に基づいてアクションをとるようなコードを書いたことはないですか? あるいは、if-elseif-elseif-else文やswitch-case文を書いた経験は? Functorsを使用すると、if-elseやswitch-caseのような条件とこれらの条件の中の式を、独立したオブジェクトとしてモデル化することができます。では、なぜそうする必要があるのでしょうか。この手法には次のような利点があります。
- モジュール型設計になるため、機能の拡張や修正が容易になります。StrategyパターンとCommandパターンを一緒に使って、操作の中のアルゴリズムを条件によって切り替えることができます。
- 単体テストケースを書くことが容易になります。
- リフレクションを使って、条件に応じた操作を動的にロードすることができます。
技術的に見ると、if、while、for、switchといったプログラム要素の多くはFunctorsを使ってモデル化することができます。この考え方は、連続的なif-else文、switch-case文、およびコレクションに対する操作でよく使われます。
このAPIに含まれる重要なインターフェイスはPredicateとClosureです。Predicateは条件を表現するのに使います。結果がtrueなら、Closureのexecuteメソッドが呼び出されます。
本稿で紹介するFunctorsサンプルは、データベースコマンドを処理する単純なスクリプト処理エンジンです。このサンプルでは、スクリプトコマンドに対してどのコマンドクラスを呼び出すかを定義したswitch-caseディシジョンツリーをFunctorsでモデル化しています。完全なコードは「in.co.narayanan.commons.collections.functors」パッケージに収められています。使用するデータベースコマンドの構文は次のとおりです。
add <tablename>, <column values>
delete <tablename>, <key_column>, <value>
modify <tablename>, <key_column>, <value>, <column name and new values>
このスクリプトエンジンは、addスクリプトコマンドが呼び出されたときに、データベースに対してSQLのINSERT文を実行する必要があります。他のコマンドも同様に処理する必要があります。ScriptCommandクラスはドメインオブジェクトであり、スクリプトコマンドとその引数を表現します。AddTask、DeleteTask、ModifyTaskの各クラスには、スクリプトコマンドのために実行するロジックが含まれています。このエンジンをモデル化するための従来の方法を理解するため、ProcessCommand1クラスから抜粋した次のコードをよく見てください。
public void process(ScriptCommand command) {
switch(command.getType()) {
case ADD: {
executeAdd(command);
} break;
case MODIFY: {
executeModify(command);
} break;
case DELETE: {
executeDelete(command);
} break;
}
}
ここでは、switch-caseブロック内でスクリプトコマンドの種類をチェックし、該当するコマンドを実行しています。
次のリストはProcessCommand2クラスからのもので、Functorsを使って同じ動作を実現できることを示しています。
public void start() {
Map predicatesAndClosures = new Flat3Map();
predicatesAndClosures.put(new AddPredicate(), new AddClosure());
predicatesAndClosures.put(
new ModifyPredicate(), new ModifyClosure());
predicatesAndClosures.put(
new DeletePredicate(), new DeleteClosure());
Closure switchClosure =
ClosureUtils.switchClosure(predicatesAndClosures);
CollectionUtils.forAllDo(commands, switchClosure);
}
private static class AddPredicate implements Predicate {
public boolean evaluate(Object object) {
ScriptCommand command = (ScriptCommand)object;
return command.getType() == CommandType.ADD ? true : false;
}
}
private static class AddClosure implements Closure {
public void execute(Object input) {
ScriptCommand command = (ScriptCommand)input;
AddTask add = new AddTask();
add.add(command.getTableName(), command.getArgs());
}
}
AddPredicateはコマンドタイプを評価します。AddClosureにはaddスクリプトコマンドを実行するロジックが含まれています。プレディケイトとクロージャはマップされて、ClosureUtilsユーティリティクラス内のswitchClosureメソッドに渡されます。結果がtrueの場合、返されたSwitchClosure参照は、クロージャにマップされたプレディケイトと呼び出しを評価します。trueが返されるまで、マップ内の各プレディケイトについて、これが繰り返されます。
このサンプルは、クロージャを併用してプログラムフローを実現する方法を示しています。ここでは、上記のAddClosureとCommons Collections API内のSwitchClosureを一緒に使用しています。たとえば、SwitchClosure参照をIfClosureと使用することができます。
ビジネスアプリケーションでのFunctorsの一般的な用途は、一群のビジネスルールをドメインオブジェクトインスタンスのコレクションに適用し、ルール結果に関連してアクションをとることです。ルールはプレディケイトとしてモデル化し、アクションはクロージャとしてモデル化する必要があります。Jythonに統合したPythonスクリプティング言語を使用すれば、完全なルールエンジンを構築することができます。その場合、プレディケイトとクロージャを書くためにPythonスクリプトを使用する必要があります。
Lang
このLang APIは「java.lang」パッケージに対する拡張です。私はCommons Langが将来JDKの一部になることを望んでいます。このパッケージにはJavaプロジェクトで使用できるユーティリティメソッドが数多く含まれています(JakartaのWebサイトに包括的なユーザーガイドがあり、javadocには大部分のクラスについて簡単な用法が記載されています)。本稿では特に興味深いクラスだけを紹介します。サンプルのソースファイルは「in.co.narayanan.commons.lang」パッケージに収められています。
StringUtils、WordUtils、StringEscapeUtils、RandomStringUtilsの各クラスには有用な文字列操作メソッドが備わっています。以下のリストでは、これらのメソッドの中でよく使うものを取り上げ、その用法を示します。
次のメソッドは、たとえばJTableセルなどでGUI内の表示領域が制限されている場合に、メッセージの一部だけを表示します。
String message = "Please check the logs for an error.";
message = StringUtils.abbreviate(message, 25);
次のメソッドは、Javaで開発されるデスクトップアプリケーションで非常に役立ちます(たとえば、電子メールを作成して送信する機能が必要なJava Swingアプリケーションなど)。
String message = "jakarta commons";
message = StringUtils.capitalize(message);
次に示すのはWordUtilsクラスからのもので、文字列を大文字にする2種類のメソッドです。
String message = "jakarta commons";
message = WordUtils.capitalize(message);
String message = "jakarta COMMONS";
message = WordUtils.capitalizeFully(message);
詳しい用法についてはjavadocを参照してください。
次のメソッドは前後にスペースを入れてメッセージを位置合わせするのに便利です。
String message = "java";
message = StringUtils.center(message, 10);
これまでは文字列処理のためにトークン化のコードを書くことがよくありましたが、Langを使えばその必要はありません。splitメソッドはトークン化の処理を行い、String配列を返してくれます。joinメソッドを使用すると、String配列から新しい文字列を作成できます。
String message = "hello how are you";
String messageTokens[] = StringUtils.split(message, ’ ’);
String newMessage = StringUtils.join(messageTokens, ’$’);
文字列内でのパスワード表示も、overlayメソッドで簡単に実現できます。replaceメソッドは非常に優秀で、単語を検索して置換する機能も備えています。そのため、文字位置を気にする必要はありません。
String message = "This is a demo for replace and overlay feature";
message = StringUtils.replace(message, "demo", "illustration");
message = "Your password is: somepassword";
message = StringUtils.overlay(message, "*****", 18, message.length());
StringEscapeUtilsクラスには、HTML、Java文字列、JavaScript文字列、SQL、およびXMLをエスケープ/エスケープ解除するためのメソッドがあります。
String html = "<h1>This will be displayed in the <i>HTML</i> page.
Hence the <b>tags</b> will be escaped.<h1>";
String escapedHtml = StringEscapeUtils.escapeHtml(html);
次のメソッドではランダムなパスワードを生成できます。
String randomPassword = RandomStringUtils.randomAlphanumeric(8);
System.out.println("Random Password:" + randomPassword);
以降では、各パッケージについて簡単に触れておきます。
org.apache.commons.lang
ArrayUtils、BooleanUtils、CharRange、CharSet、CharUtils、SystemUtils、Validateが注目すべきクラスです。詳細はjavadocを参照してください。
org.apache.commons.lang.builder
クラスCompareToBuilder、EqualsBuilder、HashCodeBuilder、ToStringBuilderは、それぞれメソッドcompareTo、equals、hashCode、toStringを実装するのに役立ちます。これらのクラスは、ほぼすべてのJavaプロジェクトで必要になります。
org.apache.commons.lang.enums
Java 1.5では列挙型がサポートされています。したがって、このパッケージが役に立つのはJDK 1.5より前のプロジェクトです。このパッケージのjavadocには使用例が掲載されています。
org.apache.commons.lang.exception
NestableExceptionクラスは例外の原因をリンクするためのサポートを提供します。すべての例外についてスタックトレースができると、エラーの根本原因をすばやく突き止めるのに役立ちます。これは特にJDK 1.4より前のプロジェクトで有用です。JDK 1.4以降には、ネストした例外のサポートが備わっています。
org.apache.commons.lang.math
IntRange、Fraction、NumberUtils、RandomUtilsが注目すべきクラスです。詳細はjavadocを参照してください。
org.apache.commons.lang.mutable
java.langパッケージ内の基本型のラッパーは不変ですが、このパッケージに含まれているラッパークラスは可変です。詳細はjavadocを参照してください。
org.apache.commons.lang.time
DateUtils、DataFormatUtils、FastDateFormat、StopWatchなどのクラスが有用です。詳細はjavadocを参照してください。
まとめ
Jakarta Commonsについて紹介するシリーズ第1回では、次のことを取り上げました。
- Commonsのさまざまなコンポーネントが提供する豊富な機能
- ユーティリティクラスの特定のコンポーネントやメソッドの具体的な用途
- さまざまなAPIで使われているデザインパターン
- さまざまなAPIのパッケージとクラスの概要
これからJavaアプリケーションをデザインしたり開発したりするときには、有用なクラスを選んで適切に使用できるのではないでしょうか。このシリーズの第2回と第3回では、さらに興味深いコンポーネントについて解説します。最後までお付き合いいただけば、あなたもJakarta Commonsコンポーネントの大部分のクラスをJDKの一部にすべきだと確信するでしょう。
Narayanan A.R.(Narayanan A.R.)
テスト駆動開発、アジャイル方法論、Javaテクノロジ、およびデザインパターンの熱烈な擁護者。Javaテクノロジを使ったソフトウェアのデザインおよび開発に携わって数年になる。