WicketのComponentにオブジェクトをインジェクトする
私はWicketというフレームワークが大好きです。
それからJavaEE6の新仕様であるCDIも大好きです。
だったらWicketの中でCDIを使いたくなるのは、ごく自然な流れ。
一応WicketとCDIを統合するライブラリもあるにはあるようですが、まともに動いたことがないんですよね。。。
そんなわけで今回はこれを自作してみました。
public class MyApplication extends WebApplication { @Override public Class<? extends Page> getHomePage() { return HomePage.class; } @Override protected void init() { try { final BeanManager beanManager = InitialContext.doLookup("java:comp/BeanManager"); getComponentInstantiationListeners().add(new IComponentInstantiationListener() { @Override public void onInstantiation(final Component pComponent) { inject(beanManager, pComponent); } }); } catch (final NamingException e) { throw new IllegalStateException(e); } } @SuppressWarnings({ "rawtypes", "unchecked" }) protected void inject(final BeanManager pBeanManager, final Component pComponent) { final Class pType = pComponent.getClass(); final Bean<Object> bean = (Bean<Object>) pBeanManager.resolve(pBeanManager.getBeans(pType)); final CreationalContext<Object> cc = pBeanManager.createCreationalContext(bean); final AnnotatedType<Object> at = pBeanManager.createAnnotatedType(pType); final InjectionTarget<Object> it = pBeanManager.createInjectionTarget(at); it.inject(pComponent, cc); } }
やってることは単純で、IComponentInstantiationListenerを使ってComponentがインスタンス化されるタイミングをフックして、そこでBeanManagerで明示的にインジェクトしてあげているだけです。
改善点はもちろんあります。JNDIが使えない環境では動かない、とか全てのComponentじゃなくてWebPageにだけDIすれば充分なんじゃない?とか。
でもアイデアの核は上記コードに凝縮されています。
CDIに勝ってる?
実はこの方法、本家CDIより機能的に上回っている部分があります。それはnewで生成したComponentにもDI可能なこと。
これはWicketのIComponentInstantiationListenerのおかげです。WicketのIComponentListenerListenerはCompnentクラスのコンストラクタの中から呼び出されるので、こんなことが可能になります。
以下がその部分のコードです。
public Component(final String id, final IModel<?> model) { setId(id); getApplication().getComponentInstantiationListeners().onInstantiation(this); ...(以下省略) }
初めてこのコードを見たときは「なるほど!」と膝を打ってしまいました。
Wicketって、本当におもしろい。
eclipseのWTPでテストクラスをデプロイ対象から外す
StrutsのメッセージファイルをXMLにする
Strutsではメッセージをpropertiesファイルに記述しますが、これを変更してXMLで書けるようにする方法を紹介します。
仕様
今回のサンプルでは、メッセージファイルはクラスパス直下に置くことにします。
多言語化を考慮して、メッセージファイルの末尾にはロケールを付与するようにします。
(ファイル名の例)
message_ja.xml
XMLの形式はこんな感じで。
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <messages> <message key="k0"> <text>メッセージ0</text> </message> <message key="k1"> <text>メッセージ1</text> </message> <message key="k2"> <text>メッセージ2</text> </message> </messages>
見たまんま。
クラスを作る
MessageResourcesとMessageResourcesFactoryを拡張したクラスを作ります。
まずはMessageResourcesの拡張。長いですが省略なしで。なおXMLのパースにはJAXBを使っています。
/** * */ package aaa.bbb; import java.io.BufferedInputStream; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import javax.xml.bind.JAXB; import javax.xml.bind.annotation.XmlAttribute; import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlRootElement; import org.apache.struts.util.MessageResources; import org.apache.struts.util.MessageResourcesFactory; /** * @author jabaraster */ public class XmlMessageResources extends MessageResources { private static final long serialVersionUID = -1390664385723178825L; private final Map<Locale, Map<String, Message>> messages = new ConcurrentHashMap<Locale, Map<String, Message>>(); /** * @param pFactory * @param pConfig */ public XmlMessageResources(final MessageResourcesFactory pFactory, final String pConfig) { super(pFactory, pConfig); } /** * @see org.apache.struts.util.MessageResources#getMessage(java.util.Locale, java.lang.String) */ @Override public String getMessage(final Locale pLocale, final String pMessageKey) { Map<String, Message> m = this.messages.get(pLocale); if (m == null) { m = load(pLocale, this.config); this.messages.put(pLocale, m); } final Message message = m.get(pMessageKey); if (message == null) { return "*** no message found for '" + pMessageKey + "' ***"; } return message.getText(); } private static void appendIfNotNull(final StringBuilder pBuffer, final String pToken) { if (pToken != null && pToken.trim().length() > 0) { pBuffer.append("_" + pToken); } } private static Map<String, Message> load(final Locale pLocale, final String pConfig) { try { final String path = resolvePath(pLocale, pConfig); final URL resourceLocation = XmlMessageResources.class.getResource(path); if (resourceLocation == null) { throw new IllegalStateException("メッセージリソース " + path + " が見付かりませんでした."); } final InputStream in = new BufferedInputStream(resourceLocation.openStream()); final Messages messages = JAXB.unmarshal(in, Messages.class); final Map<String, Message> ret = new ConcurrentHashMap<String, XmlMessageResources.Message>(); for (final Message msg : messages.getList()) { ret.put(msg.getKey(), msg); } return ret; } catch (final IOException e) { throw new IllegalStateException(e); } } private static String resolvePath(final Locale pLocale, final String pKey) { final StringBuilder buffer = new StringBuilder(); buffer.append("/"); buffer.append(pKey); appendIfNotNull(buffer, pLocale.getLanguage()); appendIfNotNull(buffer, pLocale.getCountry()); appendIfNotNull(buffer, pLocale.getVariant()); buffer.append(".xml"); return new String(buffer); } /** * @author jabaraster */ @XmlRootElement private static class Message { private String key; private String text; /** * */ @SuppressWarnings("unused") public Message() { // } /** * @return the key */ @XmlAttribute public String getKey() { return this.key; } /** * @return the text */ public String getText() { return this.text; } /** * @param pKey the key to set */ @SuppressWarnings("unused") public void setKey(final String pKey) { this.key = pKey; } /** * @param pText the text to set */ @SuppressWarnings("unused") public void setText(final String pText) { this.text = pText; } } /** * @author jabaraster */ @XmlRootElement private static class Messages { private List<Message> list = new ArrayList<XmlMessageResources.Message>(); /** * @return the list */ @XmlElement(name = "message") public List<Message> getList() { return this.list; } /** * @param pList the list to set */ @SuppressWarnings("unused") public void setList(final List<Message> pList) { this.list = pList; } } }
次にMessageResourceFactoryの拡張。さっき作ったXmlMessageResourcesを返すようにするだけです。
import org.apache.struts.util.MessageResources; import org.apache.struts.util.MessageResourcesFactory; /** * @author jabaraster */ public class XmlMessageResourcesFactory extends MessageResourcesFactory { private static final long serialVersionUID = -5520240862924828575L; /** * @see org.apache.struts.util.MessageResourcesFactory#createResources(java.lang.String) */ @Override public MessageResources createResources(final String pConfig) { return new XmlMessageResources(this, pConfig); } }
sturts-config.xmlを編集
XmlMessageResourcesFactoryをstruts-config.xmlに設定します。
今回は2つメッセージファイルを設定する例を挙げます。
(修正前)
<message-resources parameter="application" factory="org.seasar.struts.util.S2PropertyMessageResourcesFactory"/> <message-resources parameter="label" key="label" factory="org.seasar.struts.util.S2PropertyMessageResourcesFactory"/>
(修正後)
<message-resources parameter="application" factory="aaa.bbb.XmlMessageResourcesFactory"/> <message-resources parameter="label" key="label" factory="aaa.bbb.XmlMessageResourcesFactory"/>
メッセージファイルを作る
application_ja.xml
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <messages> <message key="k0"> <text>メッセージ0</text> </message> <message key="k1"> <text>メッセージ1</text> </message> <message key="k2"> <text>メッセージ2</text> </message> </messages>
label_ja.xml
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <messages> <message key="k3"> <text>メッセージ3</text> </message> <message key="k4"> <text>メッセージ4</text> </message> <message key="k5"> <text>メッセージ5</text> </message> </messages>
使う
準備は完了です。
JSPでメッセージを表示してみます。
<%@ page pageEncoding="UTF-8" contentType="text/html; charset=UTF-8" %> <!DOCTYPE html> <html> <head> <title>XMLでメッセージ</title> <meta charset="UTF-8"> </head> <body style="line-height: 1.5em;"> <bean:message key="k0"/> <br/> <bean:message key="k3" bundle="label"/> </body> </html>
YesodでHTML/JavaScript/CSSを共通化する
Webアプリを作ってると、HTML/JavaScript/CSSを共通化したくなることがあります。
Yesodでは、これはWidgetって仕組みを使うようです。
なお今回出てくるコードは全て、scaffoldで生成されたソース群がある環境を前提にしています。
Widgetとは?
WidgetについてはYesod Bookに説明があります。
たぶん「hamlet/julius/luciusの3点セットを作ると再利用可能な物が出来る」というところだと思います。
hamletはHTMLのテンプレート、juliusはJavaScriptのテンプレート、luciusはCSSのテンプレートです。
英語は得意ではないので正しく理解出来ている自信はありませんが、大きくは外していないと思います。
試してみたところ、常に3点セットである必要はなくて、1つ、あるいは2つが欠けていてもいいようです。
Widget例・submitボタンにスタイルと動作を与えてみる
ではWidgetを作ってみます。
削除用のsubmitボタンに、特定のスタイルと動作を与えてみましょう。
class属性"delete"のsubmitボタンはこんな外観になるようにして、さらにこのボタンを押すと確認ダイアログが出るようにします。キャンセルを押すとサブミットをキャンセル、OKを押すとサブミット実行、というわけですね。
これを「submit」という名前のWidgetとして作ってみます。
今回の場合、HTMLは必要ないのでhamletファイルは作らないことにします。
まずはCSSのテンプレート「submit.lucius」を作ります。ファイルの置き場所はtemplatesディレクトリです。なおhamletファイル、juliusファイルもこのディレクトリに置きます。
input[type="submit"].delete { font-weight: bold; color: rgb(255, 0, 0); background-color: rgb(255, 180, 180); }
ごく普通のCSSです。
次にJavaScriptのテンプレート「submit.julius」を作ります。
$(function() { $("input[type=submit].delete").click(function() { return confirm("削除していい?") }); });
これもごく普通のJavaScriptなんですが、jQueryに依存しています。この依存はWidgetを使う側には意識させたくないです。
そこでWidgetを使うための関数を作って、それを通して使ってもらうようにします。この関数の中でjQueryへの依存を解決してしまうわけですね。
ここではWidgetsというモジュールを作って、そこにWidgetを使うための関数を定義することにします。
module Widgets where import Import submitWidget :: GGWidget Jabaraster (GHandler Jabaraster Jabaraster) () submitWidget = do addScriptRemote "https://ajax.googleapis.com/ajax/libs/jquery/1.6.2/jquery.min.js" $(widgetFile "submit")
ポイントは2つです。
- addScriptRemoteでjQueryへの依存を解決
- $(widgetFile "submit")でjuliusファイルとluciusファイルを読み込んでパース
これでWidgetが作れました。
Widgetを使う
作ったWidgetを使うのは簡単で、単にsubmitWidget関数を呼び出せばOKです。
一例を挙げます。
getRootR :: Handler RepHtml getRootR = do ((_, widget), enctype) <- runFormPost someForm defaultLayout $ do setTitle "title" submitWidget -- 作ったWidgetを使う $(widgetFile "homepage")
引数を取るWidget*1
引数を取るWidgetも作ってみましょう。例えば特定のid属性値に対して操作するjuliusは以下のように書けます。
$(function() { $("##{tagId}").click(function() { return confirm("削除していい?") }); });
#{tagId}の部分が引数の値で置き換えられます。このWidgetを使う関数は以下のように書けます。
submitWidget' :: Text -> GGWidget Jabaraster (GHandler Jabaraster Jabaraster) () submitWidget' tagId = do addScriptRemote "https://ajax.googleapis.com/ajax/libs/jquery/1.6.2/jquery.min.js" $(widgetFile "submit2")
引数が多くなる場合は引数オブジェクトみたいなものを導入してもいいでしょう。その場合juliusは以下のように書きます。
$(function() { $("##{submitTagId context}").click(function() { return confirm("削除していい?") }); });
juliusの中でHaskellの関数が呼び出せます。もちろんhamletの中やluciusの中でも呼び出せます。これはめちゃくちゃ強力です(強力故、使い方には気を付けないといけないと思いますが)。
Widgetを使うための関数と引数オブジェクトは以下のようになります。
data SubmitWidgetContext = SubmitWidgetContext { submitTagId :: Text , submitOther :: Text } submitWidget'' :: SubmitWidgetContext -> GGWidget Jabaraster (GHandler Jabaraster Jabaraster) () submitWidget'' context = do addScriptRemote "https://ajax.googleapis.com/ajax/libs/jquery/1.6.2/jquery.min.js" $(widgetFile "submit3")
今回はここまで。
なんか最近はYesodのエントリばかりで、いっそのことYesodをカテゴリにしてしまおうか、とか考えています。
MacでHerokuにYesodを(Herokuに公開編)
さていよいよYesodアプリをHerokuに公開します。
ただ、当然ながらHerokuにアカウントが必要なので、作業はそこからスタートです。
相変わらずUbuntu on VirtualBoxで作業します。
Heroku Toolbeltをインストール
このツールをインストールすると、herokuコマンドを通してHerokuのリモートサイトを操作出来るようになります。
このツールをUbuntu on VirtualBoxにインストールしないといけないのですが、詳しい手順をメモし忘れたの紹介出来ません・・・
結構面倒だったので、自分としてもメモを取っておかなかったことを後悔しています。絶対、将来また同じことをやろうとして同じところにはまると思う。
ちなみにMacの場合、Heroku Dev Center内のGetting Started with Herokuの「Step 2: Install the Heroku Toolbelt」からダウンロード出来ます。
Gitをインストール
Herokuは先のHeroku Toolbeltに加えてgitを使います。例えば実行ファイルのアップロードはgitで行います。
sudo apt-get install git-core
Macの場合は
sudo port install git-core
で。
gitは今回初めて触りましたが、便利ですね。Subversionから鞍替えしようと思います。
gitリポジトリとHerokuスタックの作成
Herokuに実行ファイルをアップロードするのはgitで行います。そのため、gitのローカルリポジトリを作っておく必要があります。
Yesodアプリのルートディレクトリで作業します。
まずはgitローカルリポジトリの作成
git init
次にHerokuにスタックを作成。
heroku create --stack cedar
これでgitのリモートにherokuのサイトが登録されます。
Yesodアプリのビルド
cabal install
Yesod0.9.4からは、-fproductionフラグは不要になっているので、このコマンド一発でビルド出来ます。
これでdist/build/first-heroku/に実行ファイルfirst-herokuが出来ます。
Procfileを移動して編集
deploy/ProcfileはHerokuにデプロイするときに必要なファイルで、ルートディレクトリに置いておく必要があります。
mv deploy/Procfile .
また、Yesod0.9.4でビルドすると作成される実行ファイルを起動するには、引数が1つ必要なのですが、scaffoldが生成してくれるProcfileにはこれが反映されていません。なのでProcfileを編集する必要があります。
編集前
web: ./dist/build/first-heroku/first-heroku -p $PORT
編集後
web: ./dist/build/first-heroku/first-heroku production -p $PORT
package.jsonを作成
よく分かりませんがpackage.jsonを作らないとHerokuで動かないらしいです。
echo '{ "name": "first-yesod", "version": "0.0.1", "dependencies": {} }' >> package.json
Herokuにアップロード
Herokuにアップロードするにはgitを使います。
まずはローカルリポジトリに今までに作ったファイルをコミット。
git add .
git commit -m "master"
さていよいよHerokuにアップロード!
ブランチを切った方がいい、と書いてあるサイトも多いしProcfileのコメントにもそう書いてありますが、Ubunto on VirtualBoxがそもそもアップロード専用なので、ここではブランチは切りません。
git push heroku master:master
試してみる
アップロードしたアプリを試してみましょう!
heroku open
おおおおおおおおおおおおおお!!!!
表示された!!!!
これはすごい!!!!
目に見えておかしいところもあるのですが、Yesodの領域で解決出来る気がします。
最大の難関と思われたビルド&デプロイをクリア出来たのは大きい!
今回のソース、一応githubにアップしておきます。
http://github.com/jabaraster/first-heroku
MacでHerokuにYesodを(Yesod導入・アプリビルド編)
前回cabal-installがインストール出来たので、いよいよYesodのインストール、更にアプリのビルドまで行ってみましょう!
相変わらずUbuntu on VirtualBoxで作業します。
yesodのインストール
cabal-installがインストール出来たので、Yesodはこれを使って楽にインストールしてしまいましょう。
参考ページは本家のGetting Started with Yesod。
sudo cabal update
sudo cabal install Cabal cabal-install yesod
アプリの作成
yesodコマンドでアプリの雛形を自動生成します。
雛形とは言いましたが、今回はHerokuにテストで載せる実行ファイルがほしいだけなので何も手を加えないです。
yesod init
アプリ名はfirst-herokuにしました。
もしyesodコマンドがPATHの通っていない場所にある場合はパスのが必要です。私の環境では必要でした。
~/.cabal/bin/yesod init
ビルドに必要な依存ライブラリのインストール
実はYesodで作ったアプリをビルドするには、まだライブラリが足りません。
cabal-installで楽しようと思っても、依存を解決出来ずにインストール出来ません。
仕方がないのでcabal-installに頼らず、HackageDBからダウンロードしてきてインストールします。
インストールしたライブラリは以下。入れ子は依存関係を示しています。
- language-javascript-0.4.5
- utf8-light-0.4.0.1
- happy-1.18.8
- alex-3.0.1
- QuickCheck-2.4.2
下の方からインストールするとうまくいきます。
アプリのビルド
いよいよビルドです。
cd first-heroku
sudo cabal install
ここでのcabalコマンドにはsudoを付ける必要があります。
Yesodアプリのビルドにはまだ足りないライブラリがあって、cabal installコマンドはこれらのライブラリをインストールしようとするのですが、このときにルート権限が必要なのです。
2つ目以降のYesodアプリの場合は、sudoは必須ではありません。
ずらずらっとコンソールにログが出て
... Linking dist/build/first-heroku/first-heroku ... Installing executable(s) in /home/****/.cabal/bin
おお!!成功!!
実行してみよう・・・ドキドキ・・・
sudo ~/.cabal/bin/yesod devel
とやってコンソールに
Devel application launched, listening on port 3000"
と表示されたら
http://localhost:3000/
にアクセスします。
・・・おおおっっっ!!
ついにやった!!
しかしまだ安心出来ません。
目標はあくまでHerokuに実行ファイルを公開すること。
これは次のエントリにて。
MacでHerokuにYesodを(GHC/cabal-install導入編)
前回Ubuntu on VirtualBox環境を作成したので、次はGHC。
更にYesod開発に必須のcabal-installもインストールしてしまいます。
GHCをインストール
ここからはUbuntu on VirtualBoxでの作業。
まず依存ライブラリをインストール。
sudo apt-get install libreadline5 libgmpxx4ldbl libgmp3c2 libgmp3-dev
次にGHCのダウロードページからGHCのバイナリをダウンロードしてきましょう。
バージョンは、今回は7.0.4を採用。
ファイル名はghc-7.0.4-x86_64-unknown-linux.tar.bz2となります。
32bit版とまちがわないように気を付けて下さい。
ダウンロードしたファイルを解凍。
tar xvf ghc-7.0.4-x86_64-unknown-linux.tar.bz2
解凍したディレクトリに降りていってインストール。
cd ghc-7.0.4 >|| ./configure
sudo make install
ここでこんな↓エラーが出るかもしれません。
make -r --no-print-directory -f ghc.mk install BINDIST=YES NO_INCLUDE_DEPS=YES mk/config.mk:28: /home/****//mk/project.mk: No such file or directory mk/config.mk:210: /home/****//mk/install.mk: No such file or directory ghc.mk:128: *** Please run ./configure first. 中止. make: *** [install] エラー 2
project.mkとinstall.mkがホームディレクトリの下にないって言っています。
カレントの下のmkディレクトリにはあるので、無理矢理コピーするとうまくいきました。
mkdir ~/mk
cp ./mk/project.mk ~/mk
cp ./mk/install.mk ~/mk
cabal-installをインストール
cabal-installをインストールしますが、先に依存ライブラリをインストールする必要があります。
依存ライブラリの中にはapt-getでインストールすることが出来るものもありますが、GHC6まで一緒にインストールされてしまうようなので、HackageDBから一つ一つダウンロードしてインストールすることにしました。
インストールしたライブラリは以下の通りです。入れ子は依存関係を示しています。
- HTTP-4000.2.2
- network-2.3.0.8
- parsec-3.1.2
- text-0.11.1.12
- deepseq-1.2.0.1
- text-0.11.1.12
- parsec-3.1.2
- mtl-2.0.1.0
- transformers-0.2.2.0
- network-2.3.0.8
- zlib-0.5.3.1
- sudo apt-get install zlib1g-dev (Haskellのライブラリにあらず)
mtlはparsecのインストールにも必要なので、下からインストールして行くとOKです。
以上のライブラリをインストールしたら、cabal-installをインストールします。
Hackageからcabal-installのインストールファイルをダウンロードします。
バージョンは今回は0.10.2。
ん?0始まりっちゃーなにごと?
なおcabal-installのインストールの途中、Ubuntu on VirtualBoxが不安定になり再起動せざるを得なくなる、という現象が起きました。
これはゲストOSのメモリを増やしたら解消しました。
今回はここまで。
次はYesodのインストールです。
メモ・HackageDBからダウンロードしたライブラリのインストール手順
以下の5手。
tar xvf HackageDBからダウンロードしたファイル
cd 解凍したディレクトリ
sudo runghc Setup.hs configure
Setup.hsはSetup.lhsのこともあります。
sudo runghc Setup.hs build
sudo runghc Setup.hs install