2009年11月30日月曜日

Tuscany SCA 1.5/m2eclipse

Tuscany SCA に関する↓の連載のコードを動かしてみた。 『オープンソースApache Tuscanyで楽しむSOA

現在、連載5回目で、ソースコードもダウンロードできる。記事のとおりに一通り動かしてみてから、使用ライブラリを User Library から Maven管理に変えてみた。やっぱり本番のプロジェクトで使う場面を考えると、Maven との連携を試してみたい。

tuscany-sca-all を指定するだけで、必要なものは芋づる式に Maven が集めてくれることを期待していたけど、そう簡単にはいかない。わかりにくい実行時例外が出て、なかなか Jetty が立ち上がるところまで行かず、デバッガを動かしながら必要な Jar を推理して集める破目になってしまった。意外と個別に設定するものが多い。

以下、<repositories>と<dependencies>。これだけあれば、とりあえず記事のサンプルは動く。

<repositories>
   <repository>
      <id>apache-incubating2</id>
      <name>Apache Incubating Repository</name>
      <url>http://dist.wso2.org/maven2/</url>
   </repository>
</repositories>
<dependencies>
   <dependency>
      <groupId>org.apache.tuscany.sca</groupId>
      <artifactId>tuscany-sca-all</artifactId>
      <version>1.5.1</version>
   </dependency>
   <dependency>
      <groupId>org.apache.neethi</groupId>
      <artifactId>neethi</artifactId>
      <version>2.0.4</version>
   </dependency>
   <dependency>
      <groupId>javax.annotation</groupId>
      <artifactId>jsr250-api</artifactId>
      <version>1.0</version>
   </dependency>
   <dependency>
      <groupId>org.apache.xmlbeans</groupId>
      <artifactId>xmlbeans</artifactId>
      <version>2.4.0</version>
   </dependency>
   <dependency>
      <groupId>com.metaparadigm</groupId>
      <artifactId>json-rpc</artifactId>
      <version>1.0</version>
   </dependency>
   <dependency>
      <groupId>net.sf.saxon</groupId>
      <artifactId>saxon-dom</artifactId>
      <version>8.7</version>
   </dependency>
   <dependency>
      <groupId>org.codehaus.jettison</groupId>
      <artifactId>jettison</artifactId>
      <version>1.0.1</version>
   </dependency>
   <dependency>
      <groupId>cglib</groupId>
      <artifactId>cglib</artifactId>
      <version>2.2</version>
   </dependency>
    <dependency>
      <groupId>org.apache.tuscany.sdo</groupId>
      <artifactId>tuscany-sdo-impl</artifactId>
      <version>1.1.1</version>
   </dependency>
   <dependency>
      <groupId>org.mortbay.jetty</groupId>
      <artifactId>jetty</artifactId>
      <version>6.1.17</version>
   </dependency>
   <dependency>
      <groupId>commons-httpclient</groupId>
      <artifactId>commons-httpclient</artifactId>
      <version>3.0.1</version>
   </dependency>
   <dependency>
      <groupId>org.apache.abdera</groupId>
      <artifactId>abdera-parser</artifactId>
      <version>0.4.0-incubating</version>
   </dependency>
   <dependency>
      <groupId>org.apache.ws.commons.schema</groupId>
      <artifactId>XmlSchema</artifactId>
      <version>1.4.4</version>
   </dependency>
   <dependency>
      <groupId>log4j</groupId>
      <artifactId>log4j</artifactId>
      <version>1.2.15</version>
   </dependency>
   <dependency>
      <groupId>org.apache.axis2</groupId>
      <artifactId>axis2-kernel</artifactId>
      <version>1.4.1</version>
      <scope>runtime</scope>
   </dependency>
</dependencies>

上記の追加で、記事どおりの動作を確認できた。第5回分まで実装すると、Web サービスが立ち上がり、ブラウザで http://localhost:8080/CalcCharge?wsdlを叩けば、WSDL が観察できるようになる。あまりWeb サービスっぽい作業はしていないので、ちょっと面白い。

あと、Tuscany 2.0への移植もやってみたかったけど、Tuscany 1.5を用いたこの記事中のコードで、サービスを起動するために使われている SCADomain というクラスが、Tuscany 2.0では見当たらず、現時点での自分の知識では代替案がわからずに断念した。いずれ機を改めて挑戦したい。

また、maven archetype を使ってみたかったけど、これも Tuscany 2.0 用しか見当たらなかった。うーん、今から勉強を始めるなら、Tuscany 2.0から出発した方がいいような気がしないでもない。

2009年11月29日日曜日

Caché / Jalapeño / jdk1.6

初めて Caché をダウンロードして、Java API の Jalapeño を使ってみた。

2年半ほど昔のこの記事を参考にやってみた。

記事で使っていたプロダクトとの、以下のようなバージョンの違いがある。
記事当時
JDK1.51.6
Eclipse3.23.5
Caché2007.12009.1

内容は、
  1. Eclipse で POJO を定義する
  2. POJO を元にして Caché データベース上にクラスを作る
  3. Caché データベース上に出来てきたクラスを「スタジオ」ツールで確認する
  4. Java コードから POJO を 永続化し、続いてそれを読み出す
  5. 永続化されたオブジェクトを管理ポータルで見てみる
といったもの。

ほとんど問題なく記事どおりに作業できるが、上リスト2に関して、どうも CacheDB.jar に、 com.jalapeno.tools.SchemaBuilderWizard が見当たらないので、ここを見ながら properties ファイルに設定情報を書いて、これを SchemaBuilder の -f オプションで与えて動かした。

試行した内容に限っていうと、コーディング的には JPA / Hibernate とそれほど変わらない感じ。JPA の EntityManager、Hibernate の Session に当たるものに ObjectManager というものがあって、これをつかって CRUD やトランザクション操作を行うが、これを初期化するために JDBC の Connection を与えているのを見ると、もしかすると結構ベタに SQL を投げている気配が無いでもない。

生産性の観点でいうと、あまり他の言語と組み合わせないで、Caché のテリトリー(Zen, CSP 込み)内で、Caché メインで開発したときに、一番生産性が高そうな感じ。パフォーマンスに関しては高速が謳い文句だけど、Jalapeño 経由で Java から操作しても速いかどうかは、別途調べる必要があるかもしれない。

読み方は、Caché (キャシエ)に Jalapeño(ハラーペーニョ)。é はフランス語(カナダ)キーボード設定で[/]キー、ñ はスペイン語(トラディショナルソート)キーボード設定で[;]キー。

2009年11月28日土曜日

Rampart 1.4/Tomcat v6.0/Eclipse 3.5

Tomcat 上の rampart 1.4 を Eclipse から動かしてみるテスト。 平文のユーザ名とパスワードを含む SOAP メッセージを、HTTPS で保護してみたい。Ant から動かすサンプルがあるけど、敢えてEclipse で動かす事にした。コード類はほぼそのまま。 ■使ったもの
  • Eclispe 3.5 Galileo
  • WTP 3.1.1
  • Axis2 1.4.1 (1.5.1 はダメだった…)
  • Rampart 1.4
  • Rampart policy サンプル下の sample-tomcat
  • もしかしたら、いるかもしれないもの
    • jaxen-1.1.1.jar
    • mex-1.5.1-impl.jar
■ 前提
  • Eclipse WTP で Tomcat v6.0 サーバが構成されている事(Servers ビューに表示されていること。)
  • rampart 1.4 がインストールされているフォルダを{rampart}とする。
  • {rampart}/sample/policy/sample-tomcatを{sample-tomcat}とする。
■ 作り方 ◆ サーバ の構成変更
  • [Project Explorer]/[Servers] 下の Tomcatサーバ構成 (普通は「Tomcat v6.0 Server at localhost-config」的な名前) に着目する。
  • ここに Rampart サンプル内の service.jks をコピーする。中身を見たかったら、コマンドプロンプトで パスの通っている所から keytool -list -v -keystore service.jks と打ち込めば、パスワードを聞いてくるので apache と応えれば、詳しい内容が表示される。
  • service.xml を開いて、以下のように port 8443 の定義を追加する。
    <Connector 
       SSLEnabled="true" 
       clientAuth="false" 
       keystoreFile="conf/service.jks" 
       keystorePass="apache" 
       maxThreads="150" 
       port="8443" 
       protocol="HTTP/1.1" 
       scheme="https" 
       secure="true" 
       sslProtocol="TLS" 
       truststoreFile="conf/service.jks" 
       truststorePass="apache" 
       truststoreType="JKS"/>
  • サーバを [Publish] する。
  • サーバを [Start] して、コンソールに「情報: Coyote HTTP/1.1を http-8443 で起動します」と表示され、他に以上が無いのを確認。
◆ サービスを作る
  • 新規 Dynamic Web Project ウィザードで以下のように指定
    • 適当なプロジェクト名をつける。rampart-trial2とした
    • Configuration -> Modify...->Axis Web Services にチェック
  • {sample-tomcat}のソース(*.javaファイル3つ)を, src 下に[Import]する。
  • WebContent/WEB-INF/lib 下に、{rampart}/lib 下の*.jarをコピーする。
  • WebContent/WEB-INF/modules 下に、{rampart}/modules/rampart-1.4.marをコピーする
  • SimpleService ウェブ・サービス作成
    • プロジェクトの コンテキストメニューから、[New]->[Other]->[Web Service]で、[Next>]押下
    • タイプにBottomUp、インプリメンテーションにSimpleService を設定して、スライダーを AssembleService にあわせる。
    • 「Configuration」の下を見て、Server→Tomcat v6.0、Web service runtime→Axis2、Service project→rampart-trial2になっている事を確認して、[Next>]押下.
    • [Finish]を押下して、WebContent/WEB-INF/services下に SimpleService フォルダが出来ていることを確認
  • SimpleService/META-INF/service.xmlの内容を、{sample-tomcat}/services.xmlの内容で置換。(新規 Web Service のウィザードの中で、既存 services.xml を指定するところがあるけど、上手く動かなかった。)
  • Tomcat サーバに追加して、[Restart]する.
  • ブラウザからhttps://127.0.0.1:8443/rampart-trial2/axis2-admin/を叩いて、admin/axis2 でログインする。
  • [Available Services]を見て、SimpleService が表示されているのを確認。EPR はこんな感じのはず。
    https://127.0.0.1:8443/rampart-trial2/services/SimpleService
  • ついでに WSDL も見て、<wsp:Policy>が入っている事と、他に以上が無いことを確認。
◆ クライアントを作る
  • Client クラスのコードは上でインポート下のをそのまま使う
  • プロジェクト直下に client_repository を以下のように構成する。
    client_repository/
       conf/
       module/
          addressing-1.5.1.mar
          rampart-1.4.mar
  • Client クラスのコンテキストメニューから、[Run As...]/[Run Configuration...] でフォームを開いて、"Java Application" から[New]
    • 「Name」には適当に実行の名前を指定。ここでは 「rampart client」とした。
    • Program Arguments に以下のように指定。
          https://127.0.0.1:8443/rampart-trial2/services/SimpleService
          client_repository
          client_repository/conf/policy.xml
  • もし無かったら、jaxen-1.1.1.jar と、mex-1.5.1-impl.jar を適当に探して、WEB-INF/lib 下に 置いておく。
■ 動かす
  • rampart clientを実行する。Eclipse の Console ビュー にて、以下のような内容のレスポンスが表示されることを確認(本当は一行)。
    <ns:echoResponse xmlns:ns="http://sample.tomcat.rampart.apache.org">
      <ns:return>Hello world</ns:return>
    </ns:echoResponse>
  • クラスパスの通っているところにlog4j.properties を置いておけば、クライアントのログ出力で、リクエストとレスポンスの中身がわかる。ユーザ名とパスワードを平文で送っている事がわかる。
    <?xml version='1.0' encoding='UTF-8'?>
    <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
      <soapenv:Header xmlns:wsa="http://www.w3.org/2005/08/addressing">
        <wsse:Security xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" soapenv:mustUnderstand="1">
          <wsu:Timestamp xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" wsu:Id="Timestamp-16291471">
            <wsu:Created>2009-11-28T04:49:45.562Z</wsu:Created>
            <wsu:Expires>2009-11-28T04:54:45.562Z</wsu:Expires>
          </wsu:Timestamp>
          <wsse:UsernameToken xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" wsu:Id="UsernameToken-27667505">
            <wsse:Username>alice</wsse:Username>
            <wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText">bobPW</wsse:Password>
          </wsse:UsernameToken>
        </wsse:Security>
        <wsa:To>https://127.0.0.1:8443/rampart-trial2/services/SimpleService</wsa:To>
         <wsa:MessageID>urn:uuid:488460AB6C6837F5671259383785225</wsa:MessageID>
        <wsa:Action>urn:echo</wsa:Action>
      </soapenv:Header>
      <soapenv:Body>
        <ns1:echo xmlns:ns1="http://sample.tomcat.rampart.apache.org">
          <param0>Hello world</param0>
        </ns1:echo>
      </soapenv:Body>
    </soapenv:Envelope>
  • localhost をキャプチャできるネットワークモニタで見てみると、ちゃんと暗号化して通信している事も確認できる。
一応、Eclipse + Axis + Rampart を連携させられた。ただ、Axis2 1.5.1 では README 通りの作法でも動かなかったのと、WTP との相性が悪いのもあって、1.4.1 でしか動かせてないのが悩ましい。まあ、実プロジェクトに導入するときのたたき台くらいにはなるかも。

2009年11月27日金曜日

Rampart 1.4 サンプルの観察 (policy編)

前ポストでは、Rampart サンプルのうち basic フォルダ下にあるものを眺めてみた。今回は、policy フォルダにあるものを見てみる。

basic サンプルには client.axis2.xml があって、これがクライアントの挙動を定義していたけど、policy サンプルでは 同じような事を WS-Policy の形式で policy.xml に記述している。service.xml の方にも、この形式でサービスの挙動が書かれている。

また、policy.xml と service.xml への記述以外にも、http://localhost:8080/axis2/services/sample0{1~6}?wsdl を見ると、WSDL にも含まれているのがわかる。更に、sample 06 では、サービスのメタデータとしてもやり取りされているのが観察できる。

policy サンプルを動かすやり方は、基本的に basic サンプルと同様。ただし、sample 05 を動かすために以下の追加作業が必要。
  • backport-util-concurrent.jar (バージョン3.1) が無いので、どこかから落としてきて(ググればすぐ見つかる)%AXIS2_HOME%\lib に置く
  • OpenSAML をここ(→URL)から落として取り替える。
また、sample 05, 06 では echo 以外の複数のサービスを立てているので、TCPMon で観察するには、build.xml の他に Java コードや *.xml に含まれているポート番号も書き換える必要がある。(見ればすぐ分かる)

ちなみにsample 05, 06 では他サンプルと共通の echo サービスのほかに、SAML セキュリティートークンを発行するための sts サービスがあり、さらにsample 06 ではセキュリティポリシーを表示するための mex サービスもある。(この複数サービス間のセキュリティ連携の辺りの観察が、特に面白い。)

以下、サンプルで使われている主な Webサービス/XML仕様。
仕様の名前サンプル番号
WS-Addressing (wsa)01~06
WS-Security (wsse)01~06
WS-Security (wsu)01~06
WS-Security (wsse11)04~06
XML-Signatures (ds)02~06
XML-Encryption (xenc)03~06
前ポスト参照
WS-Policy (wsp)01~06
どんなセキュリティトークンを使うかだとか、サービスのポリシーを定義するフレームワーク。WS-SecurityPolicyや、その他の Web サービス仕様による記述を<wsp:Policy>要素の中に含める形で、組み合わせて使う。この<wsp:Policy>要素は、WSDL、policy.xml、service.xml、また<mex:GetMetadata>リクエストへのレスポンスなどに含まれる事になる。
WS-SecurityPolicy (sp)01~06
WS-Policy フレームワークに組み込まれる形で、ポリシーのセキュリティ関連部分を表現する。
WS-Trust (t/wst)05, 06
このサンプルでは SAML セキュリティトークンのやり取りに使っている。
<wst:RequestSecurityToken>で SAML セキュリティトークンをリクエストすると、 SAML Assertion が乗っかったレスポンスが返ってくる。
WS-SecureConversation (wsc)04
複数のメッセージのやり取りが、トークンが付与されたコンテキストの中で行われるようにして、セキュアな「会話」を実現する仕組み。sample 04 では、3対の echo リクエスト/レスポンスとキャンセル要求を交換している。
SAML1.0 protocol (samlp)05, 06
SAML1.0 assertion (saml)05, 06
SAML の セキュリティトークン。サンプルでは STSサービスから<Assetion> を取得して、これを以降のリクエストに含める形で使用している。
WS-MetadataExchange (mex)06
<mex:GetMetadata> を使って、mex サービスからサービスのポリシー(WS-Policy)を取得する。このポリシーに従って、STS サービスからセキュリティトークン(SAML Assertion)を取得する。
Exclusive XML Canonicalization (ec) 05, 06
<ec:InclusiveNamespaces>を使って名前空間指定の一貫性を守って、XML-Signatures の署名対象が改ざんされたとみなされてしまう事を防止する。

2009年11月25日水曜日

Rampart 1.4 サンプルの観察 (basic)

Web サービスを調べ始めると、SOAP 仕様以外にもたくさん仕様があって、ちょっと戸惑う。で、これらを仕様書ベースで勉強しようと思っても、なんだか良く分からないし、全然面白くも無い。で、WS-*、XML-*という名前だけ知っていて、中身はよく知らないまま面倒くさくて放置してしまったりする。

こういう仕様の場合たいていそうだけど、やはり百聞は一見にしかずというか、実際に現物が動いている様子を観察すると、割と簡単に腑に落ちる事がある。実際にクライアントとサービスの間でやり取りされている生のメッセージを見ると、思ったより簡単な話だったりしてちゃんと納得できる事が多い。

実際にメッセージを流してみるには、Java の場合だと、Axis2 が結構枯れた技術で sample もよく動くので、手っ取り早い。以下、WS-Security 関連仕様を Axis2 Rampart のサンプルを使って、メッセージを観察したメモ。Axis2 Rampart のサンプルは、basic と policy の2群があるけど、今回は、basic 下のサンプルを使ってみた。(サンプルを動かすやり方は、前にここにメモした。)

仕様の名前サンプル番号
WS-Addressing (wsa)全部
HTTP のような下層の転送プロトコルに依存せずに、宛先や返信先などを指定する仕様
WS-Security (wsse)02~10
WS-Security (wsu)02~10
WS-Security (wsse11)09, 10
SOAP メッセージのセキュア化の仕組み。他の仕様(XML Signatures、XML Encryption )と組み合わせて使える。
XML Signatures (ds)04~10
XML 文書に電子署名する仕様
XML Encryption (enc)05,06,09,10
XML 文書の暗号化の仕様
XML-binary Optimized Packaging (xop)10
別の MIME パートとしてSOAP エンベロープの外に出したバイナリデータを参照する仕組み。ここでは暗号化した本文をバイナリで別MIMEパートにしている。

2009年11月24日火曜日

BIRT 試用

BIRT (Business Intelligence and Reporting Tools) のチュートリアルをやってみた。

内容は、顧客データベースの一個のテーブルからレコードを取り出して、州と市でグループ化した帳票を作るというもの。プロジェクトの作成から、帳票の見栄えを整えて Eclipse 上のプレビューで確認して終わるところまで結構サクサク進む。

アプリケーション・サーバから動かすのはチュートリアルの範囲外だけど、ここに[URL] TomcatとJBoss 上で動かすやり方がのっていて、チュートリアルで作った帳票も問題なく表示できる。

海外サイトだと結構情報も多くて、割と実績があるような雰囲気。結構、使えるかも。

ただ、本当に試したかったことは、JCR 実装の Jackrabbit と BIRT を連携させてみる事だったんだけど、すぐに使えそうな情報が無かった。データソースとして JCR を使ってみたかったけど、やはりそれほどメジャーではないし、アウト・オブ・ザ・ボックスな選択肢としては用意されていない模様。

XML 文書 を Jackrabbit に格納して、JCR API 経由を経由して BIRT で表示したいんだけど、どうやらそう簡単に行きそうではない。あと、文書の作成もできたらうれしいけど、レポーティングツールというくらいだし、これは無理か。

2009年11月22日日曜日

!MESSAGE Product xxx.product could not be found.

以下、RCP アプリの Java Web Start 配布を実験していてハマった事と、そのオチ。
========
Eclipse RCP (Rich Client Platform)をブラウザのリンクから Java Web Start で配布できるようにしたくて、Hello RCP を JNLP 経由で起動しようと悪戦苦闘するものの、どうにもはかどらない。

具体的には、ブラウザで開いた HTML ファイルから JNLPファイル へのリンクを叩くと、Java Web Start がファイルをダウンロードして署名をチェックしたり、一通りの動作をして、さて起動するかと思ったら、何も起こらないという状況。

環境は、
・Eclipse 3.5 Galileo
・JDK 1.6.0_13

JNLP ファイルの書式・内容や、Plug-in/バンドルの構成を確認しても、どうしても問題が見当たらない。英文やかなり古い記事も含めたネット上の文章を見ても、全然、関係ありそうなものがない。

しばらく悩んで右往左往した後、JNLP の osgi.instance.area プロパティで指定したフォルダに、以下のようなエラーログが吐かれているのに気付く。
!SESSION 2009-11-22 19:31:18.265 -----------------------------------------------
eclipse.buildId=unknown
java.version=1.5.0_15
java.vendor=Sun Microsystems Inc.
BootLoader constants: OS=win32, ARCH=x86, WS=win32, NL=ja_JP

!ENTRY org.eclipse.equinox.app 0 0 2009-11-22 19:31:19.203
!MESSAGE Product hellorcp.product could not be found.

!ENTRY org.eclipse.osgi 4 0 2009-11-22 19:31:19.234
!MESSAGE Application error
!STACK 1
java.lang.RuntimeException: No application id has been found.
at org.eclipse.equinox.internal.app.EclipseAppContainer.startDefaultApp(EclipseAppContainer.java:236)
…略
at com.sun.javaws.Launcher.run(Launcher.java:165)
at java.lang.Thread.run(Thread.java:595)

!ENTRY org.eclipse.osgi 2 0 2009-11-22 19:31:19.265
!MESSAGE The following is a complete list of bundles which are not resolved, see the prior log entry for the root cause if it exists:
!SUBENTRY 1 org.eclipse.osgi 2 0 2009-11-22 19:31:19.265
!MESSAGE Bundle hellorcp_1.0.0.200911221859 [68] was not resolved.
!SUBENTRY 2 hellorcp 2 0 2009-11-22 19:31:19.265
!MESSAGE Missing Constraint: Bundle-RequiredExecutionEnvironment: JavaSE-1.6

淡い期待を抱いて、エラーメッセージをいろいろ組み合わせてググって、更にいろんな記事を漁ってみるものの、やはり情報が得られず更に深く落胆する。

ふと思い立って、これまでよりじっくりとエラーログを見直して、「java.version=1.5.0_15」となっている事を、やっとの事で理解する。もう2・3年前からJDK 1.6を使っているはずなのに、なぜか Java 5 が動いている。ファイルの関連付けを確認すると、*.jnlp に JDK 1.5 の javaws.exe が関連付けられている。くっそ、これですかと・・・

Java 6 の javaws.exe が *.jnlp を開くように関連付けると、なんなく動いた。
まあ、3時間以上は悩んだな(笑

2009年11月21日土曜日

Drools で三目並べ(コード)

前ポストの Drools 三目並べのコード。

■ ルール定義

ブログにしては、ちょっと多めの行数だけど一行当たりの文字数が少ないので、記述量は全然大した事無い(以下、prettyprint が固まり気味なので小分けする)。

package tictactoe
 
import tictactoe.Cell;
import tictactoe.Row;
import tictactoe.Game;
import tictactoe.Game.State;

rule "初期化"
   when
      game: Game(state == State.INITIALIZED)
   then
      for (Row row: game.getRows()) insert(row);
      for (Cell[] row: game.getBoard()) { 
         for (Cell cell: row) insert(cell);
      }
      modify(game) { setState(State.START); }
end

rule "開始"
   when
      game: Game(state == State.START)
   then
      modify(game) { start(); }
end
rule "マシン側戦略1" @Strategy(Win)
salience 70
   when
      game: Game(state == State.MACHINE_TURN)
      row:  Row($cells: cells)
      c1:   Cell(state == Cell.MACHINE) from $cells 
      c2:   Cell(state == Cell.MACHINE) from $cells
      c3:   Cell(state == Cell.FREE) from $cells
      eval (c1 != c2)
   then
      modify(c3) { setState(Cell.MACHINE); }
      modify(game) { setState(State.JUDGING); }
end

rule "マシン側戦略2" @Strategy(Block)
salience 60
   when
      game: Game(state == State.MACHINE_TURN)
      row:  Row($cells: cells)
      c1:   Cell(state == Cell.USER) from $cells 
      c2:   Cell(state == Cell.USER) from $cells 
      c3:   Cell(state == Cell.FREE) from $cells 
      eval (c1 != c2)
   then
      modify(c3) { setState(Cell.MACHINE); }
      modify(game) { setState(State.JUDGING); }
end
rule "マシン側戦略3" @Strategy(Fork)
salience 50
   when
      game: Game(state == State.MACHINE_TURN)
      c:    Cell(state == Cell.FREE)
      mc1:  Cell(state == Cell.MACHINE)
      mc2:  Cell(state == Cell.MACHINE)
      fc1:  Cell(state == Cell.FREE)
      fc2:  Cell(state == Cell.FREE)
      r1:   Row(cells contains c, cells contains mc1, cells contains fc1)
      r2:   Row(cells contains c, cells contains mc2, cells contains fc2)
      eval (r1 != r2 && c != fc1 && c != fc2)
   then
      modify(c) { setState(Cell.MACHINE); }
      modify(game) { setState(State.JUDGING); }
end

rule "マシン側戦略4" @Strategy(Block Opponent^s Fork)
salience 40
   when
      game: Game(state == State.MACHINE_TURN)
      c:    Cell(state == Cell.FREE)
      mc1:  Cell(state == Cell.USER)
      mc2:  Cell(state == Cell.USER)
      fc1:  Cell(state == Cell.FREE)
      fc2:  Cell(state == Cell.FREE)
      r1:   Row(cells contains c, cells contains mc1, cells contains fc1)
      r2:   Row(cells contains c, cells contains mc2, cells contains fc2)
       eval (r1 != r2 && c != fc1 && c != fc2)
   then
      modify(c) { setState(Cell.MACHINE); }
      modify(game) { setState(State.JUDGING); }
end
rule "マシン側戦略5" @Strategy(Center)
salience 30
   when
      game: Game(state == State.MACHINE_TURN)
       c: Cell(state == Cell.FREE, x == 1, y == 1)
   then
      modify(c) { setState(Cell.MACHINE); }
      modify(game) { setState(State.JUDGING); }
end

rule "マシン側戦略6" @Strategy(Opposite Corner)
salience 20
   when
      game: Game(state == State.MACHINE_TURN)
      uc:   Cell(state == Cell.USER, $ux: x, $uy: y)
      mc:   Cell(state == Cell.FREE, $mx: x, $my: y)
      eval (($ux==0 && $mx==2 || $ux==2 && $mx==0) 
          && ($uy==0 && $my==2 || $uy==2 && $my==0))
   then
      modify(mc) { setState(Cell.MACHINE); }
      modify(game) { setState(State.JUDGING); }
end
rule "マシン側戦略7" @Strategy(Empty Corner)
salience 10
   when
      game: Game(state == State.MACHINE_TURN)
      c: Cell(state == Cell.FREE, x != 1, y != 1)
   then
      modify(c) { setState(Cell.MACHINE); }
      modify(game) { setState(State.JUDGING); }
end

rule "マシン側戦略8" @Strategy(Empty Side)
   when
      game: Game(state == State.MACHINE_TURN)
      c: Cell(state == Cell.FREE)
   then
      modify(c) { setState(Cell.MACHINE); }
      modify(game) { setState(State.JUDGING); }
end
rule "ユーザの番"
   when
      game: Game(state == State.USER_TURN)
   then
      update(game.play());
      modify(game) { setState(State.JUDGING); }
end

rule "決着"
salience 20
   when
      game: Game(state == State.JUDGING)
      row:  Row($cells: cells)
      c1:   Cell($s: state, state != Cell.FREE) from $cells
      c2:   Cell(state == $s) from $cells
      c3:   Cell(state == $s) from $cells
      eval (c1 != c2 && c2 != c3 && c3 != c1)
   then
      System.out.printf("== %s won!%n",
            (Cell.MACHINE == $s) ? "machine": "user");
      modify(game) { setState(State.FINISHED); }
end

rule "引き分け"
salience 10
   when
      game: Game(state == State.JUDGING)
      not Cell(state == Cell.FREE)
   then
      System.out.println("== draw!");
      modify(game) { setState(State.FINISHED); }
end

rule "続行"
   when
      game: Game(state == State.JUDGING)
   then
      modify(game) { change(); }
end
(割とすっきり書けたと思うが、いかにも Java なコードがまだまだ少なくないので、その辺りもう少し工夫したい。)

■ Java コード

ルール定義を見れば察しが付くようなコードばかりなので面白く無いので、以下のエベントリスナの定義を含む main() だけ抜粋。

 public static final void main(String[] args) throws Exception {
    KnowledgeBase kbase = readKnowledgeBase();
    StatefulKnowledgeSession ksession = kbase.newStatefulKnowledgeSession();
    final Game game = new Game();
    ksession.insert(game);
    ksession.addEventListener(new DefaultAgendaEventListener() {
       public void afterActivationFired(AfterActivationFiredEvent event) {
           Rule rule = event.getActivation().getRule();
           String strategy = rule.getMetaAttribute("Strategy");
           if (null != strategy) {
              System.out.printf(" strategy = %s%n", strategy);
           } else if (Arrays.asList(new String[]{
                   "続行", "引き分け", "決着"}).contains(rule.getName())) {
               game.printBoard();
           }
       }
    });
    ksession.fireAllRules();
 }
この辺りをもう一工夫すれば、ルール定義 と Java コードをもっときれいに分離できそうな気がする。

Drools で三目並べ

前に Drools で遊んだときは、ハノイの塔を解くルールを書いてみた。こんどは三目並べ(tic-tac-toe)を書いてみる。

初歩的なように見えて、これで状態遷移とか、対話型とか、アルゴリズムの優先順位とか、後で応用が利きそうなテーマも含まれていたりする。

■ 状態遷移
ゲームの状態遷移は下図のようになる。ひとたび Java コードから fireAllRules() して開始したら、勝敗が決まるか引分けになるまで、遷移に関しては Drools に一任したい。
■ 対話型
ユーザの番のとき、マス目の番号をコンソールから受け付ける入力ロジックを、RHS (ルールのアクション部分) で実行する。non-deterministic な(実行のたびに異なる)要素をルールの実行に組み入れてみたいというのが趣旨。

■ アルゴリズムの優先順位付け
wikipedia のこのエントリを参考にした。
高優先順位の順に、以下のようなマス目の選び方になる。
  1. 後一手で列が完成する盤面なら、その列を完成して終了。
  2. ユーザが後一手で一列完成できる盤面なら、それをブロックする。
  3. 後一手で二方向の王手が作れる盤面なら、それを作る。
  4. ユーザが後一手で二方向の王手が作れる盤面なら、ブロックする。
  5. 中央が空いていたら、そこに置く。
  6. 相手が角を取っていて対角が空いていたら、そこに置く。
  7. 角が空いていたらそこに置く。
  8. 空いているマスがあったら、そこに置く。

こういうのは手続き型とは大幅に違うコーディングになるので、面白い。Prolog みたいな宣言型論理プログラミングとも、やっぱり違っていてなんか独特。

■ 実行
実行は Eclipse 3.5 Galileo + Drools plugin 5.0.1でやってみた。ユーザ入力は Eclipse のコンソールビューを用いている。

(ちなみに Drools プラグインのデバッグ機能の Working Memory ビューを使おうとすると、実行時にエラーが出てさっぱり動かないが、これは Drools が Eclipse 3.5 に対応していないかららしい。Drools 5.1 では 直っているらしいが、まだ update サイトにないので、今回は見送った。[参考URL])

こんな感じの実行結果になる。
first player? (y/n)
y
<user> input cell number(0-8): 5
- - -
- - u
- - -
<machine> strategy = Center
- - -
- m u
- - -
<user> input cell number(0-8): 1
- u -
- m u
- - -
<machine> strategy = Block Opponent^s Fork
- u m
- m u
- - -
<user> input cell number(0-8): 6
- u m
- m u
u - -
<machine> strategy = Empty Corner
- u m
- m u
u - m
<user> input cell number(0-8): 0
u u m
- m u
u - m
<machine> strategy = Block
u u m
m m u
u - m
<user> input cell number(0-8): 7
== draw!
u u m
m m u
u u m

次ポストでは、コードをさらしてみる。

2009年11月20日金曜日

Rampart 1.4/暗号化/クライアント側

前ポストで、暗号化された SOAP メッセージを、サービス側が期待して待つようにした。今度はクライアントに暗号機能を与えてみる。 ■ コード クライアントのリクエスト送信コードは以下のようになる。見ての通りいかにもセキュリティっぽいコードは無い。ただし ConfigurationContext というものを使って、実行時の振る舞いに設定情報を反映させることになる。
import …略…

public class Client {
   private static final String URL 
       = "http://localhost:8888/axis2/services/StockQuoteService";

   private static final String AXIS2_REPO = "resources/axis-repo";

   public static void main(String[] args) throws Exception {
      ConfigurationContext ctx = ConfigurationContextFactory
            .createConfigurationContextFromFileSystem(
               AXIS2_REPO, AXIS2_REPO + "/conf/axis2.xml");

      Options options = new Options();
      options.setAction("urn:getPrice");
      options.setTo(new EndpointReference(URL));

      ServiceClient client = new ServiceClient(ctx, null);
      client.setOptions(options);

      OMElement response = client.sendReceive(getPayload("test"));
      System.out.println(response);
   }
   private static OMElement getPayload(String value) {
       OMFactory factory = OMAbstractFactory.getOMFactory();
       OMNamespace ns = factory.createOMNamespace(
          "http://quickstart.samples/xsd","ns1");
       OMElement elem = factory.createOMElement("getPrice", ns);
       OMElement childElem = factory.createOMElement("symbol", null);
       childElem.setText(value);
       elem.addChild(childElem);

       return elem;
   }
}
main() 冒頭で ConfigurationContext を初期化している AXIS2_REPO はこんな感じ。
axis-repo/
   conf/
      axis2.xml
   modules/
      addressing-1.5.1.mar
      rampart-1.4.mar
modules 下の *.mar は手近などこかから持ってきて配置する。axis2.xml は、サンプルなんかに含まれるものを加工すると楽。ここでは rampart サンプル 05番 の client.axis2.xml を使う事にした。このサンプルには IN と OUT 両方の暗号化設定が記述されているけど、今やりたいのはリクエストだけなので InflowSecurity の記述は削除する。 残った要点はこの部分。
<module ref="rampart" />

<parameter name="OutflowSecurity">
   <action>
      <items>Encrypt</items>
      <encryptionUser>service</encryptionUser>
      <encryptionPropFile>client.properties</encryptionPropFile>
   </action>
</parameter>
rampart モジュールを使う事の宣言と、OutflowSecurity のところで、クライアントからサービス方向のセキュリティ設定が記述されているのがわかる。 上の XMLで <encryptionPropFile>から参照されている client.properties は、以下のような内容で resources 直下に作る。
org.apache.ws.security.crypto.provider=org.apache.ws.security.components.crypto.Merlin
org.apache.ws.security.crypto.merlin.keystore.type=jks
org.apache.ws.security.crypto.merlin.keystore.password=rampart
org.apache.ws.security.crypto.merlin.file=ourkeys.jks
~crypto.merlin.file で指定しているキーストア・ファイル ourkeys.jks は、ここでは手を抜いてサーバと同じものを使う事にした(なまじ真面目にやると、思い通りに動かないときキーの作り方が悪いのか設定の仕方が悪いのか切り分けしにくくて面倒くさい)。この ourkeys.jks も resouces 下に置く。resouces には、お好みで log4j.properties なども置いておく。 ここまでまとめるとこんな感じになる
{作業ディレクトリ}
   resources/
      axis-repo/ … 前述
      client.properties
      log4j.properties
      ourkeys.jks
   src/
      Client.java
      PWCBHandler.java
■ ビルド → 実行 この作業ディレクトリで、こんな要点の build.xml を書く。
  • axis2 の lib ディレクトリ内の *.jar を java タスク と javac タスクのクラスパスに含める。
  • build フォルダを作って、javac で コンパイルした2つのクラスファイルをそこに入れる。
  • resources 下の client.properties、log4j.properties、ourkeys.jks を build 下にコピーする。
  • java タスクでは build/ もクラスパスに含める。
実行する前に、TCPMon (他の何かでもOK)をポート:8888で待たせておく。 で、ant から実行。 上手くいくと、こんな感じのリクエストが送られる。
POST /axis2/services/StockQuoteService HTTP/1.1
Content-Type: text/xml; charset=UTF-8
SOAPAction: "urn:getPrice"
User-Agent: Axis2
Host: 127.0.0.1:8888
Transfer-Encoding: chunked

83c
<?xml version='1.0' encoding='UTF-8'?>
   <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xenc="http://www.w3.org/2001/04/xmlenc#">
      <soapenv:Header>
         <wsse:Security xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" soapenv:mustUnderstand="1">
            <xenc:EncryptedKey Id="EncKeyId-urn:uuid:7934811BD783B11FAA12585771335152">
               <xenc:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-1_5" />
               <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
                  <wsse:SecurityTokenReference>
                     <ds:X509Data>
                        <ds:X509IssuerSerial>
                           <ds:X509IssuerName>CN=service,OU=a,O=a,L=a,ST=a,C=aa</ds:X509IssuerName>
                           <ds:X509SerialNumber>1258572637</ds:X509SerialNumber>
                        </ds:X509IssuerSerial>
                     </ds:X509Data>
                  </wsse:SecurityTokenReference>
               </ds:KeyInfo>
               <xenc:CipherData>
                  <xenc:CipherValue>KWbsTXPE5TB4KzH9VRe2TJXT51tPDQ3z0UilymTUu165MR/hGB7uHyscSeYB1TEkA+23jrx3S9uo+nFahrBOuMOUsUvTckYvmi66K/0GLP8QAjg0gubiFmxF6d3JjI5mNuaGHWWspplKxEAFS99Xp4MQTXO52Gn/qo99d+vibzg=</xenc:CipherValue>
               </xenc:CipherData>
               <xenc:ReferenceList>
                  <xenc:DataReference URI="#EncDataId-6393126" />
               </xenc:ReferenceList>
            </xenc:EncryptedKey>
         </wsse:Security>
      </soapenv:Header>
      <soapenv:Body>
         <xenc:EncryptedData Id="EncDataId-6393126" Type="http://www.w3.org/2001/04/xmlenc#Content">
            <xenc:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#aes128-cbc" />
            <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
               <wsse:SecurityTokenReference xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
                  <wsse:Reference URI="#EncKeyId-urn:uuid:7934811BD783B11FAA12585771335152" />
               </wsse:SecurityTokenReference>
            </ds:KeyInfo>
            <xenc:CipherData>
               <xenc:CipherValue>9l9w23N4cauBPWarHAMrPI2bjzR5bfZNsHKAC5NTkMNzitW/3JM74mWh83LuhH5e/RAV3mxaDcYJczmLD0tiVqJJGGuF85p+AocHCnjWz1we0/dYaMoDBSYn2kINp+6//70wwGBxHByT53OP7SpLfkJD9Fnnd4TVBg3iwamszR49oCYv64Z3wYLV/Ie6OMVy+kFbcXf/xFG/MpfmrJdkaEwq+uheOrdT3yYneoNoUbvvhmVHirnEXHryGfKu+36pgIpKDkrvbS9EFZ3WmIo+pQwJ3wq2BzFLWK/ackH0LV8=</xenc:CipherValue>
            </xenc:CipherData>
         </xenc:EncryptedData>
      </soapenv:Body>
   </soapenv:Envelope>0
(ちなみにレスポンス方向は暗号化してないので、プレーンなSOAP メッセージのままなのが TCPMon を見るとわかる。) この見所は、以下のようなところ
  • Header に EncryptedKey、Body に EncryptedData があること
  • EncryptedKey の暗号方式はRSA、EncryptedData の暗号方式はAES
  • EncryptedKey の DataReference から、EncryptedData を参照している事
  • EncryptedData の SecurityTokenReference から、EncryptedKey を参照している事
  • X509IssuerSerial に鍵を作ったときに入力した内容が現れている事
■ 補足 Rampart のsample を見ると、タイムスタンプ、署名、署名+暗号化、暗号化部分を別のMIMEパートに分けて出す方法など、必要な情報がほぼ十分に載っていてお勧め。

Rampart 1.4/暗号化/サービス側

前ポストの StockQuoteサービスとクライアントのペアを修正して、暗号化された SOAP メッセージ をやり取りするようにしてみる。まずはサービスから。 ■ サービスを暗号前提にする
  • コールバッククラスを書く。現実のコールバックは LDAP とかを見に行ったりして本物のパスワードを設定するけど、関係ないのでここは手を抜く。
    package samples.quickstart.callback;
    import 略
    
    public class PWCBHandler implements CallbackHandler {
       public void handle(Callback[] callbacks) 
             throws IOException, UnsupportedCallbackException {
          for (Callback callback: callbacks) {
             WSPasswordCallback pwcb = (WSPasswordCallback)callback;
             System.out.println("ID=" + pwcb.getIdentifer());
             pwcb.setPassword("rampart");
          }
       }
    }
  • resources/META-INF/services.xml に暗号機能を追記する。
    <service name="StockQuoteService" scope="application" targetNamespace="http://quickstart.samples/">
        <descript…
          …略
        …name="ServiceClass">samples.quickstart.service.pojo.StockQuoteService</parameter>
        <module ref="rampart" />
        <parameter name="InflowSecurity">
          <action>
            <items>Encrypt</items>
            <passwordCallbackClass>samples.quickstart.callback.PWCBHandler</passwordCallbackClass>
            <decryptionPropFile>service.properties</decryptionPropFile>
          </action>
        </parameter>
    </service>
  • 上のコードの<decryptionPropFile>から参照されているファイル service.properties を、resources/ 下に作り、arrファイル 直下に含まれるようにbuild.xml を編集。
    org.apache.ws.security.crypto.provider=org.apache.ws.security.components.crypto.Merlin
    org.apache.ws.security.crypto.merlin.keystore.type=jks
    org.apache.ws.security.crypto.merlin.keystore.alias=service
    org.apache.ws.security.crypto.merlin.keystore.password=rampart
    org.apache.ws.security.crypto.merlin.file=ourkeys.jks
  • 上記 service.properties の中から参照されているキーストア ourkeys.jksを、keytool を使って resources 下に作る。キーストア・パスワードを聞いてくるので、service.properties にあわせて rampart と入力。出来てきた ourkeys.jks も arr ファイル直下に含まれるようにbuild.xml を編集
    keytool -keyalg "RSA" -sigalg "MD5withRSA" -genkeypair ourkeys.jks -alias client
    いろいろ聞かれるので適当に答える
    keytool -keyalg "RSA" -sigalg "MD5withRSA" -genkeypair ourkeys.jks -alias service
    いろいろ聞かれるので適当に答える
  • generate.service で arr ファイルに丸める。中身はこんな感じ
    ourkeys.jks
    service.properties
    META-INF/
       MANIFEST.MF
       services.xml
    samples/
       quickstart/
          callback/
             PWCBHandler.class
          service/
             pojo/
                StockQuoteService.class
  • 再デプロイする。手でコピーしても良いけど、一発で成功しなかったら後で繰り返すとき面倒なので ant タスクを書いておくと便利。
    <target name="deploy.service" depends="generate.service">
        <copy file="${build.dir}/StockQuoteService.aar" 
           toDir="${TOMCAT_HOME}/webapps/axis2/WEB-INF/services"/>
    </target>
soapUI なんかを使って、暗号化なしの素の getPrice メッセージを送ってみると、サーバ側で以下のような例外がトレースされる。
org.apache.axis2.AxisFault: WSDoAllReceiver: 
Incoming message does not contain required Security header
「セキュリティヘッダが見当たらない」と言ってきたので、ひとまず上手くいった模様。 次は、クライアントがリクエストを暗号化して送るようにする。→[次ポスト]

2009年11月19日木曜日

Rampart 1.4/quickstart

Axis のサンプルを独立にビルド・デプロイして、独立のクライアントから呼ぶ方法。 こういうシンプルなサービスとクライアントのペアを作れるようにしておくと、例えば、これに対して暗号化を追加してみるとか、いろんな実験の叩き台として便利。 以下、前提
  • Tomcat v6.0 が{tomcat-home} にインストールされている
  • Axis2 1.5 が{axis2_home} にインストールされている
■ quickstart のビルド
  • 適当な所に{axis2_home}/samples/quickstart をコピーする。
  • build.xml の AXIS2_HOME が相対パスで指定されているので、絶対パス(もしくは環境変数)に書き換える。
  • ant を叩いてビルド。generics 関連の警告が出るので嫌だったら直す。
  • 出来上がったStockQuoteService.aarを{tomcat-home}/webapps/axis2/WEB-INF/services下に置く。面倒なので build.xml に deploy.service 的な ターゲットを書いておくと楽。
  • axis2 の管理コンソール を開いてWSDLのURLを得る
  • その WSDL を利用して、soapUI とかからメッセージを投げて動きを確かめる。 update{symbol="hundred", price=100}のような適当なメッセージを送って、getPrice{symbol="hundred"}で return=100 なら成功
■ クライアントコードを書く。
  • WSDL を作成する。ant generate.wsdl
  • WSDL からクライアントスタブを作成する。
    <target name="generate.stub">
      <taskdef name="axis2-wsdl2java"
                classname="org.apache.axis2.tool.ant.AntCodegenTask"
                classpathref="axis2.classpath"/>
      <mkdir dir="generated"/>
      <axis2-wsdl2java
                wsdlfilename="${build.dir}/StockQuoteService.wsdl" output="generated/" />
    </target>
    
  • クライアントのエントリポイントを書く
    package samples.quickstart;
    
    import samples.quickstart.StockQuoteServiceStub.*;
    
    public class Client {
      public static void main(String[] args) throws Exception {
        StockQuoteServiceStub service = new StockQuoteServiceStub();
        GetPrice arg0 = new GetPrice();
        arg0.setSymbol("hundred");
        GetPriceResponse response = service.getPrice(arg0);
        System.out.println("price=" + response.get_return());
      }
    }
  • price=100 がコンソール出力されたら成功
■ 自動生成クライアントスタブを使わない場合
  • こんな感じのコードでも可
    public class Client {
        private static final String URL 
      = "http://localhost:8888/axis2/services/StockQuoteService.StockQuoteServiceHttpSoap12Endpoint";
    
     private static final String AXIS2_REPO = "axis-repo";
    
        public static void main(String[] args) throws Exception {
            ConfigurationContext ctx = 
                ConfigurationContextFactory.createConfigurationContextFromFileSystem(
                    AXIS2_REPO, AXIS2_REPO + "/conf/axis2.xml");
            
            ServiceClient client = new ServiceClient(ctx, null);
            Options options = new Options();
            options.setAction("urn:getPrice");
            options.setTo(new EndpointReference(URL));
            client.setOptions(options);
            
            OMElement response = client.sendReceive(getPayload("hundred"));
            System.out.println(response);
        }
        private static OMElement getPayload(String value) {
            OMFactory factory = OMAbstractFactory.getOMFactory();
            OMNamespace ns = factory.createOMNamespace(
              "http://quickstart.samples/xsd","ns1");
            OMElement elem = factory.createOMElement("getPrice", ns);
            OMElement childElem = factory.createOMElement("symbol", null);
            childElem.setText(value);
            elem.addChild(childElem);
            
            return elem;
        }
    }
  • ちょっと何かを試してみる程度なら、スタブ版よりこっちの方が見通しが良くて楽かもしれない。

2009年11月18日水曜日

Axis2 1.5/Rampart 1.4

Rampart をいろいろ試していると、暗号化するところで下のような例外が発生してなかなか解決しない。何やら署名/暗号化のアルゴリズムがサポートされていないという例外が出る。
org.apache.ws.security.WSSecurityException: 
WSHandler: Encryption:
error during message processingorg.apache.ws.security.WSSecurityException:
An unsupported signature or encryption algorithm was used
(unsupported key transport encryption algorithm:
No such algorithm: http://www.w3.org/2001/04/xmlenc#rsa-1_5)

一応、原因と解決策がわかったので、記録しておく。
参考URL:No such algorithm: http://www.w3.org/2001/04/xmlenc#rsa-1_5

■ 原因
暗号技術は米国の輸出法で規制されていて、JDK (JRE)のデフォルトでは、制限つきの暗号レベルになっている。

■ 対策
以下のステップで解決
◆Java Cryptography Extension (JCE) を 「Unlimited Strength Jurisdiction Policy Files 6 」に差し替える。
  • jce_policy-6.zip をダウンロードして展開する。
  • 新しい local_policy.jar と US_export_policy.jar で、{JDK_HOME}/jre/lib/security 下のものを置き換える。詳しくは README.txt参照。

◆対応するセキュリティプロバイダが、実行時に使われるようにする。
  • bcprov-jdk16-144.jarをダウンロードする。
  • クラスパスに含める。スタンドアロンなら単にjava コマンドの-classpath に含まれるようにする。Web アプリなら、多分 WEB-INF/lib に置けばいいはず。

■ 補足
参考URLと以下の点で違う。
  • security.policyファイル に security.provider.nを指定する箇所があるが、無くても大丈夫だった。
  • jre/lib/ext には、とりあえず追加しなくても問題なかった。

2009年11月17日火曜日

Rampart sample と TCPmon

Axis2 1.5.1で rampart 1.4 の sample を動かして、SOAP メッセージを TCPMon で観察するやり方のメモ。basic と policy があるけど、ここでは basic で説明。Windows 環境で、基本、コマンドプロンプトから Ant 叩くだけ。 (以下、rampart を展開したディレクトリを {rampart}とする。あと {rampart}\samples\basic\README.txt を読んでいること前提。) ■ 準備
  • コマンドプロンプトから、{rampart}/samples/basic に行く
  • 環境変数 AXIS2_HOME が 設定されていることを確認。
  • build.xml の client.port プロパティを 8080 から適当な値に書き換える(ここでは 8888 とした)。このポートがTCPMonの受け口になる。
    <property name="client.port" value="8888"/>
  • addressing.mar プロパティを addressing-1.4.mar から ~1.5.1 に書き換える
    <property name="addressing.mar" value="addressing-1.5.1.mar"/>
  • client.axis2.xml の以下の部分をコメントアウトする
    <!--    <transportSender name="tcp"
                         class="org.apache.axis2.transport.tcp.TCPTransportSender"/>-->
■ 実行 まずは、とりあえず basic 下の sample01 を動かしてみる。
  • TCPMon を起動。Admin タブで以下の設定を追加
    • Listen Port 8888
    • Target Hostname 127.0.0.1
    • Target Port 8080
  • コマンドプロンプトで、samples\basic下からサービスを立ち上げる。自前の HTTPサーバを立てている模様。
    ~~\samples\basic>ant server.01    
  • もう一個のコマンドプロンプトでクライアントを実行する。
    ~~\samples\basic>ant client.01    
  • 以下の出力を確認(ほんとは1行)
    [java] <ns:echoResponse xmlns:ns=
        "http://sample01.samples.rampart.apache.org">
        <ns:return>Hello world</ns:return></ns:echoResponse>
  • ログを見たかったら、{rampart}\samples\basic\build\temp_client に log4j.properties を適当に書けばダラダラ出力される。
  • TCPMon はこんな感じになる。sample01 は WS-Security を使っていないので、Adressing が付いただけのあっさりしたヘッダ。
  • ちなみにsample02をやってみると、SOAPヘッダにこんなのが追加されたのわかる。
     <wsse:Security 
           xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
           soapenv:mustUnderstand="1">
       <wsu:Timestamp 
           xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" 
           wsu:Id="Timestamp-7866553">
         <wsu:Created>2009-11-16T10:47:08.375Z</wsu:Created>
         <wsu:Expires>2009-11-16T10:52:08.375Z</wsu:Expires>
       </wsu:Timestamp>
       <wsse:UsernameToken 
           xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" 
           wsu:Id="UsernameToken-31658378">
         <wsse:Username>bob</wsse:Username>
         <wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest"
           >KnUNxZhw8uDNeBpJXOwb6uZzdP8=</wsse:Password>
         <wsse:Nonce>O0u/xram0QS4x7P6sgFijQ==</wsse:Nonce>
         <wsu:Created>2009-11-16T10:47:08.359Z</wsu:Created>
       </wsse:UsernameToken>
     </wsse:Security>
ちょっと面白い。

Eclipse の BPMN ツール→駄目

Eclipse の [Install New Software]で、SOA Development の下を眺めていたら、BPMN Project Feature というのがあった。試しに入れてみたけどどうも使えない。
  • 日本語が使えない。Swimlane 名には使えるようだけど、Task 名に日本語を指定しても表示されない。
  • すでにダイアグラムにおいてある要素を Connecting Object で接続しようとしても繋がらない。(BPMNのルールとは関係なし)
  • Connecting Object の 表示が途切れ途切れになる。
  • 画面に置いたはずの Flow Object がSwimlane の裏側に行ってしまって、表示されない。
  • プロダクトのWeb サイトがところどころリンク切れで管理されていない。
  • その他無数・・・

とまあ、要するに開発者が EMF の勉強のために、ためしに挑戦してみた程度のレベルで、他人に使ってもらえる域のものではない感じ。

速攻で Uninstall。

2009年11月16日月曜日

Tomcat v6.0/Axis2 1.5.1/Rampart 1.4

Tomcat に Axis Rampart を組み込んでみる。

  • rampart 1.4 をダウンロードして展開(以降 {RAMPART_HOME})。
  • Tomcat の webapps に axis2.war (war 版でダウンロードしたもの) を置く。hotdeploy が効いて、展開された war から axis2フォルダ(以下 {AXIS_HOME})が作られるのを確認。
  • {RAMPART_HOME}/module 下の*.jar を、{AXIS_HOME}/WEB-INF/modules 下にコピーする。
  • {RAMPART_HOME}/lib 下の*.jar を、{AXIS_HOME}/WEB-INF/lib 下にコピーする。
  • (上の二つのコピーは、sample 下のbuild.xml を Ant で叩いても良いらしい。)
  • Quick Start Guide には log4j と xalan を別途用意するようなことが書いてるけど、Axis2 1.5.1には最初から入ってるっぽい。)
  • Tomcat を立ち上げて、ブラウザで http://localhost:8080/axis2/ を開く。
  • [Administration] から username=admin, password=axis2でログイン
  • "engage module">[For all Services]>[Select a Module] で rampart を指定し、[Engage]。
  • "System Components">[Available Modules] で、rampart が入った事を確認。

WS-Security の準備ができた。

2009年11月15日日曜日

Axis2 1.4/Galileo/WTP

古い developerworks の記事事の Web サービスのサンプルを、Eclipse 3.5 Galileo の WTP から動かしてみる。

結構良さげな記事なのに、細かいところが各製品の最近の版といろいろ食い違っていてもったいない。それと Eclipse でできるかも試してみたい。というわけでやってみる。(記事を見るには登録が必要)

■ 事前準備
まずは、Axis2 1.5.1 をダウンロードして展開して。Eclipse 3.5 の [Preference] -> [Web Services] から、Axis2 の runtime location として設定しておく。重要:現時点(2009/11/15)最新の Axis2 1.5.1 だと、何かがかみ合わなくて動かないので、1.4.1 を使う。)

■ Dynamic Web Project 作成
  • 名前はとりあえず"axis2-trial1"とする
  • Configuration で Axis2 を選択。無かったら [Modify...]で Axis2 Web Services にチェックして Project Facet に追加指定。
  • Finish する。WEB-INF の下がちょっと独特なので、観察しておく。
■ Web サービス作成
  • リスト 26 の CMSService クラスを作る。
  • リスト 22 の services.xml を書く。
  • CMSService 上のコンテキストメニューから Web サービスを新規作成。
    • CMSServiceクラスからの Bottom up になっている事を確認
    • スライダーを Assemble service に下げる
    • [Next]で、service.xml を指定する。
    • Finish
  • WEB-INF/service 下に自動生成ファイルが出来てくるので、ちょっと観察しておく。
■ 実行してみる
  • Servers ビューで、Tomcat v6.0 に プロジェクトを追加
  • Tomcat v6.0 を起動
  • ブラウザから下記URLを開いて管理コンソールが立ち上がっているのを確認。
    http://localhost:8080/axis2-trial1/axis2-web/index.jsp
  • 管理コンソール の [Services]→[CMSService] で wsdl を確認
  • 上記 WSDL を用いて、ブラウザとか、Eclipse の Web Service Explorer とか、soapUI (フリーのSOAPツール。お勧め。)とかで確認。
    • getNumberOfArticle では、固定値42が返される。
    • addArticle では、Eclipse のコンソールにリクエストがトレースされる。ただしレスポンスは返されない(非同期)
  • リスト27リスト30のクライアントコードを試してみる。ポート番号を書き換えれば、WTP のTCP/IP Monitor で SOAP メッセージが確認できる。

2009年11月14日土曜日

WS-Addressing/CXF 2.2.4

前回のポストで、ある文字列を渡すと、それを加工して別の文字列を返すWeb サービスを作った。これに WS-Addressing を追加してみる。 (作業環境は前回同様 Galileo + Tomcat v6.0。CXF 2.2.4 は Web service runtime として使用。) もともとのSOAPメッセージは、こんなリクエストとレスポンスだった。 ◆リクエスト
<soap:Envelope 
    xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
  <soap:Body>
    <ns2:greet xmlns:ns2="http://mycompany.com/greeting/">
      <username>World</username>
    </ns2:greet>
  </soap:Body>
</soap:Envelope>
◆レスポンス
<soap:Envelope 
    xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
  <soap:Body>
    <ns2:greeting-response xmlns:ns2="http://mycompany.com/greeting/">
      <return>Hello, World!</return>
    </ns2:greeting-response>
  </soap:Body>
</soap:Envelope>
これが、WS-Addressing を使うと、ヘッダのところがこんな風になる。 ◆リクエスト
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
  <soap:Header>
    <Action xmlns="http://www.w3.org/2005/08/addressing">
      http://mycompany.com/greeting/Greeting/greet
    </Action>
    <MessageID xmlns="http://www.w3.org/2005/08/addressing">
      urn:uuid:364e3949-e49a-44e7-a6c9-843ac2acce3c
    </MessageID>
    <To xmlns="http://www.w3.org/2005/08/addressing">
      http://localhost:8080/cxf-trial1/services/GreetingPort
    </To>
    <ReplyTo xmlns="http://www.w3.org/2005/08/addressing">
      <Address>
        http://www.w3.org/2005/08/addressing/anonymous
      </Address>
    </ReplyTo>
  </soap:Header>
  <soap:Body>
  ・・・
◆レスポンス
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
  <soap:Header>
    <Action xmlns="http://www.w3.org/2005/08/addressing">
      http://mycompany.com/greeting/Greeting/response
    </Action>
    <MessageID xmlns="http://www.w3.org/2005/08/addressing">
      urn:uuid:764d8c64-d8fb-46ba-8bee-d86761386b8d
    </MessageID>
    <To xmlns="http://www.w3.org/2005/08/addressing">
      http://www.w3.org/2005/08/addressing/anonymous
    </To>
    <RelatesTo xmlns="http://www.w3.org/2005/08/addressing">
      urn:uuid:364e3949-e49a-44e7-a6c9-843ac2acce3c
    </RelatesTo>
  </soap:Header>
  <soap:Body>
  ・・・
リクエストに返信先を示す<ReplyTo>が含まれ、レスポンスにはもともとのメッセージを識別するIDが含まれているのがわかる。 このための作業は、CXF 的には Feature と言うものを指定して行うらしい。例えば今回は、サービス側のエンドポイントを beans.xml で構成したけど、その jaxws:endpoint で以下のように Feature を追加する。
    <jaxws:features>
      ・・・
        <wsa:addressing xmlns:wsa="http://cxf.apache.org/ws/addressing"/>
    </jaxws:features>
またクライアント側は 非 Spring の Java コードで書いたけど、その場合こんな風にFeature を追加する。
    factory.getFeatures().add(new WSAddressingFeature());
Eclipse の Type Hierarchy で見ると、WSAddressingFeature の他に、LoggingFeature や WS-Policy などの AbstractFeature の派生クラスが12個ほどあるらしい。 今回は WS-Addressing の細部にまで深入りできないけど、何となく CXF の作法がわかってきた。beans.xml で Feature を付与するようにして、サービス実装クラス(ビジネスロジック)への変更を拝するというのは、これも separation of concern の良い例だなあと思う。 他にも WS-* 関連の CXF 実装を調べて見たいが、思ったより資料が少なくて難しい。

Apache CXF 2.2.4/Tomcat v6.0/Eclipse 3.5

Apache CXF を、Eclipse の WTP から Tomcat v6.0 で動かしてみる。 ■ Eclipse の準備
  • SOA Tools Platform プラグインをインストールしておく。update site は "http://download.eclipse.org/releases/galileo"。
  • Servers ビューで、Tomcat v6.0 サーバを構成しておく
  • [Preference]から[Web Services]→[CXF 2.0 Preferences]の[CXF Runtime]タブで、CXF Home に CXF のインストールディレクトリを指定。
  • 続けて、[Server and Runtime] で Tomcat v6.0 とApache CXF 2.xを指定
■ プロジェクトと WSDL ファイルの作成
  • Dynamic Web Project を作成する。(ここでは プロジェクト名:cxf-trial1とした)
  • Project Facets で CXF 2.x Web Services が指定されていることを確認
  • 新規ソースフォルダ resources を作成する。
  • resources 下に greeting.wsdl を作成する。
    <?xml version="1.0" encoding="UTF-8"?>
    <wsdl:definitions name="greeting"
       targetNamespace="http://mycompany.com/greeting/"
       xmlns:tns="http://mycompany.com/greeting/"
       xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
       xmlns:xsd="http://www.w3.org/2001/XMLSchema"
       xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
       <wsdl:types>
          <xsd:schema targetNamespace="http://mycompany.com/greeting/">
             <xsd:element name="greet">
                <xsd:complexType>
                   <xsd:sequence>
                      <xsd:element name="username" type="xsd:string" />
                   </xsd:sequence>
                </xsd:complexType>
             </xsd:element>
             <xsd:element name="greeting-response">
                <xsd:complexType>
                   <xsd:sequence>
                      <xsd:element name="return" type="xsd:string" />
                   </xsd:sequence>
                </xsd:complexType>
             </xsd:element>
          </xsd:schema>
       </wsdl:types>
       <wsdl:message name="request">
          <wsdl:part name="parameters" element="tns:greet" />
       </wsdl:message>
       <wsdl:message name="response">
          <wsdl:part name="parameters" element="tns:greeting-response" />
       </wsdl:message>
       <wsdl:portType name="Greeting">
          <wsdl:operation name="greet">
             <wsdl:input message="tns:request"/>
             <wsdl:output message="tns:response"/>
          </wsdl:operation>
       </wsdl:portType>
       <wsdl:binding name="GreetingSoapBinding" type="tns:Greeting">
          <soap:binding style="document"
             transport="http://schemas.xmlsoap.org/soap/http" />
          <wsdl:operation name="greet">
             <wsdl:input>
                <soap:body use="literal" />
             </wsdl:input>
             <wsdl:output>
                <soap:body use="literal" />
             </wsdl:output>
          </wsdl:operation>
       </wsdl:binding>
       <wsdl:service name="GreetingService">
          <wsdl:port name="GreetingPort" binding="tns:GreetingSoapBinding">
             <soap:address location="http://localhost:8080/GreetingService" />
          </wsdl:port>
       </wsdl:service>
    </wsdl:definitions>
■ Web サービスのスケルトンの作成
  • greeting.wsdl のコンテキストメニューから新規 Web Service を作成(Web Service Runtime に、CXF 2.x が設定されている事を確認)
  • http://localhost:8080/cxf-trial/services/ で Greeting の WSDL へのリンクが表示され、これを辿ると WSDL が表示される事を確認。
  • Web Service Explorer から greet を実行してみると、与えた文字列に関係なく、自動生成コードにより固定文字列(_return-1938145346みたいな)が返される事を確認。
■ Web サービスのスケルトンの実装 GreetingImpl.greet()を書き換える
   public java.lang.String greet(java.lang.String username) { 
      return String.format("Hello, %s!", username);
   }
WTP なので、変更したら勝手に 再発行される。Web Service Explorer で 文字列 "World" を与えて greet を実行してみると、"Hello, World!" が返される事を確認。 これで、他の CXF の機能(WS-Security とか)を試す準備ができた。

2009年11月13日金曜日

Ganymede/Geronimo 2.1.4

Geronimo 2.1.4 と Eclipse の組み合わせを試してみた。

当初、Galileo (Eclipse 3.5)でやってみたものの、下記リンクの質疑応答のとおり、Geronimo 2.1.4 と Galileo は適合しないらしく、アダプタをインストールできない。
[Problem with Eclipse Galileo]

で、Galileo は断念。Ganymede は上手くいった。

以下のようにアダプタをインストール。
  • [Windows]->[Preference]
  • [Server]->[Runtime Environment]->[Add...]
  • [Download additional server adopters]
  • リストアップされる Geronimo v2.1 Server Adapterを選択。

動作確認は、定番の Snoopサーブレット

Geronimo は余り使われていないようだけど、たまに Geronimo 前提の技術解説とかあるから、やっぱりいつでも使えるように用意しておきたかった。

Drools でハノイの塔

Drools で再帰を試したくてハノイの塔(Tower of Hanoi)を書いてみた。思ったよりだいぶスッキリと書ける。
package com.sample
 
import com.sample.HanoiTest.Tower;
import com.sample.HanoiTest.Action;
 
rule "move one disk"
   when
      a: Action($height: height, 
         $from: fromTower, $to: toTower, 
         height==1)
   then
      $from.moveOneDisk($to);
end

rule "move disks"
   when
      a: Action($height: height, 
         $from: fromTower, $to: toTower, $other: otherTower, 
         height>1)
   then
      insert(new Action($height - 1, $other, $to, $from));
      insert(new Action(1, $from, $to, $other));
      insert(new Action($height - 1, $from, $other, $to));
      retract(a);
end
package com.sample;

import 略

public class HanoiTest {
   public static final void main(String[] args) throws Exception {

      KnowledgeBase kbase = readKnowledgeBase();
      StatefulKnowledgeSession ksession = kbase.newStatefulKnowledgeSession();

      final Tower t1 = new Tower(1, "A", "B", "C", "D");
      final Tower t2 = new Tower(2);
      final Tower t3 = new Tower(3);

      ksession.insert(t1);
      ksession.insert(t2);
      ksession.insert(t3);
      ksession.insert(new Action(4, t1, t3, t2));

      ksession.addEventListener(new DefaultAgendaEventListener() {
         public void afterActivationFired(AfterActivationFiredEvent event) {
            String ruleName = event.getActivation().getRule().getName();
            if ("move one disk".equals(ruleName)) {
               System.out.printf("%-15s%-15s%-15s%n", t1, t2, t3);
            }
         }
      });
      System.out.printf("%-15s%-15s%-15s%n", t1, t2, t3);
      ksession.fireAllRules();
   }
   
   private static KnowledgeBase readKnowledgeBase() ・・・ 略
   
   public static class Tower {
      private final int number;
      private final Stack<String> disks = new Stack<String>();
      public Tower(int number, String...disks) {
         this.number = number;
         this.disks.addAll(Arrays.asList(disks));
      }
      public int getNumber() {
         return number;
      }
      public String toString() {
         return String.format("塔%d%s", number, disks.toString());
      }
      public void moveOneDisk(Tower to) {
         to.disks.push(disks.pop());
      }
   }
   public static class Action {
      private int height;
      private Tower fromTower;
      private Tower toTower;
      private Tower otherTower;
      コンストラクタとgetter/setter 略
   }
}
予定通りの出力になる。
塔1[A, B, C, D] 塔2[]           塔3[]           
塔1[A, B, C]    塔2[D]          塔3[]           
塔1[A, B]       塔2[D]          塔3[C]          
塔1[A, B]       塔2[]           塔3[C, D]       
塔1[A]          塔2[B]          塔3[C, D]       
塔1[A, D]       塔2[B]          塔3[C]          
塔1[A, D]       塔2[B, C]       塔3[]           
塔1[A]          塔2[B, C, D]    塔3[]           
塔1[]           塔2[B, C, D]    塔3[A]          
塔1[]           塔2[B, C]       塔3[A, D]       
塔1[C]          塔2[B]          塔3[A, D]       
塔1[C, D]       塔2[B]          塔3[A]          
塔1[C, D]       塔2[]           塔3[A, B]       
塔1[C]          塔2[D]          塔3[A, B]       
塔1[]           塔2[D]          塔3[A, B, C]    
塔1[]           塔2[]           塔3[A, B, C, D] 

2009年11月12日木曜日

暗記/SOA manifesto/Agile manifesto

SOA マニフェスト というものが公開されたらしい。

Agile マニフェスト とそっくり。

どちらも「左辺 over 右辺」形式のいくつかの条項でできていて、「右辺の価値を否定するものではないが、それよりも左辺の方をこそ重視する」といったスタンスが表明されている。

まだちょっと固まりきっていないというか、今後も変更がありそうな気配もあるけど、まあ6個だし覚えてしまおう。Agile マニフェストも含めるとちょうど10個でキリがいいからこれも含めてしまう。

暗記というと余り良いイメージがないし自分も昔は嫌いだったけど、最近は暗記できるものは暗記してしまうようにしている。当たり前だけど、情報が脳内に格納されている方が思考の効率が良い事が多い。脳だけあればいいわけだから便利だし。

例えば、この SOA マニフェストなんかも、どうやったら各条項を他人に説明できるかなんて事を、満員電車の中でも考えたりできる。というわけで10問の暗記カードを作ってみた。
SOA / Agile Manifesto
{ "height":160,"width":400,"opacity":0.9,"answer-bg-color":"#FFEEDD","question-bg-color":"#FFEEDD","answer-color":"#006622","question-color":"#002266","caption":"暗記帳 アルファ版"}
??? over processes and tools
<Agile manifesto>
Individuals and interactions
over
processes and tools
??? over comprehensive documentation
<Agile manifesto>
Working software
over
comprehensive documentation
??? over contract negotiation
<Agile manifesto>
Customer collaboration
over
contract negotiation
??? over following a plan
<Agile manifesto>
Responding to change
over
following a plan
??? over technical strategy
<SOA manifesto>
Business value
over
technical strategy
??? over project-specific benefits
<SOA manifesto>
Strategic goals
over
project-specific benefits
??? over custom integration
<SOA manifesto>
Intrinsic interoperability
over
custom integration
??? over specific-purpose implementations
<SOA manifesto>
Shared services
over
specific-purpose implementations
??? over optimization
<SOA manifesto>
Flexibility
over
optimization
??? over pursuit of initial perfection
<SOA manifesto>
Evolutionary refinement
over
pursuit of initial perfection

JSF 2.0/h:datatable/Paginator

h:datatable の ページ切り替えを考えてみた。 基本的には、以下のような Paginator になるんじゃないだろうか。
package study.jsf.pagination;

import java.util.List;

public abstract class Paginator<T> {
   private final int size; 
   private int page = 0;

   public Paginator(int size) {
      this.size = size;
   }
   public abstract List<T> currentElements();

   public abstract int itemsCount();
   
   public int start() {
      return page * size; 
   }
   public int size() { 
      return size; 
   }
   public boolean isHasNextPage() {
      return (page + 1) * size() + 1 <= itemsCount();
   }
   public void nextPage() {
      if (isHasNextPage()) page++;
   }
   public boolean isHasPreviousPage() {
      return page > 0;
   }
   public void previousPage() {
      if (isHasPreviousPage()) page--;
   }
   public int getPageSize() { 
      return size(); 
   }
}
ManagedBean など、これを使う側で itemsCount()と currentElements()メソッドを実装し、型パラメータに具体型を与える。適当すぎるサンプルだが、例えば以下のように書く。
package study.jsf.pagination;

import 略

@ManagedBean(name="memberList")
@SessionScoped
public class PersonListBean {
   private static final List MEMBERS_LIST = Arrays.asList(
      new Person(1, "Wilhelm"),
      new Person(2, "Hendrik"),   
      new Person(3, "Pieter"),   
      new Person(4, "Antoine"),
      new Person(5, "Pierre"),
      new Person(6, "Marie"),
      new Person(7, "John"),
      new Person(8, "Philipp")   
   ); 
   private Paginator paginator = new Paginator(3) {
      @Override
      public List currentElements() {
         return MEMBERS_LIST.subList(start(), 
               Math.min(start() + size(), itemsCount()));
      }
      @Override
      public int itemsCount() {
         return MEMBERS_LIST.size();
      }
   };
   public Paginator getPaginator() {
      return paginator;
   }
   public List getMembers() {
      return paginator.currentElements();
   }
   public void next() {
      paginator.nextPage();
   }
   public void previous() {
      paginator.previousPage();
   }
}
faces ファイルはこんな感じになる
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
   xmlns:f="http://java.sun.com/jsf/core"
   xmlns:h="http://java.sun.com/jsf/html">
<h:head>
   <title>ajax-dataTable</title>
</h:head>
<h:body>
  <h:form>
   <h:dataTable 
      id="partyTable" 
      value="#{memberList.members}"
      var="member">
     <h:column>#{member.id}</h:column>
     <h:column>#{member.name}</h:column>
   </h:dataTable>
   <h:commandLink id="prevLink" action="#{memberList.previous}"
      value="前 #{memberList.paginator.pageSize}件"
      rendered="#{memberList.paginator.hasPreviousPage}">
      <f:ajax execute="@this" render="@form"/>
   </h:commandLink>
   &#160; 
   <h:commandLink
      id="nextLink" action="#{memberList.next}"
      value="次#{memberList.paginator.pageSize}件"
      rendered="#{memberList.paginator.hasNextPage}">
      <f:ajax execute="@this" render="@form"/>
   </h:commandLink>
  </h:form>
</h:body>
</html>
実行すると、Person オブジェクトから 3つずつ(3ページ目は2つ)がテーブルに表示され、また 3ページ目以外では「次3件」リンクが、1ページ目以外では「前3件」リンクが表示される。 あと考える事は以下のような感じか
  • currentElements() は List<T>じゃなくて、DataModel<T>を返す方がいいかもしれない。
  • 呼び出し側では無駄に currentElements()を呼ばずにすむような仕組みを考える必要があるかも

JSF 2.0/JCR 2.0/JSR-250/JNDI

JSF 2.0 と JCR 2.0(Jackrabbit)を併用してみる。 JDBC の DataSource に JNDI 経由でアクセスするのと同じやり方で、JCR の Repository を扱いたい。ただし ManagedBean が Repository にアクセスするときには、JNDI は使わずに、標準アノテーション(JSR-250)によりインジェクトされたリソースとして扱いたい。 ■ 前提
  • プロジェクトディレクトリ直下に、既に Jackrabbit の repository.xml と repository フォルダがある。
  • このリポジトリの /hello/world@message に、文字列"Hello, World!"が格納されている。(前ポスト参照)
■ プロジェクトを作る
  • 適当に Dynamic Web Project を作って Maven 管理にする(例えばこんなやり方)。(Maven プロジェクトを作って Dynamic Web Project ファセットを与えるやり方でも可。)プロジェクト名はここでは "jcr-tomcat" とした。
  • pom.xml の dependency に 以下を指定。
    groupIdartifactIdversion
    javax.jcrjcr2.0
    org.apache.jackrabbitjackrabbit-core2.0-beta1
    org.slf4jslf4j-log4j121.5.5
    com.sun.facesjsf-api2.0.2-b02
    com.sun.facesjsf-impl2.0.2-b02
  • META-INF 下に、Tomcat の context.xmlを書く
    <?xml version="1.0" encoding="UTF-8"?>
    <Context>
       <Resource name="jcr/repository" auth="Container" type="javax.jcr.Repository"
          factory="org.apache.jackrabbit.core.jndi.BindableRepositoryFactory"
          configFilePath="C:/devel/ws/java/eclipse/standards1/jcr-tomcat/repository.xml" 
          repHomeDir="C:/devel/ws/java/eclipse/standards1/jcr-tomcat/repository" />
    </Context>
  • お決まりのファイルを用意する。
    • log4j.propertiesを適当に書く
    • web.xml に faces 関連のサーブレット定義を追加。
■ コードを書く 以下のような ManagedBean を書いてみる。
package mypackage;

import 略

@ManagedBean
public class JcrBean {
   private static final String USERNAME = "username";
   private static final char[] PASSWORD = "password".toCharArray();

   @Resource(name="jcr/repository", type=Repository.class)
   private Repository repository;
   
   public String getGreet() {
      Session session = null;
      try {
         session = repository.login(
                   new SimpleCredentials(USERNAME, PASSWORD));
         Node node = session.getNode("/hello/world");
         return node.getProperty("message").getString();
      } catch (RepositoryException e) {
         return e.getMessage();
      } finally {
         if (null != session) session.logout();
      }
   }
}
この管理 bean を使う jcrtext.xhtml を書く。
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
   xmlns:f="http://java.sun.com/jsf/core"
   xmlns:h="http://java.sun.com/jsf/html">
   <h:body>#{jcrBean.greet}</h:body>
</html>
■ 確認
  • ブラウザでhttp://localhost:8080/jcr-tomcat/jcrtest.faces を見てみる
  • "Hello, World!"が表示されたら JSF から Jackrabbit まで疎通OK
■ 後記
  • ここでやった JNDI のセッティングは、アプリ毎に別々のリポジトリを使うやり方だけど、共有するとか、他にも構成のパターンがある。(参考URL
  • 試行錯誤しながらやってると、いろんなタイミングでしょっちゅう.lock ファイルが残って面倒くさい。本番で使うときは、なんか考えておいた方がいいかも。

2009年11月11日水曜日

JCR 2.0/Jackrabbit 2.0/galileo

10/30 にリリースされた Apache Jackrabbit 2.0 beta1 を使ってみる。 JCR (Content repository API for Java) は、ツリー状に構成されたデータを保管するAPI。データ構造の雰囲気は Windows のレジストリとそっくりだけど、XML のようなドキュメントも直に扱えて XPath 式なんかも普通に使える。データの読み書きに加えてバージョニングやトランザクションも Java 標準として提供している。 (個人的には、例えばログインユーザごとに画面や操作をカスタマイズできるようなWebアプリで、カスタマイズ情報を管理する仕組みなんかに利用してみたい。よく RDB 前提で一からテーブル設計したりするけど、ちょっといかがなものかと思ったりする。) Jackrabbit はその実装で、最新の正式リリースは 8月にリリースされた1.6だけど、開発中の2.0のベータ版も先月末にリリースされたらしい。これを Eclipse 環境で試してみる。 とりあえず JCR 1.0/Jackrabbit1.6用に書かれたチュートリアル「First Hops」の"Hop 2"を元ネタにして、最新プロダクトを動かしてみる。 環境は Eclipse 3.5(Galileo)、m2eclipse(Maven 2.2.1)、JUnitプラグイン(JUnit 4)。 ■ プロジェクト作成
  • Eclipse で Maven Project を作る。アーキタイプは maven-archetype-quickstart。
  • [Add Libraries...] で JUnit 4を追加する。
  • pom.xml の dependencies を以下のようにする。
    <dependencies>
      <dependency>
        <groupId>javax.jcr</groupId>
        <artifactId>jcr</artifactId>
        <version>2.0</version>
      </dependency>
      <dependency>
        <groupId>org.apache.jackrabbit</groupId>
        <artifactId>jackrabbit-core</artifactId>
        <version>2.0-beta1</version>
      </dependency>
      <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-log4j12</artifactId>
        <version>1.5.5</version>
      </dependency>
    </dependencies>
    slf4j-log4j12 バージョンは1.5.5 が限界で、1.5.6 以上になると実行時に例外が発生する。org.slf4j.impl.StaticLoggerBinder クラスの"SINGLETON"フィールドのvisibility が、public からprivate になったため
■ 動かしてみる JUnitは、ここでは単に共通の前処理・後処理つきのエントリポイントとして利用する。正しい作法ではないけど、気にしない。 で、以下のようなテストコードを書いて、テストメソッドを一個ずつ実行してみる。
package xad.yasuabe.studies.standards1.jcr_study1;

import javax.jcr.Node;
import javax.jcr.PathNotFoundException;
import javax.jcr.Repository;
import javax.jcr.Session;
import javax.jcr.SimpleCredentials;

import org.apache.jackrabbit.core.TransientRepository;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.*;

public class AppTest {
   private static final String USERNAME = "username";
   private static final char[] PASSWORD = "password".toCharArray();

   private Session session;
   private Node root; 
      
   @Before
   public void setup() throws Exception {
        Repository repository = new TransientRepository();
        session = repository.login(
                new SimpleCredentials(USERNAME, PASSWORD));
        root = session.getRootNode();
   }
   
   @After
    public void tearDown() {   
        session.logout();
   }

   @Test
   public void storeContent() throws Exception {
        Node hello = root.addNode("hello");
        Node world = hello.addNode("world");
        world.setProperty("message", "Hello, World!");

        session.save();
   }
   @Test
   public void retrieveContent() throws Exception {
        Node node = root.getNode("hello/world");

        assertEquals("/hello/world", node.getPath());
        assertEquals("Hello, World!", 
              node.getProperty("message").getString());
   }
   @Test
   public void removeContent() throws Exception {
        root.getNode("hello").remove();
        session.save();
   }
   @Test
   public void verifyContentRemoved() throws Exception {
      boolean failed = false;
      try {
         assertNull(root.getNode("hello"));
      } catch (PathNotFoundException e) {
         failed = true;
      }
      assertTrue(failed);
   }
}
※ メソッドごとに個別に実行するやり方は、Outlineビューから実行したいメソッドの右クリから[Run As]->[JUnit Test]。
StoreContent()
ルート直下に hello、その下に world というノードを作って、message という名前のプロパティに値"Hello, World!"を設定する。
retrieveContent()
ルート下で、xpath式 "hello/world"で表されるノードを得る。フルパスとプロパティ"message"の値を確認する。
removeContent()
ルート直下のノード"hello"を削除する。
verifyContentRemoved()
ルート直下でノード"hello"の取得を試みる。例外が出るのを確認する。
実行してみると、期待通りに動くのがわかる。 また、Eclipse の Package explorer か Navigator から、プロジェクトの右クリで [Refresh] を実行するとツリーが更新され、プロジェクトディレクトリ直下に repository フォルダとrepository.xml ファイルが生成されているのが分かる。repository 直下には.lock ファイルがあって、session が生きている間、排他してるらしい。 今回は JCR 1.0(JSR 170) レベルの事しかやっていないが、Jackrabbit 2.0 から JCR 2.0 API(JSR 283) がフルサポートされたので、次はその辺りを調べてみたい。

jBPM 4.1/workflowpattern/Discriminator

jBPM では、Discriminator パターンが直接サポートされていないので、ちょっと試作してみた。 Discriminator は Join の一種で、パラレルに分岐した実行経路のうちで、最初に 合流点に到達したものをすぐに後ろに通して、後から来たものは無視するワークフローパターンで、一言で言うと「早い者勝ち」といった感じ。 これを org.jbpm.jpdl.internal.activity.JoinActivity を参考にして、<custom> アクティビティで実装してみる。
  ┌→ activityA →→┐
  │         ↓
○→┼→ activityB → discriminator→activityD→ activityE
  │         ↑
  └→ activityC →→┘
上ののようなフローを定義して activityA を 実行すると、フローの先端が discriminator を通過して activityD に到達する。これが、ただの <join> だと、activityB,C が実行されるまで activityD には行かない。 続いてactivityB を実行すると、discriminatorに届いたところで経路が終わる(エラーにはならない)。ここで、discriminator を使わず、単に activityDに activityA,B,Cをつないでいるだけだと、activityD 上で AからのものとBからのものとで、実行経路が重複する事になる。 以下のようなコードでやってみた。 ※試作なので Session.lock() とか、プロセス定義の正当性チェックは、ここでは省略した。
package test.trial;

import static org.jbpm.api.Execution.STATE_ACTIVE_ROOT;
import java.util.List;
import java.util.Map;
import org.jbpm.api.activity.ActivityExecution;
import org.jbpm.api.activity.ExternalActivityBehaviour;
import org.jbpm.pvm.internal.model.Activity;
import org.jbpm.pvm.internal.model.ExecutionImpl;
import org.jbpm.pvm.internal.model.Transition;

public class Discriminator implements ExternalActivityBehaviour {
   private static final long serialVersionUID = 1L;
   private static final String IS_SUCCEEDING = "__IS_SUCCEEDING__";

   @Override
   public void signal(ActivityExecution execution, String signalName,
         Map<String, ?> parameters) throws Exception {
      assert false;
   }

   @Override
   public void execute(ActivityExecution e) throws Exception {
      assert e instanceof ExecutionImpl; 
      execute((ExecutionImpl)e);
   }

   private void execute(ExecutionImpl exec) throws Exception {
      if (Boolean.TRUE.equals(exec.getVariable(IS_SUCCEEDING))) {
         exec.end();
         return;
      }
      exec.waitForSignal();

      ExecutionImpl root = exec.getParent();
      Activity activity = exec.getActivity();

      markSucceedingExecution(root, activity);

      root.setState(STATE_ACTIVE_ROOT);
      exec.setActivity(activity, root);

      Transition transition = activity.getDefaultOutgoingTransition();
      root.take(transition);
   }

   private void markSucceedingExecution(
         ExecutionImpl root, Activity activity) {
      for (ExecutionImpl concurrent : root.getExecutions()) {
         if (concurrent.getActivity() == activity) continue;
         concurrent.setVariable(IS_SUCCEEDING, true);
      }
   }
}
開始直後、activityA 実行直後、activityB 実行直後、activityD 実行直後の各段階で、それぞれのアクティビティ上の Execution の様子を調べてみたが、想定している部分についてはほぼ期待通り。(JUnit4 と mysql コンソールを使って、<task> と <state> で同じ事を試行。) ただし、上記コードだけだと、プロセス全体の終了(<end>到達)後にも activityC が未終了のまま残ってしまうらしいので、何か工夫が必要かも。 まとめると、本番で使うには以下の追加作業が必要になるか
  • 排他制御
  • プロセス定義のチェック
  • 取り残されたアクティビティをどうするかの決めと実装
これを応用したら、N-out-of-M Join (Partial Join)パターンとか、BPMNの Complex Gatewayなんかも、jBPM で 実装できそうな気がする。

2009年11月9日月曜日

JSF2.0/jBPM4.1/Maven2/WTP/Galileo

JSF2.0 と JBoss jBPM の併用。

Eclipse 3.5 上 で、WTP と m2eclipse を使ってやってみる。試しに、jBPM のプロセス定義をJSF 2.0の表で表示してみる。

■ 使用プロダクト

  • Eclipse 3.5 Galileo
  • Maven 2.2.1
  • WTP 3.1.1
  • MySQL 5.0.45
  • JSF 2.0.1
  • jBPM 4.1

■ プロジェクトの作成

  • 名前:jsf-jbpm2 で Dynamic Web Projectを作成。
  • プロジェクトのコンテキストメニューから [Maven]->[Enable Dependency Management]を実行
  • pom.xml の dependencies に以下を追記
    <dependency>
       <groupId>mysql</groupId>
       <artifactId>mysql-connector-java</artifactId>
       <version>5.1.10</version>
    </dependency>
    <dependency>
       <groupId>org.jbpm.jbpm4</groupId>
       <artifactId>jbpm-jboss4</artifactId>
       <version>4.1</version>
       <exclusions>
          <exclusion>
             <groupId>juel</groupId>
             <artifactId>juel</artifactId>
          </exclusion>
       </exclusions>
    </dependency>
    <dependency>
       <groupId>javax.faces</groupId>
       <artifactId>jsf-api</artifactId>
       <version>2.0.1-FCS</version>
    </dependency>
    <dependency>
       <groupId>javax.faces</groupId>
       <artifactId>jsf-impl</artifactId>
       <version>2.0.1-FCS</version>
    </dependency>
  • もし、なんかの jar が 無いとか言われたら repositories に、例えば以下の URL とかを追加するといいかもしれない。ダメかもしれない。 http://repository.jboss.com/maven2/
  • プロジェクトの Properties の[Java EE Module Dependencies] で、Maven Dependencies にチェックを入れる

■ JSF 2.0 の確認

  • Servers ビューで jsf-jbpm2 プロジェクトを追加する
  • WebContent 直下に以下の test.xhtml を置く
    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
            "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
    <html xmlns="http://www.w3.org/1999/xhtml"
       xmlns:h="http://java.sun.com/jsf/html">
    <h:body>
       <h:outputLabel for="text1">
          <h:outputText id="text1" value="Hello, World!" />
       </h:outputLabel>
    </h:body>
    </html>
    
  • Servers ビューから Tomcat v6.0 を起動する
  • 適当なブラウザで、http://localhost:8080/jsf-jbpm2/test.faces を見てみる
  • ”Hello, World!”が表示されて、ソースにも怪しいところが無ければ成功。
■ jBPMの確認

※ 以下、すでにプロセス定義がいくつか登録されている前提

  • ソースフォルダ resources を新規作成する
  • resources 下で、以下のファイルを作成する
    • jbpm.cfg.xml
      <?xml version="1.0" encoding="UTF-8"?>
      <jbpm-configuration>
         <import resource="jbpm.default.cfg.xml" />
         <import resource="jbpm.jpdl.cfg.xml" />
         <import resource="jbpm.tx.hibernate.cfg.xml" />
         <import resource="jbpm.identity.cfg.xml" />
      </jbpm-configuration>
      
    • jbpm.hibernate.cfg.xml (MySQL サービスが立ち上がっている前提。データベースは適当でOK。)
      <?xml version="1.0" encoding="utf-8"?>
      
      <!DOCTYPE hibernate-configuration PUBLIC
                "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
                "http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">
      
      <hibernate-configuration>
        <session-factory>
          <property name="hibernate.dialect">org.hibernate.dialect.MySQLInnoDBDialect</property>
          <property name="hibernate.connection.driver_class">com.mysql.jdbc.Driver</property>
          <property name="hibernate.connection.url">jdbc:mysql://localhost/jbpm</property>
          <property name="hibernate.connection.username"></property>
          <property name="hibernate.connection.password"></property>
          <property name="hibernate.hbm2ddl.auto">verify</property>
          <property name="hibernate.format_sql">true</property>
      
          <mapping resource="jbpm.repository.hbm.xml" />
          <mapping resource="jbpm.execution.hbm.xml" />
          <mapping resource="jbpm.history.hbm.xml" />
          <mapping resource="jbpm.task.hbm.xml" />
          <mapping resource="jbpm.identity.hbm.xml" />
      
        </session-factory>
      </hibernate-configuration>
      
    • log4j.properties を適当に書く
      log4j.rootLogger=DEBUG, CONSOLE
      log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
      log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout
      log4j.appender.CONSOLE.layout.ConversionPattern=%d{HH:mm:ss,SSS} %-5p %C{1} : %m%n
      log4j.logger.org.jbpm=DEBUG
      log4j.logger.org.hibernate=INFO
      
  • ソースフォルダ src を新規作成する
  • 以下のクラスを src 下に作成する
    package mypackage;
    
    import 略
    
    @ManagedBean(name="definitionList")
    public class ProcessDefinitions {
       public List<ProcessDefinition> getItems() {
          ProcessEngine engine = Configuration.getProcessEngine();
          RepositoryService rs = engine.get(RepositoryService.class);
    
          return rs.createProcessDefinitionQuery().list();
       }
    }
  • resources 下に以下のようにmessages.properties を作成する
    idLabel=ID
    keyLabel=KEY
    nameLabel=NAME
    versionLabel=VERSION
    
  • WebContent 下に、以下のようにprocessDefinitions.xhtmlを作成する。
    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
            "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
    <html xmlns="http://www.w3.org/1999/xhtml"
       xmlns:h="http://java.sun.com/jsf/html"
       xmlns:f="http://java.sun.com/jsf/core">
    <h:head>
       <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
       <title>rules</title>
       <style>
       .list-background{border-collapse:collapse;border:1px solid grey;}
       .list-header{background-color:Plum;border:1px solid grey;width:96px;}
       .col-center{border-right:1px solid grey;}
       .row-odd{background-color:moccasin;}
       .row-even{background-color:mistyRose;}
       </style>
    </h:head>
    <h:body>
       <f:loadBundle basename="messages" var="msg" />
       <h:dataTable id="processDefinitionList"
          columnClasses="col-center,col-center,col-center"
          headerClass="list-header" rowClasses="row-odd,row-even"
          styleClass="list-background" value="#{definitionList.items}" 
          var="definition"
          cellpadding="0"
          cellspacing="0">
          <h:column>
             <f:facet name="header">#{msg.idLabel}</f:facet>
             #{definition.id}
          </h:column>
          <h:column>
             <f:facet name="header">#{msg.keyLabel}</f:facet>
             #{definition.key}
          </h:column>
          <h:column>
             <f:facet name="header">#{msg.nameLabel}</f:facet>
             #{definition.name}
          </h:column>
          <h:column>
             <f:facet name="header">#{msg.versionLabel}</f:facet>
             #{definition.version}
          </h:column>
       </h:dataTable>
    </h:body>
    </html>
    

・以下のように内容が表示されれば、JSF と jBPM の両方がちゃんと使えてる事になる。

2009年11月8日日曜日

Glassfish v3 Java EE 6 + Eclipse 3.5 → NG

昔から Glassfish と Eclipse を併用しようとすると、必ず困った事が連発していたが、まただ。やっぱりだよ。

Servlet 3.0 を試したくて、Eclipse 3.5 で 「Glassfish v3 Java EE 6」の Server Runtime Environment を構成して、軽くサンプルコードを書いてみた。

まず Servers ビューから、コンテキストメニューの[Debug] を実行したはずなのに、どうしてもServers ビューの表示が [Debugging, Synchronized]にならない。管理コンソールでデバッグを有効にしても同じ。

リモートデバッグで接続しようとしても、"failed to connect to remote vm"なんて言ってくる。ググっても全然情報がない。仕方なく、Servers から起動してのデバッガ使用は諦める。

で動かしてみると、今度はビルド時に問題なく使えている Servlet 3.0の API が 認識されずにエラーが出てくる。具体的には HttpServletRequest.getPart() を使おうとしてNoSuchMethodException なんてのがあがってくる。Glassfish 以外は何一つ jar だのライブラリだのを使っていないのに、どこを見てるんだろう?

まあ、under construction らしいし、そもそも Java EE 6 も正式じゃないし、仕方ないか・・・

NetBeans なら使えそうな気がしないでもないが、Eclipse と比べると使い勝手悪すぎて、あれを使うくらいなら javac と ant の方がマシな気がしないでもない。

今のところ Java EE 6 を試そうとしたら GlassFish をコマンドプロンプトから立ち上げて、後から Eclipse で接続するってやり方しかないか。この場合、修正する度、いちいち war に丸めてデプロイし直すのが面倒くさい。まあ Ant を使えばちょっとは手間を減らせるけど、Eclipse + WTP + Tomcat に慣れてしまった今では、ちょっと気が滅入る。今日も愚痴になってしまった・・・

JSF 2.0/tomahawk/失敗

JSF2.0 で Tomahawk を使おうとして諦めたレポート。

■ 作業 A
  1. JSF2.0 を使った簡単な Webアプリを用意する。
  2. Tomcat v6.0で正常動作を確認。
  3. pom.xml で tomahawk 1.1.9を指定する。
  4. 再デプロイを試みる。
  5. 以下の例外発生で、デプロイ失敗。
      java.lang.NoClassDefFoundError: com/sun/facelets/tag/jsf/ComponentHandler


■ 作業 B
  • FACELETを使わないので、以下のパラメータを web.xml で指定して再デプロイ
    <context-param>
    <param-name>javax.faces.DISABLE_FACELET_JSF_VIEWHANDLER</param-name>
    <param-value>true</param-value>
    </context-param>
  • 5)の例外は出ずにデプロイは成功する。
  • 2)と同じことをやって動作確認失敗。以下のような例外。
      java.lang.IllegalStateException: レスポンスをコミットした後でフォワードできません


■ 作業 C
  • 作業 Bの変更を戻す
  • WEB-INF/faces-config.xml を作成。
  • 5)の例外は出ずにデプロイは成功するが、作業 Bと同じ結果。


上記作業と逆に、JSF 1.2 + Tomahawk のWebアプリを作って、それをJSF 2.0に移行しようとしても、結局、上と同じになる。

・・・もういい。諦めた。

2009年11月7日土曜日

workflow patterns/jBPM/Eclipse

jBPM の JPDL の ワークフローパターン への対応を試してみた。とりあえず、前回のポストの "Basic Control Flow Patterns"の 5つ。

やり方は、JUnit でプロセスの実行がパターンの定義どおりに進行している事を assert で確認する。JUnit で動かすための環境は、だいぶ前のポストに書いたとおりにプロジェクトを作って、あとはビルドパスに JUnit 4ライブラリを追加すれば、すぐテスト実行できる。
テストメソッド以外の部分は次のようになる。
package net.yasuabe.trial.jboss_tasks1;

import 省略

public class WorkflowPatternTest {
   private ProcessEngine engine = null;
   private ExecutionService es = null;
   private RepositoryService rs = null;
   @Before
   public void setUpEngine() {
      engine = Configuration.getProcessEngine();
      es = engine.getExecutionService();
      rs = engine.get(RepositoryService.class);
   }
   @After
   public void cleanJBPM4Tables() {
      Db.clean(engine);
   }
}
これに、各パターンごとのテストメソッドを追加していく。
■ Sequence パターン
○─S1─S2
  • フローを開始する。S1 に進んだが、S2 には届いていない事を確認
  • S1 からフローを進める。S1 から出てS2に入った事を確認。
@Test
public void sequencePattern() {
   String jpdl =
      " <process name=\"pat1\"> " +
      "   <start> "+
      "     <transition to=\"S1\" /> " +
      "   </start> "+
      "   <state name=\"S1\"> "+
      "      <transition to=\"S2\" /> " +
      "   </state> "+
      "   <state name=\"S2\"/> " +
      " </process> ";
   rs.createDeployment().addResourceFromString(
         "pat1.jpdl.xml", jpdl).deploy();
   
   ProcessInstance pi = es
         .startProcessInstanceByKey("pat1");
   assertNotNull(pi.findActiveExecutionIn("S1"));
   assertNull(pi.findActiveExecutionIn("S2"));
   
   String exec1Id = pi.findActiveExecutionIn("S1").getId();
   pi = es.signalExecutionById(exec1Id);

   assertNull(pi.findActiveExecutionIn("S1"));
   assertNotNull(pi.findActiveExecutionIn("S2"));
}
■ Parallel Split パターン
    ┌S2
○-S1-F1┤
    └S3
  • フローを開始する
  • 更にS1 から先に進める
  • S1 から出て、実行がS2 S3 に入っている事を確認
@Test
public void parallelSplitPattern() {
   String jpdl =
      " <process name=\"pat2\"> " +
      "   <start> "+
      "     <transition to=\"S1\" /> " +
      "   </start> "+
      "   <state name=\"S1\"> "+
      "      <transition to=\"F1\" /> " +
      "   </state> "+
      "   <fork name=\"F1\"> "+
      "      <transition to=\"S2\" /> " +
      "      <transition to=\"S3\" /> " +
      "   </fork> "+
      "   <state name=\"S2\"/> " +
      "   <state name=\"S3\"/> " +
      " </process> ";
   rs.createDeployment().addResourceFromString(
         "pat2.jpdl.xml", jpdl).deploy();
   
   ProcessInstance pi = es
         .startProcessInstanceByKey("pat2");
   Execution exec1 = pi.findActiveExecutionIn("S1");
   pi = es.signalExecutionById(exec1.getId());

   assertNull(pi.findActiveExecutionIn("S1"));
   assertNotNull(pi.findActiveExecutionIn("S2"));
   assertNotNull(pi.findActiveExecutionIn("S3"));
}
■ Synchronization パターン
   ┌S1┐
○-F1┤ ├J1-S3
   └S2┘
  • フローを開始する
  • 並行してS1, S2 に入っている事を確認
  • S1 だけ進めて,それだけでは S3 には入らない事を確認
  • S2 も進めると、S3 に入った事を確認
@Test
public void synchronizationPattern() {
   String jpdl =
      " <process name=\"pat3\"> " +
      "   <start> "+
      "     <transition to=\"F1\" /> " +
      "   </start> "+
      "   <fork name=\"F1\"> "+
      "      <transition to=\"S1\" /> " +
      "      <transition to=\"S2\" /> " +
      "   </fork> "+
      "   <state name=\"S1\"> " +
      "      <transition to=\"J1\" /> " +
      "   </state> "+
      "   <state name=\"S2\"> " +
      "      <transition to=\"J1\" /> " +
      "   </state> "+
      "   <join name=\"J1\"> " +
      "      <transition to=\"S3\" /> " +
      "   </join> "+
      "   <state name=\"S3\"/> " +
      " </process> ";
   rs.createDeployment().addResourceFromString(
         "pat3.jpdl.xml", jpdl).deploy();
   
   ProcessInstance pi = es
         .startProcessInstanceByKey("pat3");
   assertNotNull(pi.findActiveExecutionIn("S1"));
   assertNotNull(pi.findActiveExecutionIn("S2"));
   assertNull(pi.findActiveExecutionIn("S3"));

   String exec1Id = pi.findActiveExecutionIn("S1").getId();
   pi = es.signalExecutionById(exec1Id);

   assertNull(findActiveExecutionIn("S1"));
   assertNotNull(findActiveExecutionIn("S2"));
   assertNull(pi.findActiveExecutionIn("S3"));

   String exec2Id = pi.findActiveExecutionIn("S2").getId();
   pi = es.signalExecutionById(exec2Id);

   assertNull(pi.findActiveExecutionIn("S1"));
   assertNull(pi.findActiveExecutionIn("S2"));
   assertNull(pi.findActiveExecutionIn("S3"));
}
■ Exclusive Choice パターン
   ┌S1 (color=black)
○-D1┤
   └S2 (color=white)
  • color=white の条件でフローを開始
  • S2 が選択されて実行に入っている事を確認
  • color=red の条件でフローを開始
  • 該当するパスがないので例外が出る事を確認
@Test
public void exclusiveChoicePattern() {
   String jpdl =
      " <process name=\"pat4\"> " +
      "   <start> "+
      "     <transition to=\"D1\" /> " +
      "   </start> "+
      "   <decision name=\"D1\" expr=\"#{color}\"> "+
      "      <transition name=\"black\" to=\"S1\" /> " +
      "      <transition name=\"white\" to=\"S2\" /> " +
      "   </decision> "+
      "   <state name=\"S1\"/> " +
      "   <state name=\"S2\"/> " +
      " </process> ";
   rs.createDeployment().addResourceFromString(
         "pat4.jpdl.xml", jpdl).deploy();
   
   Map<String, Object> v1 = new HashMap<String, Object>();
   v1.put("color", "white");
   ProcessInstance pi1 = es
         .startProcessInstanceByKey("pat4", v1);
   
   assertNull(pi1.findActiveExecutionIn("S1"));
   assertNotNull(pi1.findActiveExecutionIn("S2"));

   boolean exceptionThrown = false;
   try {
      Map<String, Object> v2 = new HashMap<String, Object>();
      v2.put("color", "red");
      es.startProcessInstanceByKey("pat4", v2);
   } catch (JbpmException ex) {
      exceptionThrown = true;
   }
   assertTrue(exceptionThrown);
}
■ Simple Merge パターン
   ┌S1 (color=white)┐
○-D1┤        ├S3
   └S2 (color=black)┘
  • color=white でフローを開始
  • S2 だけに進んでいる事を確認
  • S2 を進めると、S1に関係なく S3 に進んだ事を確認
@Test
public void simpleMergePattern() {
   String jpdl =
      " <process name=\"pat5\"> " +
      "   <start> "+
      "     <transition to=\"D1\" /> " +
      "   </start> "+
      "   <decision name=\"D1\" expr=\"#{color}\"> "+
      "      <transition name=\"black\" to=\"S1\" /> " +
      "      <transition name=\"white\" to=\"S2\" /> " +
      "   </decision> "+
      "   <state name=\"S1\"> " +
      "      <transition to=\"S3\" /> " +
      "   </state> "+
      "   <state name=\"S2\"> " +
      "      <transition to=\"S3\" /> " +
      "   </state> "+
      "   <state name=\"S3\"/> " +
      " </process> ";
   rs.createDeployment().addResourceFromString(
         "pat5.jpdl.xml", jpdl).deploy();
   
   Map<String, Object> v = new HashMap<String, Object>();
   v.put("color", "white");
   ProcessInstance pi = es
         .startProcessInstanceByKey("pat5", v);
   assertNull(pi.findActiveExecutionIn("S1"));
   assertNotNull(pi.findActiveExecutionIn("S2"));
   assertNull(pi.findActiveExecutionIn("S3"));

   String exec2Id = pi.findActiveExecutionIn("S2").getId();
   pi = es.signalExecutionById(exec2Id);

   assertNull(pi.findActiveExecutionIn("S2"));
   assertNotNull(pi.findActiveExecutionIn("S3"));
}