2009年12月9日水曜日

Chromeベータでブックマークの同期を試したら

いい加減肥大化して動作が重たくなったFirefoxから、デフォルトブラウザをChromeに変更することにした。これまでいろいろ便利なようにFirefoxをカスタマイズしていたのだが、Chromeの軽快な動きとExtensionの利用で、これまでとほぼ同じような使い勝手がChrome上で実現できそうだったので。

それでChrome 4.0のベータ版を導入し、すべてのブックマークをGoogle Bookmarksで同期するようにしてみたら・・・。

初回同期はうまく行った。その後、試しに同期しないように設定を変更し、ローカルのPCでブックマークを編集。そして再度同期する設定にして同期を試してみたら・・・、何時まで経っても「ブックマークを統合します。」のまま。

統合に時間がかかっているのかもと思い、Chrome上のブックマークをすべて削除し、Google Bookmarksにアクセスしてオンライン上にもブックマークが一件もないことを確認。そしてFirefoxからChromeへすべてのブックマークをインポートし、再度ブックマークを同期させる設定へ。

「ブックマークを統合します。」でスタックしたまま・・・。

と思ったら、このダイアログ、広げることができました!で、広げると「統合して同期」という文字が・・・orz。なんと・・・。
「統合して同期」を選ぶとすぐに完了ダイアログが表示されました。
問題なくChromeとGoogle Bookmarksはシンクします!

2009年12月2日水曜日

Elements Autoanalyzerを止めるには

Adobe Premiere Elements 8をインストールしてから、マシーンの起動時に自動的に「Elements Autoanalyzer」というプログラムが走るようになった。Premiere Elementsを起動していなくても自動的に走る。しかもこれが常にメモリを200MBから300MBほど消費する。メモリほどではないが、CPUもそれなりに使用する。

Premiereで使用するメディアを整理して表示するAnalyzerという機能が、メディアの追加を自動的に判断するために、このサービスが走っていると思うのだが、Analyzerはあまり使わないのでオフにする。

Premiere Elementsを起動し、プレビュー画面右側にあるメニューから「整理」をクリック。別ウインドウでAnalyzerが開くので、

Analyzerのメニューから「編集」>「環境設定」>「自動解析オプション」とクリックし、
「起動時に解析を実行」のチェックをオフにする。

「起動時に」というのは「Premiere Elementsの起動時に」かと思っていたのだが、「マシーンの起動時に」という意味だったらしい。

Premiere Elementsは、使っていて思うのだが、この件に限らず全般的にソフトに詰めの甘さを感じる。UI、性能、反応速度、ともにあまりよくない。決定的なのは編集作業中にクラッシュした場合、作業中に保存しておいたプロジェクトも、保存内容が消え、Premiere Elements起動前の状態に戻ってしまうこと。

久しぶりに、「ゆとりを持って触る」という配慮が必要なソフトに出会った。そんなに遅いマシーンじゃないんだけどな。

iPhoneはステルスAPが有効だと自動再接続に失敗するようだ

無線LANの設定でステルスAPを有効にしたら、使用しているiPhoneが常に3Gで接続するようになってしまい、自動的に無線LANへ再接続しなくなってしまった。

ステルスAPが有効でも、一度接続が確立できたら自動的に再接続を行うのではないかと思っていたのだが、どうやら違うようだ。一度接続が確立し自動再接続の設定がオンになっていても、ステルスAPが有効の場合は再接続に失敗してしまうらしい。

iPhoneでスムーズに無線LANへ再接続したかったら、ステルスAPは無効に設定しておかないといけないのね。

こういう周辺機器のために、無線LANのセキュリティレベルがだんだんと弱くなっていくような気がする。

2009年11月6日金曜日

サービスを運営していくときの心構え

Google Appsは常に「バージョン・ベスト」、米グーグル副社長 - @IT

記事の後半、米グーグル製品管理担当副社長のブラッドリー・ホロウィッツ氏(Bradley Horowitz)が、システムが仕様を変更した時、ユーザーが見せる反応に対する心構えについて語っている点が興味深い。


自分達が作ったシステムを使ってくれているユーザーからのレスポンスは、本当にありがたい。示唆に富んでいるし、自信や勇気を与えてくれるものもある。しかし、影響されすぎてもいけないし無視するのは論外。自分はどちらかというと影響されすぎる派だが(^^;)。

自分の中にフィルターを持たなければならないというホロウィッツさんの言葉を、大切に心にとどめておこう。

2009年11月3日火曜日

映画「沈まぬ太陽」当たり前のことの大切さ

映画『沈まぬ太陽』公式サイト

映画の日に映画「沈まぬ太陽」を観にいった。日曜日で映画の日ということもあってか、客席は満員。同様にマイケルの「THIS IS IT」や「カイジ」なども満員だった。久々にごった返しの映画館を体験したが、大勢で見る映画は、なんとなく嬉しい。

インターミッションがある映画は初めてだったが、3時間22分という時間を長いと感じることはなかった。3時間22分を見せきるだけの迫力や意思を持った映画だった。

しかし、「沈まぬ太陽」は体力気力充実している時に見ることをお勧めします(^^;)。映画と向き合わざるをえないようなプレッシャーを持っているので、見終わるとちょっと疲れますよ。


考えさえられたのは「会社で働く」ことに対する価値観の違い。私だったら左遷やいじめにあってまでも会社に残り続けるという選択肢はとらないだろうな。勿論、詫び状なんか書かないが、会社にも残り続けない。

主人公の恩地元が会社を辞めない理由は、映画では「俺の矜持が許さないんだ」という台詞で表現されていた。パキスタンやイランやケニアへの移動を命ぜられ、望むような仕事ができなくても会社に残り続ける、つまり「逃げない」ということが彼のプライドであり、矜持だと。しかしだったらそれは何のための矜持なのか。

私だったら、会社を辞め、空の安全や社員の待遇向上を外の世界から指摘するジャーナリスト的な仕事をやろうとするかな。10年不遇の時代をすごすなら、食べることには事欠くかもしれないが、会社を変えられ、会社のためになる可能性のある仕事をしたいと思う。

そこで思ったのが、この時代、会社を辞めるということはよほどのことだったのかもしれないということ。映画の最後の方で恩地の長男、克己の台詞で「親父とお袋の生きてきた時代は俺らとは違うんだから。でも、親父とお袋は逃げなかった。」というものがあった。

この作品は、時代を超えても普遍的なものと、その時代を経験したものにしか理解できないものが混在している感じがした。


時代を超えても普遍的だと思ったのが、航空機事故で亡くなった多くの方々のご遺族の悲しみだ。さっきまで笑顔で手を振っていたわが子、電話で話していた夫が、突如として事故で亡くなってしまう。その悲しみは本当に深く、つらく、映画を見ていても涙が止まらなかった。

航空機事故だけでなく電車でもJR福知山線の脱線事故のように、多くの方が亡くなられる事故がある。事故は、突然いとしいものを奪う。その絶望は途方もないのだと感じた。

いとしいものに囲まれてすごしていることはどれほど幸せなことか。主人、両親、友人、多くのいとしいもの達に感謝した。そして彼らと共に過ごせていることが決して当たり前ではなく、どれほど恵まれていることなのか感じる感性を失ってはいけないと思った。

日本郵政新社長人事についての記事争論

日本郵政の新社長人事について、テレビやインターネット記事でさまざまな意見が展開されています。私自身もこのことについていろいろと考えていますが、その思考の材料として、いくつか記事をピックアップしてみました。

まずは竹中平蔵元総務相の意見を紹介した記事から。
郵政“再国有化”竹中元総務相、激怒 米紙に寄稿「時計の針を10年戻す」(産経新聞) - goo ニュース
今回の処置は日本を「さらにもうひとつ(10年)」失わせる道へ乗せるものだと批判されています。

一方、亀井大臣の今回の人事は実は官民のバランスが取れた人事だという記事も。そして今回の人事の本質は「反自民」「反小泉・竹中」「反西川」であるという町田氏の指摘。
元大物大蔵次官を郵政社長に登用した亀井大臣の真の狙いと、その危うさ | 経済ジャーナリスト 町田徹の“眼” | ダイヤモンド・オンライン

同様に、適性や経験などを無視した単なる小泉・竹中改革への亀井大臣の意趣返しだという岸氏の指摘。
小泉・竹中改革への意趣返しと中身のない パフォーマンスでは日本がダメになる! ~国民新党の暴走と行刷会議の迷走を憂う | 岸博幸のクリエイティブ国富論 | ダイヤモンド・オンライン

そして、郵便局ネットワークを維持するコストを今後民主党はどのように埋めていくのかビジョンが示されていないという辻広雅文氏の指摘。
“郵政改革の大転換”に見る日本の宿痾 ~なぜ、焼け野原にならなければ改革できないのか | 辻広雅文 プリズム+one | ダイヤモンド・オンライン


個人的には、生田氏が総裁を勤めていた郵政公社の時代が、郵便局のサービスが最もよかった気がします。24時間窓口営業なども行われていて、採算が心配でしたが、サービスとしてはとても助かるもので、重宝していました。

辻広雅文氏の指摘する郵便局ネットワークの維持コストだけでなく、今回最大規模になってしまった予算の概算要求額など、民主党が掲げたムダの削除というイメージが持つスリムさとは逆に、予算も行政も膨張している印象を持ちます。

また、人事の決定プロセスはやはり問題があったように感じます。会社法に基づいて「委員会設置会社」となる日本郵政は、取締役会の中に設けた指名委員会で社長を含む取締役の選任、解任案をまとめるのがルールであり、そのルールは、ましてや国が破るべきではないと思います。

民主党には政権交代で国民が求めた本筋を見失うことなく、突き進んで欲しいものです。

中国での音楽の有料配信は無理?

版権保護は進むも、健全には程遠い中国デジタル音楽市場 | News&Analysis | ダイヤモンド・オンライン

中国で音楽の有料配信がこんなに難しいとは知らなかった。

海賊版が出回る中、正規版を流通させることそのものがまず難しい。また、海賊版を無料で手に入れられる環境では、正規版を有料で配信し買ってもらうことはもっと難しい。

とりあえずGoogle中国は、広告配信で正規版のダウンロードサービスを行い、海賊版の流通に風穴を開けたようだ。音楽のダウンロードさえも広告モデルとは。


無料で当たり前、という消費者の発想を変えていくのは並大抵ではないだろう。

インターネットの普及が進めば、多くの「情報」がほぼ無料で手に入れられる。その「フリー」な環境でどうやって「サービス」や「コンテンツ」で生業を立てていくか、いわゆるビジネスモデルを確立していくことは非常に知恵がいる。

2009年10月29日木曜日

DroidのGoogle Maps Navigationはスゴそうだ!

DroidのGPSを使ったGoogle Mapsナビはスーパー・クール!

スーパー・クール、いいですねぇ。記者の興奮が伝わってきます。
実際に、YouTubeにあるGoogleの公式紹介ビデオを見ましたが、これは欲しくなります。
ちなみにビデオの中で彼が着ている「I am here.」というTシャツも欲しくなります(^^)



音声検索ができるし、ストリートビューに自動的に切り替わったりするし、ドックモードもあって、しかも無料。カーナビゲーションシステムを開発しているところは苦しくなるかもな、と心配になるくらいの出来ですね。

恐らく実際に動かしてみると、クラッシュしたり思い通りに動作しないところもあるのでしょうが、チューニングやバグフィックスが重ねられてソフトの品質が高まってくると、脅威でしょうね。


上記の公式紹介ビデオでは、実際に開発に当たっているGoogleのエンジニアの方がソフトの紹介をしていましたが、Google社員はプレゼン能力も高くないといけないんだなと思いました。そういった意味では、Googleはまるで大学の研究室のようでアカデミックですね。

YouTubeのGoogle DeveloppersのチャンネルにはGoogle社員がデモンストレーションやプレゼンテーションをしている多くのビデオがありますが、動画に対するコメントの中にプレゼン能力に対する言及も結構見受けられます。

Googleの社員はエンジニアとしてもプレゼンテーターとしても高い能力が求められるのでしょうね。

2009年10月26日月曜日

空援隊を「知ってしまった」ということ

気迫で実現したご遺骨の帰還:野口 健(アルピニスト)(1)

アルピニストの野口健さんが日本に帰還できていないご遺骨を収集する活動をなさっていることは知っていたが、これほど過酷なものとは知らなかった。というか、活動をしてらっしゃることは知っていても、それがどれほど根気も、精神力も、資金も、政治的な交渉も必要なしんどい活動なのか、その実態を全く知らなかった。登山家なのになんでそのような活動をされてるんだろう、と素朴に疑問に思っていた程度だった。

でも彼の記事を読むと、彼がこの活動を行う決意や衝動のようなものを知ることが出来る。大仰な大義を振りかざすのはなく、正直な言葉で淡々と、しかし力強く。

一番印象に残ったのは彼がなぜこの活動を続けるのかという問に対する答えの部分。


日々いろんな情報や人や出来事に出会う。中には、自分が何か出来ることはないか、考えこんでしまうものもある。でも「知ってしまって」おきながら、自分の背中にはまだそれが背負えないだろうという結論にいたることが多く、罪悪感を感じる時も。

いっそ、知らなければよかったと思ったり、知ることを怖がって新しいものに出会うことを避けようとしたりも。

知ってしまったそのことに正面から向き合い格闘している彼の姿はすごいですね。尊敬します。

自分も彼の活動を知って、何かできることはないか考えて、空援隊に寄付をしました。これからも定期的に行うつもりです。
三菱東京UFJ銀行 京都支店  普通口座 6816051  口座名 トクヒ)クウエンタイ


いろんなものを背負える人間になりたいなぁ~~~!!!!

BlazeではKey型はマッピングされないのでString型をメインに使おう

エンティティグループの永続化をunowned-relationshipで構成しようと、gae.parent-pkを使い、親キーのタイプをKey型で指定していた。

しかし、システムの環境としてBlazeDSを使っていたので、ActionScriptが展開できる型にKey型がなく問題に。ふむー。

ちなみにActionScriptとJavaで互換性のあるデータタイプの一覧はこちら。
Explicitly mapping ActionScript and Java objects

よってKey型をやめ、String型をメインに使うように仕様を変更し、encoded-pk属性を追加。

また、データ登録時に主キーにユニークな値を設定してほしいので、IdGeneratorStrategy.IDENTITYの設定も有りにして、以下のような構成になった。
@PersistenceCapable(identityType = IdentityType.APPLICATION, detachable = "true")
public class Parent{

 @PrimaryKey
 @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
 @Extension(vendorName = "datanucleus", key = "gae.encoded-pk", value = "true")
 private String parentId;

//以下アクセッサー

}

@PersistenceCapable(identityType = IdentityType.APPLICATION, detachable = "true")
public class Child {

 @PrimaryKey
 @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
 @Extension(vendorName = "datanucleus", key = "gae.encoded-pk", value = "true")
 private String childId;

 // 親キーIDを格納するプロパティ
 @Persistent
 @Extension(vendorName = "datanucleus", key = "gae.parent-pk", value = "true")
 private String parentId;

//以下アクセッサー

}
これからはBlazeDSとの互換性の意味でも、このような主キー、親キーの設定方法でいこう。


それにしても、Kindの形をドンドン変えられるというのはスゴイ。これまでのRelational DataBaseでは考えられない拡張性だ。フィールドの追加も型の変更も、オンザフライでドンドン変えられる。この体験はまさに別世界だなー。この環境を与えてくれたGoogleに感謝!

2009年10月23日金曜日

オロチのレンタル興味あり!

以前深夜TVで目にしてから気になっていたオロチ。
それがレンタルできるようになっていたそうだ。値段は1泊2日で3万5千円から。
検索・急上昇:光岡 ヒミコ - 毎日jp(毎日新聞)
光岡自動車 ミツオカ・レンタカープラン

不況の影響などで車を売るのが難しくなっている状況を考えると、とてもいいアイデアだと思います。
自分でも今度申し込んでみたいなと(^^)。

ただ、年内は既に予約いっぱいで来年1月からの利用申し込みが11月1日からできるようになるそうです。
(株)光岡自動車 『大蛇』(オロチ)、『卑弥呼』(ヒミコ)レンタカー予約状況について(PDF)

国産のスポーツカーを赤字覚悟で出す!という社長の覚悟で作られたとデザイナーの方がTVでおっしゃってましたが、車も魅力的ですが、それ以上にそういうストーリーに惹かれるのだと思います。

テレ朝のサンデープロジェクトで以前放送された、マツダのRX-8のロータリーエンジンを開発するのに開発者がどんだけ苦労したかとか、日産のフェアレディZの起死回生の話も印象に残っています。

でも、特にこのオロチを出している光岡自動車は富山県富山市の従業員600人未満の会社で、ワンアンドオンリーのものを創っている。その気概に共感します。

オロチのページのFlashもとてもいいです。 大抵こういうものはカッコばかりになりがちですが、そうじゃない。使われている写真の一つ一つに、この車がどんなに素敵か伝えたい!という写真家の愛情を感じます。オロチを十分理解した上で構成されているな、と。神社の境内にオロチを置いて撮影したり、発想もいいですね。

BGMもいい。その名も「大蛇伝説」。マジカッケェー。スクリーンセーバーにしたいくらいです。

頑張れ、光岡自動車!自分もがんばるぞ、と。

gae.parent-pkを使うと親キーの型はKeyかStringでLongはダメ

エンティティグループの永続化をunowned-relationshipで構成していた場合、親キーのタイプはStringかKey型でないとダメだそうだ。

以下のように@Extensionのアノテーションを使って簡単にリレーションを作っていた。
@PersistenceCapable(identityType = IdentityType.APPLICATION, detachable = "true")
public class Parent{

 @PrimaryKey
 @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
 private Long parentId;

//以下アクセッサー

}

@PersistenceCapable(identityType = IdentityType.APPLICATION, detachable = "true")
public class Child {

 @PrimaryKey
 @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
 private Key childKey;

 // 親キーIDを格納するプロパティ
 @Persistent
 @Extension(vendorName = "datanucleus", key = "gae.parent-pk", value = "true")
 private Long parentId;

//以下アクセッサー

}
必ずKey型でないといけない~みたいなドキュメントがなかったので、StringかLongでもいけるかなと、この場合はLongを指定していた。そうしたら以下のようなExceptionが出た。
javax.jdo.JDOFatalUserException: Error in meta-data for hoge.Child.parnetId: Parent pk must be of type String or com.google.appengine.api.datastore.Key.
なるほどですね。
StringやKeyはOKだけどLongはNGなのですね。

2009年10月16日金曜日

SDK1.2.6で開発サーバーが起動するようになった

SDK1.2.6で開発サーバーが起動しない問題の続き。

GAE/Jのグループの、SDK1.2.6でも新規に作成したプロジェクトだと問題なく開発サーバーが起動するという投稿を参考にして、新規にプロジェクトを作成した上で、これまでのリソースをコピーし、起動を試してみた。すると、起動するようになった!!やたっ!

[環境]
  • Eclipse : v3.4.2(Ganymade)
  • Google Plugin for Eclipse 3.4 : v1.1.2
  • Google App Engine Java SDK : v1.2.6
注意点は、ユニットテストをするためにappengine-api-stubs.jarとappengine-local-runtime.jarへクラスパスを通している場合。これら2つのjarに関しては、SDK1.2.5に同梱されていたものを使わないといけない。でないと、InvocationTargetExceptionが出て開発サーバーの起動に失敗する。
java.lang.reflect.InvocationTargetException
 at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
 at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
 at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
 at java.lang.reflect.Method.invoke(Method.java:597)
 at sun.instrument.InstrumentationImpl.loadClassAndStartAgent(InstrumentationImpl.java:323)
 at sun.instrument.InstrumentationImpl.loadClassAndCallPremain(InstrumentationImpl.java:338)
Caused by: java.lang.RuntimeException: Unable to find appengine-agentimpl.jar in hogehoge\war\WEB-INF\junitlib
 at com.google.appengine.tools.development.agent.AppEngineDevAgent.findAgentImplLib(AppEngineDevAgent.java:97)
 at com.google.appengine.tools.development.agent.AppEngineDevAgent.premain(AppEngineDevAgent.java:48)
 ... 6 more
FATAL ERROR in native method: processing of -javaagent failed
Exception in thread "main" 
要は、ユニットテストのために上記2つのjarをwar\WEB-INF\junitlibに入れ、クラスパスを通していたのだが(ユニットテストの環境設定についてはこちらにまとめています)、1.2.6に同梱されているバージョンのjarを使うと、クラスパスを通しているせいか、起動時にこのjunitlibの中に必要なjar(この場合はappengine-agentimpl.jar)を探しに行ってしまうのだ。

1.2.5同梱バージョンのものを使うと、InvocationTargetExceptionは表示されない。1.2.5のものではなくても、1.2.6のものでなければよいのではないかと思われ。

ちなみに自分の環境だと、-javaangentの指定をVM Argumentsに加えなくても起動します。この辺はEclipseのバージョンとGoogle Pluginのバージョンによるのかもしれません。 とりあえず、1.2.6に移行して上手く行かなくなった場合は、新規にプロジェクトを作り直してマージした方が話が早いということで。

2009年10月15日木曜日

Tweetieで自分のアイコン画像がやっと出た

Twitterクライアントとして、iPhoneでは「Tweetie」を使っている。

近所のユーザー検索などの機能を試したくて購入したが、Twitterクライアントとして機能面でも動作のレスポンス面でもほぼ満足な出来。

ただ気になるのが、自分を含めて何人かのフォロワーのアイコンが表示されないこと。これまでいくつかiPhoneのTwitterクライアントを試してみたが、アイコンが出ないという症状はこれまで経験したことは無かった。

まず疑ったのがサイズ。現在設定しているアイコンの画像サイズは240x240。これを80x80などと小さくしてみたが効果なし。

次に疑ったのがファイル名。

検索してみると、画像のファイル名に日本語が含まれたものをアイコンとして設定するとTweetieでは表示されないという記事を発見。自分の場合、ファイル名に日本語は含まれていないが、数字が含まれているし、数字始まりだ。何かしらファイル名は関係があるかもしれないと思い、アイコン画像のファイル名を全て英字に変更してみた。

すると、出た~!

Tweetie関連で検索してみると、「Tweetie アイコン」という組み合わせが結構上位に出てくる。ということは自分以外にもアイコンが表示されずに困っているユーザーが結構いるのかもしれませんね。Tweetieでアイコンが表示されない方は、画像のファイル名に英字以外が含まれていないかチェックしてみてください。

サプリーンでは数字始まりのファイル名にしているんだが、それだとファイル名をリネームしないとTweetieのアイコンとして表示されないということか・・・。ちょっと問題だなぁ。

ちなみにTweetieはユーザー情報をキャッシュするので、変更したアイコン情報がアプリ側に反映されるまで約1日かかります。効果があったかどうかは1日経ってからチェックする必要があります(^^;)

2009年10月14日水曜日

Eclipse3.4だとSDK1.2.6が問題?

先ほどのSDK1.2.6にして開発サーバーが起動しない問題の調査の続き。
同じような症状に見舞われている方はけーん。
Not able to start development application server anymore in eclipse 3.4 with SDK 1.2.6

Eclipse3.4だと問題なのか?

ちょっとすぐには解決できないかもしれない。優先順位を落として引き続き調査だな。
Eclipseのバージョンアップしかないかもしれない。
ちょっとめんどくさいな。

SDK1.2.6で開発サーバーが起動しない

今日、GAEのSDK1.2.6がリリースされた。PythonとJavaとどちらのランタイムでも出ている。
Google App Engine Blog: App Engine SDK 1.2.6 Released with Incoming Email, App Deletion, and more!

メールの送信だけじゃなく受信もできるようになったり、アプリケーションが削除できるようになったり、ローカルストレージの統計もより細かく見られるようになった。

ためしにEclipseのソフトウェア更新機能でSDK1.2.6をインストールしてアプリケーションを実行したところ、RuntimeExceptionが出て開発サーバーが起動しない。
java.lang.RuntimeException: Unable to locate the App Engine agent. Please use dev_appserver, KickStart,  or set the jvm flag: "-javaagent:<sdk_root>/lib/agent/appengine-agent.jar"
 at com.google.appengine.tools.development.DevAppServerFactory.testAgentIsInstalled(DevAppServerFactory.java:102)
 at com.google.appengine.tools.development.DevAppServerFactory.createDevAppServer(DevAppServerFactory.java:77)
 at com.google.appengine.tools.development.DevAppServerFactory.createDevAppServer(DevAppServerFactory.java:38)
 at com.google.appengine.tools.development.DevAppServerMain$StartAction.apply(DevAppServerMain.java:153)
 at com.google.appengine.tools.util.Parser$ParseResult.applyArgs(Parser.java:48)
 at com.google.appengine.tools.development.DevAppServerMain.<init>(DevAppServerMain.java:113)
 at com.google.appengine.tools.development.DevAppServerMain.main(DevAppServerMain.java:89)
Caused by: java.lang.NoClassDefFoundError: com/google/appengine/tools/development/agent/AppEngineDevAgent
 at com.google.appengine.tools.development.DevAppServerFactory.testAgentIsInstalled(DevAppServerFactory.java:98)
 ... 6 more
Caused by: java.lang.ClassNotFoundException: com.google.appengine.tools.development.agent.AppEngineDevAgent
 at java.net.URLClassLoader$1.run(URLClassLoader.java:200)
 at java.security.AccessController.doPrivileged(Native Method)
 at java.net.URLClassLoader.findClass(URLClassLoader.java:188)
 at java.lang.ClassLoader.loadClass(ClassLoader.java:306)
 at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:276)
 at java.lang.ClassLoader.loadClass(ClassLoader.java:251)
 at java.lang.ClassLoader.loadClassInternal(ClassLoader.java:319)
 ... 7 more
これまでどおり、SDK1.2.5に戻すと、正常にサーバーは起動する。

SDK1.2.6のlibの下には、SDK1.2.5のlib下にはなかったagentフォルダがあるが、それが何か関係しているのか?

不思議なのは何度か1.2.5と1.2.6をスイッチングしていると1.2.6でもRuntimeExceptionが表示されなくなるということ。
The server is running at http://localhost:8080/
と出るので、開発サーバーが正常に起動できたのかと思ってしまう。

でも、ブラウザでアドレスにアクセスしてみると、ページは真っ白で、今度はエラーがコンソールに。
java.lang.AbstractMethodError: com.google.appengine.tools.development.DevAppServerImpl.getUserPermissions()Ljava/security/Permissions;
 at com.google.appengine.tools.development.DevAppServerFactory$CustomSecurityManager.checkPermission(DevAppServerFactory.java:128)
 at java.lang.Thread.setContextClassLoader(Thread.java:1351)
 at org.mortbay.jetty.handler.ContextHandler.handle(ContextHandler.java:739)
 at org.mortbay.jetty.webapp.WebAppContext.handle(WebAppContext.java:405)
 at com.google.apphosting.utils.jetty.DevAppEngineWebAppContext.handle(DevAppEngineWebAppContext.java:54)
 at org.mortbay.jetty.handler.HandlerWrapper.handle(HandlerWrapper.java:139)
 at com.google.appengine.tools.development.JettyContainerService$ApiProxyHandler.handle(JettyContainerService.java:342)
 at org.mortbay.jetty.handler.HandlerWrapper.handle(HandlerWrapper.java:139)
 at org.mortbay.jetty.Server.handle(Server.java:313)
 at org.mortbay.jetty.HttpConnection.handleRequest(HttpConnection.java:506)
 at org.mortbay.jetty.HttpConnection$RequestHandler.headerComplete(HttpConnection.java:830)
 at org.mortbay.jetty.HttpParser.parseNext(HttpParser.java:514)
 at org.mortbay.jetty.HttpParser.parseAvailable(HttpParser.java:211)
 at org.mortbay.jetty.HttpConnection.handle(HttpConnection.java:381)
 at org.mortbay.io.nio.SelectChannelEndPoint.run(SelectChannelEndPoint.java:396)
 at org.mortbay.thread.BoundedThreadPool$PoolThread.run(BoundedThreadPool.java:442)
2009-10-14 02:19:43.885::WARN:  handle failed
java.lang.IllegalStateException: Request in context!
 at org.mortbay.jetty.Request.recycle(Request.java:163)
 at org.mortbay.jetty.HttpConnection.reset(HttpConnection.java:470)
 at org.mortbay.jetty.HttpConnection.handle(HttpConnection.java:450)
 at org.mortbay.io.nio.SelectChannelEndPoint.run(SelectChannelEndPoint.java:396)
 at org.mortbay.thread.BoundedThreadPool$PoolThread.run(BoundedThreadPool.java:442)
結局、SDKは1.2.5のまま開発を続けていますが・・・
原因は分からず。何なんでしょうねぇ。

2009年10月13日火曜日

よこはま動物園ズーラシアに行ってきた

天気が良かったので、先日ゲットしたPanasonic Lumix DMC-GH1の写真、動画の撮影練習を兼ねて、よこはま動物園ズーラシアに行ってきました。

GH1のレンズキット(GH1K)は、ボケ味が出て、かなり満足なレンズです。さらに、象の肌の質感やペンギンのおなかについている水滴まで描写でき、描写力もすごいです。

2009年10月9日金曜日

Google ストリートビューで宮古島へバーチャル帰省

Googleストリートビューが旭川や沖縄に対応した。
グーグル、ストリートビューの対象エリアを拡大--旭川や沖縄など

ということで、試しにおじぃのおうちまでトリップできるかテストしてみたら、ばっちりおじぃの軽トラまで視認できた。スゴイ。宮古島までバーチャル帰省できた感じ。

だったら勿論、厚木も対応になったんだよねと思って試してみたら、ノーorz。

相模川はまだ越えられていませんでした。うちの周辺はストリートビュー非対応。宮古島以下だなんて・・・

<2010.03.17追記>
相模川を超えたようです。厚木もストリートビュー対応になりました(^^)/

Raul Midonのプレミアムライブに行ってきた

抽選なるものにこれまで当たったことが無いのに、ラッキーにも10月8日に行われるRaul Midonのプレミアムライブに当たっちゃいました!ということで代官山UNITへ。

Raul Midonの出待ち中。
Share photos on twitter with Twitpicニューアルバム「SYNTHESIS」から「Next Generation」「Don't Take It That Way」「Blackbird」など約30分ほど演奏してくれました。やっぱりボーカリストとしてもギターリストとしてもレベルが高かったです。特に「Blackbird」が生で聴きたくて楽しみにしていましたが、鳥が空へ向かって舞い上がっていく開放感が感じられて、よかったです。

でも「おお、いい!」という感じではなく「あー、よかったねぇ」という感じでした。期待が大きすぎたんでしょうか(^^;)


個人的には、Tahnyaのヒラタ君のギターの音の方が好きかも、演奏も彼の方が多才or多彩かも、なんて感じたりして。

改めて、Tahnyaの「Blackbird」もなかなかのもんです。

2009年10月1日木曜日

pk-nameとencoded-pkはセットです

使用されたタグやその使用回数を保存するクラスをつくり、未使用のタグだったら新規追加、既使用だったら回数増加ということをやっていた。タグの文字列はString型でencoded-pk属性。こんな感じ。
@PrimaryKey
@Persistent
@Extension(vendorName = "datanucleus", key = "gae.encoded-pk", value = "true")
private String tagKey;

@Persistent
private Long count;

public Tag(String tagName) {
 this.tagName = tagName;
 count = new Long(1);
}
タグを追加するときの処理。
for (int i = 0; i < tagArray.length; i++) {
 String tag = tagNameList.get(i);
 Key k = KeyFactory.createKey(Tag.class.getSimpleName(), tag);
 try {
  tagArray[i] = pm.getObjectById(Tag.class, k);
  tagArray[i].increment();
 } catch (JDOObjectNotFoundException e) {
  tagArray[i] = new Tag(KeyFactory.keyToString(k));
 }
}
でも、エンコードされた文字列であるtagNameをデコードして見てみると
Tag(文字列1)
Tag(文字列2)
などのように表示される。うむぅ。文字列だけ取得したいな。

そこでGoogleのドキュメント復習。encoded-pkとpk-nameについて読み込み。
データの作成、取得、削除 - Google App Engine - Google Code

pk-nameにエンコードしていない文字列(つまり表示したい文字列)をセットし保存することにした。1プロパティ追加。
@PrimaryKey
@Persistent
@Extension(vendorName = "datanucleus", key = "gae.encoded-pk", value = "true")
private String tagKey;

@Persistent
@Extension(vendorName = "datanucleus", key = "gae.pk-name", value = "true")
private String tagName;

@Persistent
private Long count;

public Tag(String tagKey, String tagName) {
 this.tagKey = tagKey;
 this.tagName = tagName;
 count = new Long(1);
}
タグを追加するときはエンコードされたキーとプレーンなStringの両方を引数に与えるようにした。
for (int i = 0; i < tagArray.length; i++) {
 String tag = tagNameList.get(i);
 Key k = KeyFactory.createKey(Tag.class.getSimpleName(), tag);
 try {
  tagArray[i] = pm.getObjectById(Tag.class, k);
  tagArray[i].increment();
 } catch (JDOObjectNotFoundException e) {
  tagArray[i] = new Tag(KeyFactory.keyToString(k), tag);
 }
}
このようにすると、pk-nameを設定したtagNameからプレーンな文字列が取得できるようになった。


と、そこでちょっと考えてみた。

encoded-pkしてStringとして使ってても、結局、文字列を引数にcreateKeyして、keyを文字列へ変換して設定という流れ。ということはKey型の方がシンプルかも?

で、tagKeyの型をStringからKeyへ変更し、tagKeyにつけていた@Extension(vendorName = "datanucleus", key = "gae.encoded-pk", value = "true") を削除。タグを追加するときもkeyToString()しない仕様に変えてみた。
@PrimaryKey
@Persistent
private Key tagKey;

@Persistent
@Extension(vendorName = "datanucleus", key = "gae.pk-name", value = "true")
private String tagName;

@Persistent
private Long count;

public Tag(Key tagKey, String tagName) {
 this.tagKey = tagKey;
 this.tagName = tagName;
 count = new Long(1);
}

:
:
:
//タグを追加するとき
for (int i = 0; i < tagArray.length; i++) {
 String tag = tagNameList.get(i);
 Key k = KeyFactory.createKey(Tag.class.getSimpleName(), tag);
 try {
  tagArray[i] = pm.getObjectById(Tag.class, k);
  tagArray[i].increment();
 } catch (JDOObjectNotFoundException e) {
  tagArray[i] = new Tag(k, tag);
 }
}
そしてテストしてみたら・・・

怒られた。

javax.jdo.JDOFatalUserException: Error in meta-data for hoge.Tag.tagName: A field with the "gae.pk-name" extension can only be used in conjunction with an encoded String primary key..

なるほど。pk-name使いたかったらKey型はNGで、String型のフィールドにencoded-pkのExtension付きじゃないとダメですよ、ということなんですね。了解です。

2009年9月30日水曜日

InvocationTargetExceptionのその後

以前書いたInvocationTargetExceptionがよく起こる点について、GAE/JのGoogle Groupsを見ていたら、同じようなことで悩んでるスレッド発見。

Startup takes forever - Google App Engine for Java | Google Groups
new stack traces after upgrading to SDK 1.2.5 - Google App Engine for Java | Google Groups

後者の方で言われているが、スレッドがサポートされてないので、やっぱりこの例外はどうしようもないのかもしれないし、クリティカルなものではないからINFOレベルなわけだし。

ただそのとき、CPU時間がグッとあがる点がね・・・。ユーザー数やリクエスト数が増えたときにすぐに無料の範囲を超えないかって気になる。

2009年9月18日金曜日

JDOでListなどのコレクションを取得する時の注意点

Listのプロパティを取得しようとしたら以下のような例外が出た。
javax.jdo.JDODetachedFieldAccessException: You have just attempted to access field "tagNameList" yet this field was not detached when you detached the object. Either dont access this field, or detach it when detaching the object.
 at jp.co.tricell.sonicboom.Mutter.jdoGettagNameList(Mutter.java)
 at jp.co.tricell.sonicboom.Mutter.getTagKeys(Mutter.java:137)
 at jp.co.tricell.sonicboom.rpc.test.TestMutterUtils.testGetMutterOf(TestMutterUtils.java:69)
 at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
 at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
 at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
 at java.lang.reflect.Method.invoke(Method.java:597)
 at junit.framework.TestCase.runTest(TestCase.java:164)
 at junit.framework.TestCase.runBare(TestCase.java:130)
 at junit.framework.TestResult$1.protect(TestResult.java:106)
 at junit.framework.TestResult.runProtected(TestResult.java:124)
 at junit.framework.TestResult.run(TestResult.java:109)
 at junit.framework.TestCase.run(TestCase.java:120)
 at junit.framework.TestSuite.runTest(TestSuite.java:230)
 at junit.framework.TestSuite.run(TestSuite.java:225)
 at org.eclipse.jdt.internal.junit.runner.junit3.JUnit3TestReference.run(JUnit3TestReference.java:130)
 at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
 at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:460)
 at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:673)
 at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:386)
 at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:196)
先日購入した「Google App Engine for Java[実践]クラウドシステム構築」P124に書いてった、
「JDOにはフェッチグループという概念があり、単一のエンティティ内でも一部のプロパティを遅延して取得できます。」
と言うことの意味がこれで分かった。

プリミティブ型、そのラッパークラス、String、Dateといった基本的な型がdefault fetch groupであり、エンティティのプロパティとして「遅延無く」取得できるものなのだ。

今回使用したListはこれらに含まれていないので、自分でdefault fetch groupの指定を追加しない限り、エンティティ取得当初はデータが含まれていないということなんだな。

ということで、例外が出たプロパティに以下のアノテーションを追加。
@Persistent(defaultFetchGroup = "true")
private List<String> tagNameList;
取得成功っと。

さよならウメ子

小田原動物園の象のウメ子が天国へ旅立ってしまった。
東京新聞:突然の悲報、市民ら涙 国内最高齢ゾウ『ウメ子』天国へ 小田原:神奈川(TOKYO Web)

去年小田原に行った時のウメ子。
from Odawara Castle 2008.12.07

初めて小田原城に行った時は正直、なぜ城と象?という感じで、お城のすぐ下に動物園があるっていう光景は少し不思議だった。

でも、ご城下になぜか象がいて、猿がいて、子供たちが遊んでてっていう光景は、北条さんもちょっとびっくりだけどなんだか楽しくていいかもって思ってあるかもよ、なんて思ったりして(^^)。

それからは、小田原に行くときは、まずウメ子、そしてお城に登って、近くにある報徳神社内のcafe Gingerで一息ついて干物を買って帰るというルートが気に入ってたのに・・・。もうウメ子がいないなんて。

ウメ子は昭和25年、推定3歳のときに日本に来て以来59年。現在推定62歳で人間の年齢では約100歳。私なんかよりも長く日本を眺めてきた、ずっとずっと先輩だ。前日まで食べ物も残さず食べるなど元気だったらしいが急に亡くなったらしい。 象ほど大きくて強そうな生き物でも、不意に亡くなってしまうこともある。いつまでも小田原城にはウメ子がいるような気がしていたが、やっぱり変わらないものなんてない。

ウメ子が亡くなると、小田原城下の動物園そのものも閉園になるかもしれないとのこと。『史跡小田原城跡本丸・二の丸整備基本構想』という昔の小田原城を復元しようという構想のなかでは、「史跡にふさわしくない施設」として動物園は整理していくという方針が打ち出されているらしい。
小田原動物園の整理進む一タウンニュース

ウメ子に癒された一人として、小田原動物園という形は変わっても、あの場所が変わらず小田原の人たちの癒しや憩いの場であり続けてほしいと心から願っている。

2009年9月17日木曜日

GAE/Jでのインデックスの削除方法が分からない

検索するクエリを多く書いていくと、インデックスが増えていく。

GAE/Jでエンティティを抽出するときは、直接BigTable上のエンティティにフィルタリングしにいくのではなく、別途作成されたインデックスに対してフィルタリングを行う、という仕様になっているらしい。
参考:Java データストアのインデックスの設定 - Google App Engine - Google Code

インデックスが増加していくことは仕方ないことなのか、パフォーマンスなどのことを考えて、なるべくインデックスは増やさない方がいいのか、調査する。

すると、やはりGAE/Jでは==などの条件指定に限り、そのほかの条件による絞込みやソートはJavaのロジック側で実装した方が無難らしい。
参考:ローカルでは動作するが、GAE/J環境でエラーになってしまう - Slim3 User Japan | Google グループ

エンティティに変更が加えられるたびにインデックスが更新されるというシステムは、今後エンティティ量が増えていったとき、クエリに対してすばやく結果を返せるメリットもあるが、インデックスを更新するという作業の頻度や量が増えていくというデメリットも考えられる。以前、ほしい結果を全てクエリだけで実現できないかと思っていたが、それは少々的外れだったみたい。クエリはシンプルに徹底した方がよさそう。よって、コードを整理し、必要なインデックスを最低限に絞ることにした。

と、インデックスを削除しようとしたら、「あれ、どうすればいいんだ?」とスタックする。削除するインターフェースは見当たらないし、調べてみても削除方法について述べられているドキュメントがぱっと見当たらない。

とりあえず、war/WEB-INF/datastore-indexes.xml及びwar/WEB-INF/appengine-generated/datastore-indexes-auto.xmlを一旦削除。

ローカルでサーバーを起動し、ユニットテストするなりしてクエリを発行させて、自動的にdatastore-indexes-auto.xmlを作成。その記述内容を元にdatastore-indexes.xmlを作成しGAE/Jサーバー側へデプロイする。→インデックス、変化なし。

念のため、autoGenerate="false"にし、強制的にdatastore-indexes.xmlを使うようにしても変化無し。以下はそのときのdatastore-indexes.xml。



    
        
        
    


datastore-indexes.xmlの中身を空にしてデプロイしてみたが、それでも変化なし。


appengineのbinに含まれているappcfgコマンドを使ってみる。「0% Uploading index definitions.」ってことはインデックスの更新は一切行われなかったということか。よって削除されず、これでも変化なし。
C:\eclipse\plugins\com.google.appengine.eclipse.sdkbundle_1.2.5.v200909021031\ap
pengine-java-sdk-1.2.5\bin>appcfg.cmd update_indexes "C:\Documents and Settings\
xxx\workspace\myapp\war"
Reading application configuration data...
Beginning server interaction for myapp...
0% Uploading index definitions.
Success.
Cleaning up temporary files...
現在のインデックスに存在しない指定が含まれていた場合はインデックスが新規に作成されるが、一度作成されたインデックスを含まないような記述をdatastore-indexes.xmlに加えても、削除はされないのか?!・・・もしれないorz

「今出来るベストのことは、それらのインデックスを無視することだ」といっている会話もあったが、う~ん、結局無視するしかないのか?!
参考:Delete useless Entity and Indexes - Google App Engine for Java | Google グループ

キモチワルイ・・・。しかも実際には使用していないインデックスだが、エンティティが増えるたびにその未使用のインデックスの更新も同時に行われるということで、ムダだ。

また、ちなみに自分の環境だと、datastore-indexes-auto.xmlがmyapp/war/WEB-INF/appengine-generated/ではなく、warの外、つまりmyapp/WEB-INF/appengine-generated/の下に常に作成されるのが不便。いちいちコピペしなくてはならない。設定の問題?これもキモチワルイ・・・。

2009年9月16日水曜日

VistaでGAE/Jサーバーにlocalhostとしてアクセスするには

Windows VistaでローカルのGAE/Jサーバーにアクセスするときに、どうしてもGAE/Jのドキュメント通りに「http://localhost:8080」で接続できず、「http://127.0.0.1:8080」でないと接続できないでいた。

恐らくVistaがIPv6対応であることが影響しているんだろうとは思っていたものの放置していたが、

「C:\Windows\System32\drivers\etc\hosts」に

127.0.0.1 localhost

を追加すると、localhostで接続できることが分かった。ローカルのGAE/JサーバーはIPv4でしか待ち受けていないらしい。
参考:「Google App Engine for Java[実践]クラウドシステム構築」 P73

ログインしているユーザーでhostsが編集できるように、hostsファイルのセキュリティ設定を変更する必要があることにも注意。

2009年9月14日月曜日

不等式フィルタを使い且つ複数のプロパティでソート

緯度と経度のgeoboxで絞込みを行うことと、日付の降順でソートを行うこと、これらをクエリーだけで実装しようとトライした。
Query query = pm.newQuery(Mutter.class);
query.declareParameters("String geohashFrom, String geohashTo");
query.setFilter("geohash >= geohashFrom && geohash <= geohashTo");
List mutters = (List) query.execute(geohashFrom,geohashTo);
query.setOrdering("geohash ASC, date DESC");
これだと、Geohashの昇順で並べられた中での日付降順という意味になってしまう。

ほしいのは、日付降順で並べられた、限られたgeoboxのデータリスト。つまり、ソートの順番で先に日付を持ってきたいのだが、Geohashの不等式を使っている以上、どうしてもソートの優先順位一位としてGeohashを指定しなければならない。これはGAE/J上の制約。
参考:クエリとインデックス - Google App Engine - Google Code

勿論、
query.setOrdering("date DESC, geohash ASC");
と書くと、例外が発生する。

NestedThrowablesStackTrace:
java.lang.IllegalArgumentException: The first sort property must be the same as the property to which the inequality filter is applied. In your query the first sort property is date but the inequality filter is on geohash

Query query = pm.newQuery(Mutter.class, "ORDER BY date DESC");
とも書いてみたが、例外は発生しないものの、日付降順ではソートされておらず。。。

結局、クエリでソートすることはやめて、JavaでComparatorを使いソートすることにした(^^;)。
import java.util.Comparator;

public class DateComparator implements Comparator<Mutter> {

 // デフォルトは降順
 private boolean order = false;

 public void setOrder(boolean order) {
  this.order = order;
 }

 public int compare(Mutter m0, Mutter m1) {
  if (this.order) {
   // 昇順
   return m0.getDate().compareTo(m1.getDate());
  } else {
   // 降順
   return m1.getDate().compareTo(m0.getDate());
  }
 }
}
取得したListをComparatorでソートする。
Query query = pm.newQuery(Mutter.class);
query.declareParameters("String geohashFrom, String geohashTo");
query.setFilter("geohash >= geohashFrom && geohash <= geohashTo");
List<Mutter> mutters = (List<Mutter>) query.execute(geohashFrom,geohashTo);
Collections.sort(mutters, comparator);
これで日付降順&緯度経度の絞込みができるようになった。久しぶりにComparator書いたから、ちょっと書き方忘れてたよー(^^;)。うーん、でもやっぱりクエリだけでなんとかしたいと思うんだけど、無理かなぁ?

DatastoreNeedIndexException

実行時に例外が表示されるようになった。
com.google.appengine.api.datastore.DatastoreNeedIndexException : no matching index found.

表示されるようになったのは、ソートを2列で行う記述を追加してから。
query.setOrdering("geohash ASC, date DESC");

どうやらインデックスの設定が必要らしい。

GAE/Jのサイトを見ながらdatastore-indexes.xmlを記述。
参考:Java データストアのインデックスの設定 - Google App Engine - Google Code



    
        
        
    


datastore-indexes.xmlはwar/WEB-INF下に配置。
ローカルでGAE/Jサーバーを起動すると、war/WEB-INF/appengine-generated下にdatastore-indexes-auto.xmlが自動的に作成される。
この状態になってから、サーバー側へデプロイする。
そうでないとインデックスが作成されず、DatastoreNeedIndexExceptionが解消されない。

しかし、このデプロイ時に
"Deploying xxx to Google" 中に内部エラーが発生しました。
XML error validating xxx\war\WEB-INF\datastore-indexes.xml against C:\eclipse\plugins\com.google.appengine.eclipse.sdkbundle_1.2.5.v200909021031\appengine-java-sdk-1.2.5\docs\datastore-indexes.xsd
というエラーが解決できずにかなり嵌った。

結局、上記のGAE/Jのサイトにはxmlnsが含まれていたのだが、その記述が原因らしい。xmlnsを削除したら解消された。
参考:Issue 1545 - googleappengine - xml error while validating datastore-indexes.xml while deploying to appengine with sdk 1.2.1 - Project Hosting on Google Code

なんだよおー。頼みますよー。

datastore-indexes.xmlもdatastore-indexes-auto.xmlもあって、両ファイルの記述にも違いがないのにまだDatastoreNeedIndexExceptionが出る場合は、GAE/Jの管理パネルの「Datastore」→「Indexes」で作成したインデックスのステータスが「Building」ではなく「Serving」になっているか確認してください。

ローカルでGAE/Jサーバーが動かなくなった?!

ローカルでGAE/Jサーバーを立ち上げたら、例外が・・・
2009-09-14 03:55:04.869::INFO:  Logging to STDERR via org.mortbay.log.StdErrLog
2009-09-14 03:55:04.957::INFO:  jetty-6.1.x
2009-09-14 03:55:04.159::WARN:  failed _ah_StaticFileFilter
java.lang.ClassCastException: com.google.apphosting.utils.jetty.AppEngineWebAppContext$AppEngineServletContext cannot be cast to org.mortbay.jetty.handler.ContextHandler$SContext
 at com.google.appengine.tools.development.StaticFileFilter.init(StaticFileFilter.java:47)
 at org.mortbay.jetty.servlet.FilterHolder.doStart(FilterHolder.java:99)
 at org.mortbay.component.AbstractLifeCycle.start(AbstractLifeCycle.java:40)
 at org.mortbay.jetty.servlet.ServletHandler.initialize(ServletHandler.java:589)
 at org.mortbay.jetty.servlet.Context.startContext(Context.java:139)
 at org.mortbay.jetty.webapp.WebAppContext.startContext(WebAppContext.java:1218)
 at org.mortbay.jetty.handler.ContextHandler.doStart(ContextHandler.java:500)
 at org.mortbay.jetty.webapp.WebAppContext.doStart(WebAppContext.java:448)
 at org.mortbay.component.AbstractLifeCycle.start(AbstractLifeCycle.java:40)
 at org.mortbay.jetty.handler.HandlerWrapper.doStart(HandlerWrapper.java:117)
 at org.mortbay.component.AbstractLifeCycle.start(AbstractLifeCycle.java:40)
 at org.mortbay.jetty.handler.HandlerWrapper.doStart(HandlerWrapper.java:117)
 at org.mortbay.jetty.Server.doStart(Server.java:217)
 at org.mortbay.component.AbstractLifeCycle.start(AbstractLifeCycle.java:40)
 at com.google.appengine.tools.development.JettyContainerService.startContainer(JettyContainerService.java:152)
 at com.google.appengine.tools.development.AbstractContainerService.startup(AbstractContainerService.java:116)
 at com.google.appengine.tools.development.DevAppServerImpl.start(DevAppServerImpl.java:218)
 at com.google.appengine.tools.development.DevAppServerMain$StartAction.apply(DevAppServerMain.java:162)
 at com.google.appengine.tools.util.Parser$ParseResult.applyArgs(Parser.java:48)
 at com.google.appengine.tools.development.DevAppServerMain.<init>(DevAppServerMain.java:113)
 at com.google.appengine.tools.development.DevAppServerMain.main(DevAppServerMain.java:89)
2009-09-14 03:55:04.160::WARN:  Failed startup of context com.google.apphosting.utils.jetty.DevAppEngineWebAppContext@1d8d237{/,xxx\war}
java.lang.ClassCastException: com.google.apphosting.utils.jetty.AppEngineWebAppContext$AppEngineServletContext cannot be cast to org.mortbay.jetty.handler.ContextHandler$SContext
 at com.google.appengine.tools.development.StaticFileFilter.init(StaticFileFilter.java:47)
 at org.mortbay.jetty.servlet.FilterHolder.doStart(FilterHolder.java:99)
 at org.mortbay.component.AbstractLifeCycle.start(AbstractLifeCycle.java:40)
 at org.mortbay.jetty.servlet.ServletHandler.initialize(ServletHandler.java:589)
 at org.mortbay.jetty.servlet.Context.startContext(Context.java:139)
 at org.mortbay.jetty.webapp.WebAppContext.startContext(WebAppContext.java:1218)
 at org.mortbay.jetty.handler.ContextHandler.doStart(ContextHandler.java:500)
 at org.mortbay.jetty.webapp.WebAppContext.doStart(WebAppContext.java:448)
 at org.mortbay.component.AbstractLifeCycle.start(AbstractLifeCycle.java:40)
 at org.mortbay.jetty.handler.HandlerWrapper.doStart(HandlerWrapper.java:117)
 at org.mortbay.component.AbstractLifeCycle.start(AbstractLifeCycle.java:40)
 at org.mortbay.jetty.handler.HandlerWrapper.doStart(HandlerWrapper.java:117)
 at org.mortbay.jetty.Server.doStart(Server.java:217)
 at org.mortbay.component.AbstractLifeCycle.start(AbstractLifeCycle.java:40)
 at com.google.appengine.tools.development.JettyContainerService.startContainer(JettyContainerService.java:152)
 at com.google.appengine.tools.development.AbstractContainerService.startup(AbstractContainerService.java:116)
 at com.google.appengine.tools.development.DevAppServerImpl.start(DevAppServerImpl.java:218)
 at com.google.appengine.tools.development.DevAppServerMain$StartAction.apply(DevAppServerMain.java:162)
 at com.google.appengine.tools.util.Parser$ParseResult.applyArgs(Parser.java:48)
 at com.google.appengine.tools.development.DevAppServerMain.<init>(DevAppServerMain.java:113)
 at com.google.appengine.tools.development.DevAppServerMain.main(DevAppServerMain.java:89)
2009-09-14 03:55:04.178::INFO:  Started SelectChannelConnector@127.0.0.1:8080
The server is running at http://localhost:8080/
あれ?と思いサーチしたら、なんとwar/WEB-INF/libの下にappengine-local-runtime.jarを配置しているとjettyの起動に失敗するらしい。
参考:I broke something using Eclipse plugin - Google App Engine for Java | Google グループ

ユニットテストがやりたくて、必要なjarをプロジェクト下に配置した。適当にWEB-INF/libの下に配置したが、それが問題だったのだorz。

よって、appengine-local-runtime.jarとappengine-api-stubs.jarをWEB-INF/lib下から削除し、別途WEB-INF/にjunitlibsというフォルダを作成。そのフォルダ下にappengine-local-runtime.jarとappengine-api-stubs.jarを配置した。また、ビルドパスもその場所へ設定を変更した。

そうすると
The following classpath entry 'xxx\war\WEB-INF\junitlibs\appengine-api-stubs.jar' will not be available on the server's classpath
The following classpath entry 'xxx\war\WEB-INF\junitlibs\appengine-local-runtime.jar' will not be available on the server's classpath
Eclipseの警告が2行でますが、ムシ(^^)。

appengine-api-stubs.jarは今回の問題に関係はないのですが、ユニットテストのために追加したjar2つをまとめておきたいので、appengine-local-runtime.jarと同じ場所に管理しました。

GAE/J-Blaze接続でInvocationTargetExceptionがよく起こる

クライアントのAirアプリからGAE/JのサーバーにアクセスするとよくInvocationTargetExceptionが起こる。

発生するタイミングとしては、アイドルタイムが10分以上経ったりなど、しばらくしてからサーバーへリクエストを発生した時によく起こる。というか必ず起こっている。

ログのレベルとしてはInfoレベルなのだが、Air上でも一瞬「うっ」とリクエストが止まったように見えて、なんだか気になる。
xxx.xxx.xxx.xxx - - [13/Sep/2009:18:47:23 -0700] "POST /messagebroker/amf;jsessionid=Z6NYw5g89TFfOj6cDi50aA HTTP/1.1" 200 960 "app:/xxx.swf" "Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en) AppleWebKit/526.9+ (KHTML, like Gecko) AdobeAIR/1.5,gzip(gfe)" "xxx.appspot.com"

I 09-13 06:47PM 22.601

com.google.appengine.repackaged.com.google.common.base.FinalizableReferenceQueue$SystemLoader loadFinalizer: Not allowed to access system class loader.

I 09-13 06:47PM 22.614

com.google.appengine.repackaged.com.google.common.base.internal.Finalizer getInheritableThreadLocalsField: Couldn't access Thread.inheritableThreadLocals. Reference finalizer threads will inherit thread local values.

I 09-13 06:47PM 22.616

com.google.appengine.repackaged.com.google.common.base.FinalizableReferenceQueue <init>: Failed to start reference finalizer thread. Reference cleanup will only occur when new references are created.
java.lang.reflect.InvocationTargetException
 at com.google.appengine.runtime.Request.process-1ed2a922d6cd3b28(Request.java)
 at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
 at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
 at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
 at java.lang.reflect.Method.invoke(Method.java:40)
 at com.google.appengine.repackaged.com.google.common.base.FinalizableReferenceQueue.<init>(FinalizableReferenceQueue.java:124)
 at com.google.appengine.repackaged.com.google.common.labs.misc.InterningPools$WeakInterningPool.<clinit>(InterningPools.java:104)
 at com.google.appengine.repackaged.com.google.common.labs.misc.InterningPools.newWeakInterningPool(InterningPools.java:48)
 at com.google.appengine.repackaged.com.google.io.protocol.ProtocolSupport.<clinit>(ProtocolSupport.java:55)
 at com.google.apphosting.api.DatastorePb$Query.<init>(DatastorePb.java:1072)
 at com.google.apphosting.api.DatastorePb$Query$1.<init>(DatastorePb.java:2355)
 at com.google.apphosting.api.DatastorePb$Query.<clinit>(DatastorePb.java:2355)
 at com.google.appengine.api.datastore.QueryTranslator.convertToPb(QueryTranslator.java:27)
 at com.google.appengine.api.datastore.DatastoreServiceImpl$PreparedQueryImpl.convertToPb(DatastoreServiceImpl.java:357)
 at com.google.appengine.api.datastore.DatastoreServiceImpl$PreparedQueryImpl.runQuery(DatastoreServiceImpl.java:339)
 at com.google.appengine.api.datastore.DatastoreServiceImpl$PreparedQueryImpl.access$100(DatastoreServiceImpl.java:269)
 at com.google.appengine.api.datastore.DatastoreServiceImpl$PreparedQueryImpl$1.iterator(DatastoreServiceImpl.java:303)
 at org.datanucleus.store.appengine.query.RuntimeExceptionWrappingIterable.iterator(RuntimeExceptionWrappingIterable.java:42)
 at org.datanucleus.store.appengine.query.StreamingQueryResult.<init>(StreamingQueryResult.java:77)
 at org.datanucleus.store.appengine.query.DatastoreQuery.newStreamingQueryResultForEntities(DatastoreQuery.java:324)
 at org.datanucleus.store.appengine.query.DatastoreQuery.fulfillEntityQuery(DatastoreQuery.java:310)
 at org.datanucleus.store.appengine.query.DatastoreQuery.performExecute(DatastoreQuery.java:242)
 at org.datanucleus.store.appengine.query.JDOQLQuery.performExecute(JDOQLQuery.java:84)
 at org.datanucleus.store.query.Query.executeQuery(Query.java:1489)
 at org.datanucleus.store.query.Query.executeWithArray(Query.java:1371)
 at org.datanucleus.jdo.JDOQuery.execute(JDOQuery.java:266)
 at jp.co.tricell.sonicboom.rpc.MutterUtils.getMutterAround(MutterUtils.java:84)
 at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
 at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
 at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
 at java.lang.reflect.Method.invoke(Method.java:40)
 at flex.messaging.services.remoting.adapters.JavaAdapter.invoke(JavaAdapter.java:421)
 at flex.messaging.services.RemotingService.serviceMessage(RemotingService.java:183)
 at flex.messaging.MessageBroker.routeMessageToService(MessageBroker.java:1503)
 at flex.messaging.endpoints.AbstractEndpoint.serviceMessage(AbstractEndpoint.java:884)
 at flex.messaging.endpoints.amf.MessageBrokerFilter.invoke(MessageBrokerFilter.java:121)
 at flex.messaging.endpoints.amf.LegacyFilter.invoke(LegacyFilter.java:158)
 at flex.messaging.endpoints.amf.SessionFilter.invoke(SessionFilter.java:44)
 at flex.messaging.endpoints.amf.BatchProcessFilter.invoke(BatchProcessFilter.java:67)
 at flex.messaging.endpoints.amf.SerializationFilter.invoke(SerializationFilter.java:146)
 at flex.messaging.endpoints.BaseHTTPEndpoint.service(BaseHTTPEndpoint.java:278)
 at flex.messaging.MessageBrokerServlet.service(MessageBrokerServlet.java:322)
 at javax.servlet.http.HttpServlet.service(HttpServlet.java:806)
 at org.mortbay.jetty.servlet.ServletHolder.handle(ServletHolder.java:487)
 at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1093)
 at com.google.apphosting.runtime.jetty.SaveSessionFilter.doFilter(SaveSessionFilter.java:35)
 at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1084)
 at com.google.apphosting.utils.servlet.TransactionCleanupFilter.doFilter(TransactionCleanupFilter.java:43)
 at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1084)
 at org.mortbay.jetty.servlet.ServletHandler.handle(ServletHandler.java:360)
 at org.mortbay.jetty.security.SecurityHandler.handle(SecurityHandler.java:216)
 at org.mortbay.jetty.servlet.SessionHandler.handle(SessionHandler.java:181)
 at org.mortbay.jetty.handler.ContextHandler.handle(ContextHandler.java:712)
 at org.mortbay.jetty.webapp.WebAppContext.handle(WebAppContext.java:405)
 at com.google.apphosting.runtime.jetty.AppVersionHandlerMap.handle(AppVersionHandlerMap.java:237)
 at org.mortbay.jetty.handler.HandlerWrapper.handle(HandlerWrapper.java:139)
 at org.mortbay.jetty.Server.handle(Server.java:313)
 at org.mortbay.jetty.HttpConnection.handleRequest(HttpConnection.java:506)
 at org.mortbay.jetty.HttpConnection$RequestHandler.headerComplete(HttpConnection.java:830)
 at com.google.apphosting.runtime.jetty.RpcRequestParser.parseAvailable(RpcRequestParser.java:76)
 at org.mortbay.jetty.HttpConnection.handle(HttpConnection.java:381)
 at com.google.apphosting.runtime.jetty.JettyServletEngineAdapter.serviceRequest(JettyServletEngineAdapter.java:139)
 at com.google.apphosting.runtime.JavaRuntime.handleRequest(JavaRuntime.java:235)
 at com.google.apphosting.base.RuntimePb$EvaluationRuntime$6.handleBlockingRequest(RuntimePb.java:4950)
 at com.google.apphosting.base.RuntimePb$EvaluationRuntime$6.handleBlockingRequest(RuntimePb.java:4948)
 at com.google.net.rpc.impl.BlockingApplicationHandler.handleRequest(BlockingApplicationHandler.java:24)
 at com.google.net.rpc.impl.RpcUtil.runRpcInApplication(RpcUtil.java:359)
 at com.google.net.rpc.impl.Server$2.run(Server.java:823)
 at com.google.tracing.LocalTraceSpanRunnable.run(LocalTraceSpanRunnable.java:56)
 at com.google.tracing.LocalTraceSpanBuilder.internalContinueSpan(LocalTraceSpanBuilder.java:516)
 at com.google.net.rpc.impl.Server.startRpc(Server.java:778)
 at com.google.net.rpc.impl.Server.processRequest(Server.java:351)
 at com.google.net.rpc.impl.ServerConnection.messageReceived(ServerConnection.java:437)
 at com.google.net.rpc.impl.RpcConnection.parseMessages(RpcConnection.java:319)
 at com.google.net.rpc.impl.RpcConnection.dataReceived(RpcConnection.java:290)
 at com.google.net.async.Connection.handleReadEvent(Connection.java:428)
 at com.google.net.async.EventDispatcher.processNetworkEvents(EventDispatcher.java:762)
 at com.google.net.async.EventDispatcher.internalLoop(EventDispatcher.java:207)
 at com.google.net.async.EventDispatcher.loop(EventDispatcher.java:101)
 at com.google.net.rpc.RpcService.runUntilServerShutdown(RpcService.java:251)
 at com.google.apphosting.runtime.JavaRuntime$RpcRunnable.run(JavaRuntime.java:392)
 at java.lang.Thread.run(Unknown Source)
Caused by: java.security.AccessControlException: access denied (java.lang.RuntimePermission modifyThreadGroup)
 at java.security.AccessControlContext.checkPermission(Unknown Source)
 at java.security.AccessController.checkPermission(Unknown Source)
 at java.lang.SecurityManager.checkPermission(Unknown Source)
 at java.lang.ThreadGroup.checkAccess(Unknown Source)
 at java.lang.Thread.init(Unknown Source)
 at java.lang.Thread.<init>(Unknown Source)
 at com.google.appengine.repackaged.com.google.common.base.internal.Finalizer.<init>(Finalizer.java:96)
 at com.google.appengine.repackaged.com.google.common.base.internal.Finalizer.startFinalizer(Finalizer.java:82)
 ... 81 more
上記のログには3種類のInfoレベルのログが含まれており、最後の3つ目のInfoレベルのものがInvocationTargetExceptionなのだが、この3種類のものが必ずアイドルタイム発生後に発生する。

InvocationTargetExceptionは、JavaDocによると「呼び出されるメソッドまたはコンストラクタがスローする例外をラップする、チェック済み例外」ということ。問題なのはログの下の方にある、原因となった「Caused by~」のほうだが、securityのAccessControlExceptionということで、これはGAE/J上でスレッドなどを走らせようとしたときによく出る例外だったなぁと思いだす。

Airから接続して前回のセッションを呼び出そうとした時にスレッドを使おうとしてExceptionが発生しているのかもしれない。もともとBlazeDSのRemoting Objectもコードを改変してムリクリ動かしているわけで(^^;)。

動作上は、ちょっとリクエストがつっかえたように見えるというくらいで、その他は期待したとおりの動作が出来ているので問題はないのだが・・・。

毎回このログが出ないようにできれば改善したい。どうにかできんもんかのー。

2009年9月12日土曜日

Only one inequality filter per query is supported

GAE/J上では複数のプロパティーに対して不等式フィルタが使えないということが分かった(┳◇┳)
参考:サンフランシスコの 24 時間: Geolocation App - Google App Engine - Google Code
App Engine のクエリには固有の制約があり、複数のプロパティ(経度と緯度など)に対して不等式フィルタを実行できないからです。
ノーorz。
まさにこの、”緯度経度で絞り込む”ではまっていたが、結局ずっと例外が出て・・・。
NestedThrowablesStackTrace:
java.lang.IllegalArgumentException: Only one inequality filter per query is supported.  Encountered both latitude and longitude
当初、コードは以下のように書いていた。
Query query = pm.newQuery(Mutter.class);
query.declareParameters("double latitudeFrom,  double latitudeTo, double longitudeFrom,double longitudeTo");
query.setFilter("latitude >= latitudeFrom && latitude <= latitudeTo && longitude >= longitudeFrom && longitude <= longitudeTo");
query.setOrdering("date Desc");
List<Mutter> mutters = (List<Mutter>) query.execute(latitudeFrom, latitudeTo, longitudeFrom, longitudeTo);
pm.close();
でもこれだと、コンパイルエラーになる。理由はQueryのexecuteメソッドは引数3つまでのメソッドしかないから。

よって、以下のようにコードを変えてみた。
Query query = pm.newQuery(Mutter.class);
query.declareParameters("double latitudeFrom,  double latitudeTo, double longitudeFrom,double longitudeTo");
query.setFilter("latitude >= latitudeFrom && latitude <= latitudeTo && longitude >= longitudeFrom && longitude <= longitudeTo");
query.setOrdering("date Desc");
Object[] paramArray = { latitudeFrom, latitudeTo, longitudeFrom,longitudeTo };
List<Mutter> mutters = (List<Mutter>) query.executeWithArray(paramArray);
pm.close();
すると、上記のように「Only one inequality filter per query is supported.」が出て嵌る。 executeWithArrayメソッドの使い方がまずいのかと思い(結局これは見当違いだったが)、じかにクエリーを書く形式に変えてみる。
// SQL作成
String query = "SELECT FROM " + Mutter.class.getName()
  + " WHERE latitude >= " + latitudeFrom + " && latitude <= "
  + latitudeTo + " && longitude >= " + longitudeFrom
  + " && longitude <= " + longitudeTo + " ORDER BY date DESC";

PersistenceManager pm = PMF.get().getPersistenceManager();
List<Mutter> mutters = (List<Mutter>) pm.newQuery(query).execute();
pm.close();
でも相変わらず「Only one inequality filter per query is supported.」

で、結局冒頭に書いたとおり、それはGAE/Jでの制約だったのだ。

上記サイトで触れてあった、地図の境界ボックスを計算するオープンソースライブラリを見てみるもののPythonで書かれていたので避け、もう一方の方法Geohashを使うやり方でやってみた。

Geohashは緯度と経度の位置データから1つの文字列を生成する方法。どのような文字列が生成されるのかは、以下のサイトでテキストボックスに適当な緯度と経度を入力して確認できる。例えば「35.438399 139.361765」を入力すると神奈川県厚木市のイトーヨーカドーのGeohashが求められる。
参考:Geohash - geohash.org

GAE/Jのサイトでは、「ただし、この手法には、特に世界の特定地域において限界があります。」と書いてあったのが気になるが、とりあえず採用。Geohashのエンコードとデコードが書かれたJavaのライブラリが既にあったので、それを使ってみることにする。この辺はこちらを参考に。
参考:琴線探査: Geohashだなぁ
public boolean addMutter(String userName, double latitude,
  double longitude, String message, int boomColor, int radius) {
 boolean ok = false;

 // 緯度経度をGeohashでエンコード
 String geohash = Geohash.encode(latitude, longitude);

 Mutter mutter = new Mutter(userName, latitude, longitude, geohash,
   new Date().getTime(), message, boomColor, radius);

 PersistenceManager pm = PMF.get().getPersistenceManager();

 try {
  pm.makePersistent(mutter);
  ok = true;
 } finally {
  pm.close();
 }

 return ok;
}

public List getMutterAround(double latitudeFrom,
  double longitudeFrom, double latitudeTo, double longitudeTo) {

 // geoboxの緯度経度(南西から北東)をGeohashにエンコード
 String geohashFrom = Geohash.encode(latitudeFrom, longitudeFrom);
 String geohashTo = Geohash.encode(latitudeTo, longitudeTo);

 PersistenceManager pm = PMF.get().getPersistenceManager();

 Query query = pm.newQuery(Mutter.class);
 query.declareParameters("String geohashFrom, String geohashTo");
 query.setFilter("geohash >= geohashFrom && geohash <= geohashTo");
 query.setOrdering("geohash ASC");
 List<Mutter> mutters = (List<Mutter>) query.execute(geohashFrom,
   geohashTo);
 log.info("緯度" + latitudeFrom + "から" + latitudeTo + ", 経度"
   + longitudeFrom + "から" + longitudeTo + "までのデータは"
   + mutters.size() + "件です");

 pm.close();

 return mutters;
}
結果は成功。Geohashを使って、不等式を使わずに緯度経度での絞り込みが出来ました♪

Object Manager has been closed

GAE/Jベースで、検索するメソッドのテストケースを書いていたら実行時に以下の例外が出た。
Object Manager has been closed
org.datanucleus.exceptions.NucleusUserException: Object Manager has been closed
 at org.datanucleus.ObjectManagerImpl.assertIsOpen(ObjectManagerImpl.java:3876)
 at org.datanucleus.ObjectManagerImpl.getFetchPlan(ObjectManagerImpl.java:376)
 at org.datanucleus.store.query.Query.getFetchPlan(Query.java:497)
 at org.datanucleus.store.appengine.query.DatastoreQuery$5.apply(DatastoreQuery.java:508)
 at org.datanucleus.store.appengine.query.DatastoreQuery$5.apply(DatastoreQuery.java:507)
 at org.datanucleus.store.appengine.query.StreamingQueryResult.resolveNext(StreamingQueryResult.java:137)
 at org.datanucleus.store.appengine.query.StreamingQueryResult$1.computeNext(StreamingQueryResult.java:163)
 at org.datanucleus.store.appengine.query.AbstractIterator.tryToComputeNext(AbstractIterator.java:132)
 at org.datanucleus.store.appengine.query.AbstractIterator.hasNext(AbstractIterator.java:127)
 at org.datanucleus.store.appengine.query.StreamingQueryResult$AbstractListIterator.hasNext(StreamingQueryResult.java:229)
 at jp.co.tricell.sonicboom.rpc.test.TestMutterUtils.testGetMutterOf(TestMutterUtils.java:44)
 at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
 at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
 at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
 at java.lang.reflect.Method.invoke(Method.java:597)
 at junit.framework.TestCase.runTest(TestCase.java:164)
 at junit.framework.TestCase.runBare(TestCase.java:130)
 at junit.framework.TestResult$1.protect(TestResult.java:106)
 at junit.framework.TestResult.runProtected(TestResult.java:124)
 at junit.framework.TestResult.run(TestResult.java:109)
 at junit.framework.TestCase.run(TestCase.java:120)
 at junit.framework.TestSuite.runTest(TestSuite.java:230)
 at junit.framework.TestSuite.run(TestSuite.java:225)
 at org.eclipse.jdt.internal.junit.runner.junit3.JUnit3TestReference.run(JUnit3TestReference.java:130)
 at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
 at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:460)
 at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:673)
 at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:386)
 at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:196)
むぅーと思っていたら、ひがさんのブログでちょうど参考になる記事発見。
参考:JDOのモデルの状態を理解しよう - ひがやすを blog

テスト元メソッドは以下。つまりPersistenceManagerをclose()したのに、テストケースの方でクエリの結果を触ろうとしていたので「Object Manager has been closed」が出たということ。
// クラスのアノテーションを記述
@PersistenceCapable(identityType = IdentityType.APPLICATION)
public class Mutter {
・・・・・
}

public class MutterUtils {
・・・・・

 public List<Mutter> getMutterOf(String myKey) {
  PersistenceManager pm = PMF.get().getPersistenceManager();

  Query query = pm.newQuery(Mutter.class);
  query.declareParameters("String myKey");
  query.setFilter("userName == myKey");
  query.setOrdering("date DESC");
  List<Mutter> mutters = (List<Mutter>) query.execute(myKey);

  pm.close();

  return mutters;
 }
}
ひがさんの記事を参考に、クラスのアノテーションに太字部分を追加。
@PersistenceCapable(identityType = IdentityType.APPLICATION, detachable="true")。

これでPersistenceManagerをclose()した後でも触れるdetachedなモデルになった。

さらに、PersistenceManager#detachCopy()やPersistenceManager#detachCopyAll()を呼ぶのが面倒なので、jdoconfig.xmlに以下のpropertyタグを追加。
<property name="datanucleus.DetachOnClose" value="true"/>

あわせて、JDOのモデルには以下の4パターン
  • transient
  • persistent
  • detached
  • hollow
があるということも学んだゾ。

Google App EngineでUnit Test

GAE/JでUnit Testするには、いくつか設定が必要。設定方法は、ちゃんとGAE/Jのページにまとめてあります。素晴らしい。
参考:Unit Testing With Local Service Implementations - Google App Engine - Google Code

1.テスト実行環境を作る

まずは、appengine-api-stubs.jarappengine-local-runtime.jarへクラスパスを通す。これらはappengineのsdkをダウンロードしたフォルダのlib/implの下にあります。 分かり易いように、GAE/Jのプロジェクトのwar/WEB-INF/lib下にこれらのjarをコピー。プロジェクトのプロパティーから「Javaのビルドパス」を選び「JARの追加」でクラスパスを通します。
2009.09.14追記:appengine-local-runtime.jarをWEB-INF/lib下に配置しておくと、ローカルでのGAE/Jサーバーの起動に失敗します。jarはlib以外に配置してください。詳しくは「ローカルでGAEサーバーが動かなくなった?!

その後、上記ページを参考にして(というか、そっくりコピペして)、TestEnvironmentクラスを作成。
import com.google.apphosting.api.ApiProxy;

import java.util.HashMap;
import java.util.Map;

class TestEnvironment implements ApiProxy.Environment {
 public String getAppId() {
  return "test";
 }

 public String getVersionId() {
  return "1.0";
 }

 public String getEmail() {
  throw new UnsupportedOperationException();
 }

 public boolean isLoggedIn() {
  throw new UnsupportedOperationException();
 }

 public boolean isAdmin() {
  throw new UnsupportedOperationException();
 }

 public String getAuthDomain() {
  throw new UnsupportedOperationException();
 }

 public String getRequestNamespace() {
  return "";
 }

 public Map<String, Object> getAttributes() {
  return new HashMap<String, Object>();
 }

}

2.基本クラスを作成

1で作成したTestEnvironmentを継承してLocalServiceTestCaseクラスを作る。ローカルサービスを簡単にテストできるようにするための基本クラス。
import java.io.File;

import com.google.appengine.tools.development.ApiProxyLocalImpl;
import com.google.apphosting.api.ApiProxy;

import junit.framework.TestCase;

public class LocalServiceTestCase extends TestCase {

 @Override
 public void setUp() throws Exception {
  super.setUp();
  ApiProxy.setEnvironmentForCurrentThread(new TestEnvironment());
  ApiProxy.setDelegate(new ApiProxyLocalImpl(new File(".")) {
  });
 }

 @Override
 public void tearDown() throws Exception {
  // not strictly necessary to null these out but there's no harm either
  ApiProxy.setDelegate(null);
  ApiProxy.setEnvironmentForCurrentThread(null);
  super.tearDown();
 }

}

3.Datastore Testのための拡張クラス作成

今回、テスト目的がデータストアのチェックだったので、データストアの中身をチェックする時、毎回データをクリーンにしてからテストが実行できるように、LocalServiceTestCaseを継承した以下のようなLocalDatastoreTestCaseクラスも作成。
import com.google.appengine.api.datastore.dev.LocalDatastoreService;
import com.google.appengine.tools.development.ApiProxyLocalImpl;
import com.google.apphosting.api.ApiProxy;

public class LocalDatastoreTestCase extends LocalServiceTestCase {

 @Override
 public void setUp() throws Exception {
  super.setUp();
  ApiProxyLocalImpl proxy = (ApiProxyLocalImpl) ApiProxy.getDelegate();
  proxy.setProperty(LocalDatastoreService.NO_STORAGE_PROPERTY,
    Boolean.TRUE.toString());
 }

 @Override
 public void tearDown() throws Exception {
  ApiProxyLocalImpl proxy = (ApiProxyLocalImpl) ApiProxy.getDelegate();
  LocalDatastoreService datastoreService = (LocalDatastoreService) proxy
    .getService("datastore_v3");
  datastoreService.clearProfiles();
  super.tearDown();
 }

}

4.TestCaseを書いてみよう

ここまで終わってようやく自分のテストケースを実行できるようになりました。早速テストケースを書いて実行してみます。3で作ったLocalDatastoreTestCaseを継承して作ります。
import com.google.appengine.api.datastore.DatastoreServiceFactory;
import com.google.appengine.api.datastore.Query;

import hoge.Mutter;
import hoge.rpc.MutterUtils;
import hoge.test.LocalDatastoreTestCase;

public class TestMutterUtils extends LocalDatastoreTestCase {

 public void testAdd() {
  MutterUtils mu = new MutterUtils();

  //追加処理

  //追加後データ件数が1件増えていることを確認
  Query query = new Query(Mutter.class.getSimpleName());
  assertEquals(1, DatastoreServiceFactory.getDatastoreService().prepare(
    query).countEntities());
 }

 public void testGetMutterOf() {
  MutterUtils mu = new MutterUtils();

  //追加処理

  //追加後データ件数が1件増えていることを確認
  List<Mutter> mutters = mu.getMutterOf(userName);
  Query query = new Query(Mutter.class.getSimpleName());
  assertEquals(1, DatastoreServiceFactory.getDatastoreService().prepare(
    query).countEntities());

  //追加後のデータを検証
  for (Mutter mutter : mutters) {
   assertEquals("期待した文字列", mutter.getMessage());
  }

 }

}
テストケースを右クリックして「実行」→「JUnitテスト」をクリックしてテストを実行します。

やっぱりローカルでテストできると安心ですねぇ。

2009年9月10日木曜日

BigTableについてのビデオとスライド

BigTableの内部構造について勉強になった。
ちょっと長いけど見るに値する。



スライドは以下から見れる。
Under the covers of the App Engine Datastore by Ryan Barrett

2009年9月9日水曜日

Google App EngineでStreamingAMFChannelが使えたかとぬかった

昨日の記事に書いた以下の例外メッセージを見ていて、要はBaseStreamingHTTPEndpoint.javaの833行目にあるコード「currentThread.setName(threadName);」、つまりスレッドにsetNameしている部分をコメントしてみたら解決するのではないかということに。
2009/09/08 4:57:25 com.google.apphosting.utils.jetty.JettyLogger warn
警告: /messagebroker/streamingamf
java.security.AccessControlException: access denied (java.lang.RuntimePermission modifyThread)
 at java.security.AccessControlContext.checkPermission(AccessControlContext.java:323)
 at java.security.AccessController.checkPermission(AccessController.java:546)
 at java.lang.SecurityManager.checkPermission(SecurityManager.java:532)
 at com.google.appengine.tools.development.DevAppServerFactory$CustomSecurityManager.checkPermission(DevAppServerFactory.java:139)
 at com.google.appengine.tools.development.DevAppServerFactory$CustomSecurityManager.checkAccess(DevAppServerFactory.java:178)
 at java.lang.Thread.checkAccess(Thread.java:1263)
 at java.lang.Thread.setName(Thread.java:1050)
 at flex.messaging.endpoints.BaseStreamingHTTPEndpoint.handleFlexClientStreamingOpenRequest(BaseStreamingHTTPEndpoint.java:833)
 at flex.messaging.endpoints.BaseStreamingHTTPEndpoint.serviceStreamingRequest(BaseStreamingHTTPEndpoint.java:1022)
 at flex.messaging.endpoints.BaseStreamingHTTPEndpoint.service(BaseStreamingHTTPEndpoint.java:430)
 at flex.messaging.MessageBrokerServlet.service(MessageBrokerServlet.java:322)
 at javax.servlet.http.HttpServlet.service(HttpServlet.java:806)
 at org.mortbay.jetty.servlet.ServletHolder.handle(ServletHolder.java:487)
 at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1093)
 at com.google.apphosting.utils.servlet.TransactionCleanupFilter.doFilter(TransactionCleanupFilter.java:43)
 at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1084)
 at com.google.appengine.tools.development.StaticFileFilter.doFilter(StaticFileFilter.java:121)
 at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1084)
 at org.mortbay.jetty.servlet.ServletHandler.handle(ServletHandler.java:360)
 at org.mortbay.jetty.security.SecurityHandler.handle(SecurityHandler.java:216)
 at org.mortbay.jetty.servlet.SessionHandler.handle(SessionHandler.java:181)
 at org.mortbay.jetty.handler.ContextHandler.handle(ContextHandler.java:712)
 at org.mortbay.jetty.webapp.WebAppContext.handle(WebAppContext.java:405)
 at com.google.apphosting.utils.jetty.DevAppEngineWebAppContext.handle(DevAppEngineWebAppContext.java:54)
 at org.mortbay.jetty.handler.HandlerWrapper.handle(HandlerWrapper.java:139)
 at com.google.appengine.tools.development.JettyContainerService$ApiProxyHandler.handle(JettyContainerService.java:313)
 at org.mortbay.jetty.handler.HandlerWrapper.handle(HandlerWrapper.java:139)
 at org.mortbay.jetty.Server.handle(Server.java:313)
 at org.mortbay.jetty.HttpConnection.handleRequest(HttpConnection.java:506)
 at org.mortbay.jetty.HttpConnection$RequestHandler.content(HttpConnection.java:844)
 at org.mortbay.jetty.HttpParser.parseNext(HttpParser.java:644)
 at org.mortbay.jetty.HttpParser.parseAvailable(HttpParser.java:211)
 at org.mortbay.jetty.HttpConnection.handle(HttpConnection.java:381)
 at org.mortbay.io.nio.SelectChannelEndPoint.run(SelectChannelEndPoint.java:396)
 at org.mortbay.thread.BoundedThreadPool$PoolThread.run(BoundedThreadPool.java:442)
コメントしてビルドしなおし、再びStreaimingAMFChannelを使って接続を試してみると・・・

java.security.AccessControlExceptionが出ない(>w<)!
やった!GAE/Jのセキュリティサンドボックスにひっかからずにいけたみたい!

ローカルのGAE/Jのプロジェクトで、StreamingAMFChannelを使って各種ブラウザとチャット通信を試してみる。IE8、Firefox、Chrome、Safari、Opera、Sleipnir、Lunascape5など。

IE系エンジンを使ったブラウザでは、consumer.subscribe();しても反応が無く、再読み込みして再度subscribeすると
[BlazeDS]Endpoint with id 'my-streaming-amf' cannot grant streaming connection to FlexClient with id 'F1B77645-EE1C-F1B8-66AA-7714CC07D614' because max-streaming-connections-per-session limit of '1' has been reached.
とエラーが出て、Consumerの接続に失敗する。IEならではのセッション周りの問題かと思い、BlazeDSのドキュメントを参考に、services-config.xmlのStreamingAMFChannelの設定にpropertiesを追加する。
        
            
            
              
                
                
              
            
        
IE系エンジンでも無事にsubscribe出来るようになった。

う~ん、さすがStreaming。文字列表示がサックサク。pollingの場合は文字列が表示されるまで一呼吸ある感じがモッサリして見えるがStreamingはそんなことはない。やっぱりStreamingの方がNearリアルタイム通信だなぁとルンルンでGAE/J上にデプロイしてみると・・・

動かない・・・orz

ぬかった。ぬかった。完全にぬか喜びだった。ノー。ConsumerもProducerもchannelFaultが発生。GAE/Jの管理パネルからログを見てみると、
[<GAE/Jアプリケーション名>/1.336207119101686921].: [BlazeDS]Endpoint with id 'my-streaming-amf' cannot service the streaming request as either the supplied FlexClient id 'F2421357-2D0B-B9BA-A2B2-6F5794E9859B is not valid, or the FlexClient with that id is not valid.
というログが残っていた。このエラーメッセージを表示していたflex.messaging.endpoints.BaseStreamingHTTPEndpoint.javaの1005行目から1019行目までをコメントしてみたらどうかということに(^^;ソースいじり放題だな)
/*if (!command.equals(CLOSE_COMMAND) && !validFlexClientId)
        {
            if (Log.isError())
                log.error("Endpoint with id '" + getId() + "' cannot service the streaming request as either the supplied"
                        + " FlexClient id '" + flexClientId + " is not valid, or the FlexClient with that id is not valid.");

            try
            {
                // Return an HTTP status code 400 to indicate that the client's request was syntactically invalid (invalid id).
                res.sendError(HttpServletResponse.SC_BAD_REQUEST);
            }
            catch (IOException ignore)
            {}
            return; // Abort further server processing.
        }*/
またビルドしなおしてローカルで動作確認→OK!→GAE/Jにデプロイ→GAE/J上で動作確認→失敗orz

ログにエラーは出なくなったが、Consumerにsubscribeしてもウンともスンとも言わない。もちろんチャットのメッセージを書いて送っても何のリアクションもない。動かない。。。


つまりはこれがクラウドや分散環境ということかもしれない。

Streamingのように静的に特定のサーバーとコネクションをはり続けるようなアプリケーションは構造的にムリというか向かないということだろう。1つのクライアントからのセッション情報が重複してしまうという最初のトラブルからそもそもそういうことだったのだ。

セッションがGAE/Jでは使えないということではないが、特定のサーバーとずっと通信するのではなく、分散環境で、どのサーバーと通信されているのか分からないし知らなくてもいいようなクラウドの世界では、セッションメインのWebアプリケーションの作成手法は一旦頭から追い出さないといけない。自分でサーバーもメンテし運用していると、そういう従来のクセがついつい出てしまう。

やってみてわかることもあるものだ。

2009年9月8日火曜日

Google App EngineでBlazeDSのLong Polling機能を試す

結論から言います。
できませんでした。orz

アドレスへアクセスして動作を確認しようとすると、一瞬、接続が確立されたように見えるのですが、その後すぐにConsumerがchannelFaultイベントをはき、ProducerもchannelFaultイベントをはく。そしてまた一瞬ConsumerがchannelConnectするものの、すぐにchannelFaultイベントをはくという動作になりました。結局接続が安定的に確立できず・・・

前回のMessagingのテスト状況からの変更点は以下。

1.WEB-INF/flex/services-config.xmlのchannelsにLongPollのチャンネルを追加
        
          
          
            true
            0
            60000
            3000
            100
          
        
BlazeDSのドキュメントによると、polling-interval-secondsタグではなくpolling-interval-millisと書いてある部分もあるので、どちらも試してみました。動作は変わりませんでした。

2.WEB-INF/flex/messaging-config.xmlのdefault-channelsにLongPollのチャンネルを追加
    
        
        
    
3. mxmlのChannelSetを変更
      private function onClickButton():void{
       
       consumer.unsubscribe();
       
       if(local.selected){
         pollAmfChannel.url="http://127.0.0.1:8080/messagebroker/amfpolling";
         longPollAmfChannel.url="http://127.0.0.1:8080/messagebroker/myamflongpoll";
       }else{
         pollAmfChannel.url = "http://xxx.appspot.com/messagebroker/amfpolling";
         longPollAmfChannel.url = "http://xxx.appspot.com/messagebroker/myamflongpoll";
       }
       
       consumer.subscribe();
      }
    ]]>
  
  
  
    
  

  
    
  

 
 
ConsumerもProducerもchannelSetの値を{pollChannelSet}にすると、前回の状況と同じで、単純にpollingを使ったMessagingになります。それを、どちらも{longPollChannelSet}を指定したり、Consumerのみ{longPollChannelSet}にしたりしましたが、結局、安定的に接続が確立されませんでした。

Comet的な処理はGAE/Jではできないという記事もあり、Long Pollingは今のところGAE/Jでは動作しないのかもしれません。Pullではなく、Push的な動作をGAE/J上で実現するには、何か別の手段を考えなければならない。

Google App EngineでBlazeDSのMessagingを試す

GAE上でBlazeのROは使えることは確認したが、Messagingを使ってチャットのようなNearリアルタイム通信は可能なのか試してみる。Messagingといっても、StreamingAMFChannelを使ったものや、AMFChannelのpollingを使ったものがあるが、とりあえずはStreamingAMFChannelが使えるかチェックしてみる。

[環境]

Remotingを試した時と変わっていません。
  • OS : Windows Vista SP2
  • IDE : Eclipse v3.4.2(Ganymade)
  • SDK : appengine-sdk-1.2.5/JDK1.6.0_07/Flex Builder3.2.0
  • ライブラリ : BlazeDS3.2.0.3978

[サーバーサイド]

1.WEB-INF/flex/messaging-config.xmlにdestinationとしてid=chatを追加

GAEのプロジェクト配下にあるWEB-INF/flex/messaging-config.xmlにdestinationを追加。これがmxml側で指定するdestination先になる。


    
        
        
    

    
        
    
    
    



2.WEB-INF/flex/services-config.xmlのchannelsにStreamingAMFChannelが設定されていることを確認
    

        
            
        

    

[クライアントサイド]

1.mxmlを作成

ローカルとGAEへデプロイした時と接続するチャンネルのパスが変わるので、ラジオボタンで簡単に接続先を設定できるようにしています。BlazeDSのサンプルにあるtestdrive-chatをベースにしています。

  
  
    
  
  
    


 
 

  
    
    
    
  

  
    
    
       
       
    
  
  


2.swfなどのコンテンツをGAEのプロジェクトへ配置

swf、html、AC_OETags.js、playerProductInstall.swfをGAEプロジェクト下のwarに配置。history機能を使う場合はhistoryフォルダもwarに配置する。


[動作確認]

ローカル上で動作確認。Streaming通信に
失敗・・・ノーorz
2009/09/08 4:57:25 com.google.apphosting.utils.jetty.JettyLogger warn
警告: /messagebroker/streamingamf
java.security.AccessControlException: access denied (java.lang.RuntimePermission modifyThread)
 at java.security.AccessControlContext.checkPermission(AccessControlContext.java:323)
 at java.security.AccessController.checkPermission(AccessController.java:546)
 at java.lang.SecurityManager.checkPermission(SecurityManager.java:532)
 at com.google.appengine.tools.development.DevAppServerFactory$CustomSecurityManager.checkPermission(DevAppServerFactory.java:139)
 at com.google.appengine.tools.development.DevAppServerFactory$CustomSecurityManager.checkAccess(DevAppServerFactory.java:178)
 at java.lang.Thread.checkAccess(Thread.java:1263)
 at java.lang.Thread.setName(Thread.java:1050)
 at flex.messaging.endpoints.BaseStreamingHTTPEndpoint.handleFlexClientStreamingOpenRequest(BaseStreamingHTTPEndpoint.java:833)
 at flex.messaging.endpoints.BaseStreamingHTTPEndpoint.serviceStreamingRequest(BaseStreamingHTTPEndpoint.java:1022)
 at flex.messaging.endpoints.BaseStreamingHTTPEndpoint.service(BaseStreamingHTTPEndpoint.java:430)
 at flex.messaging.MessageBrokerServlet.service(MessageBrokerServlet.java:322)
 at javax.servlet.http.HttpServlet.service(HttpServlet.java:806)
 at org.mortbay.jetty.servlet.ServletHolder.handle(ServletHolder.java:487)
 at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1093)
 at com.google.apphosting.utils.servlet.TransactionCleanupFilter.doFilter(TransactionCleanupFilter.java:43)
 at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1084)
 at com.google.appengine.tools.development.StaticFileFilter.doFilter(StaticFileFilter.java:121)
 at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1084)
 at org.mortbay.jetty.servlet.ServletHandler.handle(ServletHandler.java:360)
 at org.mortbay.jetty.security.SecurityHandler.handle(SecurityHandler.java:216)
 at org.mortbay.jetty.servlet.SessionHandler.handle(SessionHandler.java:181)
 at org.mortbay.jetty.handler.ContextHandler.handle(ContextHandler.java:712)
 at org.mortbay.jetty.webapp.WebAppContext.handle(WebAppContext.java:405)
 at com.google.apphosting.utils.jetty.DevAppEngineWebAppContext.handle(DevAppEngineWebAppContext.java:54)
 at org.mortbay.jetty.handler.HandlerWrapper.handle(HandlerWrapper.java:139)
 at com.google.appengine.tools.development.JettyContainerService$ApiProxyHandler.handle(JettyContainerService.java:313)
 at org.mortbay.jetty.handler.HandlerWrapper.handle(HandlerWrapper.java:139)
 at org.mortbay.jetty.Server.handle(Server.java:313)
 at org.mortbay.jetty.HttpConnection.handleRequest(HttpConnection.java:506)
 at org.mortbay.jetty.HttpConnection$RequestHandler.content(HttpConnection.java:844)
 at org.mortbay.jetty.HttpParser.parseNext(HttpParser.java:644)
 at org.mortbay.jetty.HttpParser.parseAvailable(HttpParser.java:211)
 at org.mortbay.jetty.HttpConnection.handle(HttpConnection.java:381)
 at org.mortbay.io.nio.SelectChannelEndPoint.run(SelectChannelEndPoint.java:396)
 at org.mortbay.thread.BoundedThreadPool$PoolThread.run(BoundedThreadPool.java:442)
GAEのセキュリティサンドボックスにひっかかってしまったようだ。よって、Martinの記事を参考に、StreamingAMFChannelではなく、AMFChannelのpollingを使って動作チェックしてみる。
参考:Martin's Blog: AppEngine & BlazeDS (Messaging)


[エラー解決までの道のり]

1.mxmlでStreamingAMFChannelを使っていた箇所をAMFChannelのpollingを使った記述へ変更。以下、変更部部分。
      private function onClickButton():void{
       
       consumer.unsubscribe();
       
       if(local.selected){
        amfchannel.url="http://127.0.0.1:8080/messagebroker/amfpolling";
       }else{
        amfchannel.url = "http://xxx.appspot.com/messagebroker/amfpolling";
       }
       
       consumer.subscribe();
      }
    ]]>
  
  
  
    
  

2.WEB-INF/flex/services-config.xmlのchannelsにAMFChannelが設定されていることを確認
2秒おきにポーリングするようにしてみました。
    

        
            
            
                true
                2
            
        

    


3.通信テスト
ローカル上で通信が確立することを確認!イェイ。
GAE上にデプロイして動作確認。
通信成功!やたっ!

結局、polling使わないとGAEではダメだった。今度はLong Pollingを試してみよう。

2009年9月7日月曜日

Google App EngineでBlazeDSのRemoting通信を試す

Google App Engine(以下GAE/J)でBlazeDSのRemotingObjectを拡張したリモーティング通信を試してみる。

[環境]
  • OS : Windows Vista SP2
  • IDE : Eclipse v3.4.2(Ganymade)
  • SDK : appengine-sdk-1.2.5/JDK1.6.0_07/Flex Builder3.2.0
  • ライブラリ : BlazeDS3.2.0.3978

[サーバーサイド]

1.Eclipse上でWeb Application Projectとして新規プロジェクトを作成

2.1で作ったプロジェクト下にBlazeDS用のライブラリや設定ファイルをコピー

デプロイ図
BlazeDSをダウンロードして展開するとblazeds.warがある。それが基本的にBlazeDSの動作に最低限必要なコンポーネントになっている。

なのでblazeds.warを展開し、WEB_INFの下にある、flexフォルダ(4つのxmlファイルがある)、及びlibフォルダ(12個のjarファイルがる)をコピーし、GAE/Jのプロジェクト/war/WEB-INF下に配置する。

プロジェクト内のリソースの配置は以下。赤枠は新規追加したファイル。青枠はプロジェクト作成時に自動生成されるファイルで変更が必要なファイル。









3.クラス作成

動作確認のため、HelloWorld的な簡単な文字列を返すクラスを作ってみる。コードはこんな感じ。
package jp.co.tricell.sonicboom.rpc;
import java.util.logging.Logger;
public class HelloDS {
 // ロガーを指定
 private static final Logger log =  Logger.getLogger(HelloDS.class.getName());
 public String sayHello(String name) {
  log.info("input str = " + name);
  return "こんにちは、" + name + "さん!";
 }
}

4.WEB-INF/flex/remoting-config.xmlにdestinationを追加

HelloDSクラスをリモーティング接続先として設定ファイルに追加する。SyntaxHighlighterが一行だけで閉じるxmlのタグの書き方に対応していないのでワザワザ</adapter-definition>と書いてますが、実際は<adapter-definition id=~ default="true"/>でOKです。


    
        
    

    
        
    
    
    
          
              jp.co.tricell.sonicboom.rpc.HelloDS
          
    



5.WEB-INF/flex/services-config.xmlのsystemタグにmanageableの値falseを追加

これは今回の環境で必要な設定なのか不明だが、GAE/J上でBlazeDSを動作させるために参考にしたいくつかのページで記述されていたので設定として追加。
参考:
・Martin's Blog: AppEngine & Adobe BlazeDS (fix)
・Flex/AIRハマり帳 ~第2回 Google App Engine for Java+BlazeDSでハマらない方法・後編~ | デベロッパーセンター
    
 
        
        false
 
        
            false
            
        
    

6.WEB-INF/appengine-web.xmlのsessions-enabledタグに値trueを追加

セッションを有効にするため設定を追加する。
 true

 
 
  
 

7.WEB-INF/web.xmlにMessageBroker Servletを追加

  
        flex.messaging.HttpFlexSession
  
  
  
    
        MessageBrokerServlet
        MessageBrokerServlet
        flex.messaging.MessageBrokerServlet
        
            services.configuration.file
            /WEB-INF/flex/services-config.xml
       
       1
    
    
        MessageBrokerServlet
        /messagebroker/*
    

[クライアントサイド]

Airアプリを作ってみました。encpoint先はお持ちのGAE/Jのドメインを指定してください。
タグを含んだコードの場合、SyntaxHighlighterがコードを全て、Firefoxなどは小文字(<mx:WindowedApplication~→<mx:windowedapplication~など)、Internet Explorerの場合は大文字に展開してしまいます。コードをコピーする場合は大文字小文字に注意してください。

  
  
    
  
   
      
      
      
   


[動作確認]

Airアプリをクライアントとしてつくり、HelloDSと通信させてみた。但し!!!ここで問題が・・・
GAE/J上にデプロイしたクラスと通信させてみたら、以下のようなメッセージが表示され実行失敗orz。
[RPC Fault faultString="Detected duplicate HTTP-based FlexSessions, generally due to the remote host disabling session cookies. Session cookies must be enabled to manage the client connection correctly." faultCode="Server.Processing.DuplicateSessionDetected" faultDetail="null"]
at mx.rpc::AbstractInvoker/http://www.adobe.com/2006/flex/mx/internal::faultHandler()[C:\autobuild\3.2.0\frameworks\projects\rpc\src\mx\rpc\AbstractInvoker.as:220]
at mx.rpc::Responder/fault()[C:\autobuild\3.2.0\frameworks\projects\rpc\src\mx\rpc\Responder.as:53]
at mx.rpc::AsyncRequest/fault()[C:\autobuild\3.2.0\frameworks\projects\rpc\src\mx\rpc\AsyncRequest.as:103]
at NetConnectionMessageResponder/statusHandler()[C:\autobuild\3.2.0\frameworks\projects\rpc\src\mx\messaging\channels\NetConnectionChannel.as:569]
at mx.messaging::MessageResponder/status()[C:\autobuild\3.2.0\frameworks\projects\rpc\src\mx\messaging\MessageResponder.as:222]
GAE/Jが自動的に複数のサーバノード上でサーブレットを展開するために、1つのクライアントからのセッション情報が重複したように見えてしまうのが原因らしい。参考サイトを元にソースコードを編集してビルドする。
参考:http://prepro.wordpress.com/2009/05/17/googleappengineでblazeds環境を構築してみた


[エラー解決までの道のり]

1.BlazeDSのソースコードをダウンロード
BlazeDSのページからソースコードをダウンロードする。

2.BaseHTTPEndpoint.javaのコードの一部をコメント
ソースコードのzipを展開。編集が必要なソースは、flex.messaging.endpoints.BaseHTTPEndpoint.java。 ここの405行目からのduplicateSessionDetectedのチェックをコメントする。
/*if (duplicateSessionDetected)
        {
            List sessions = flexClient.getFlexSessions();
            for (Iterator iter = sessions.iterator(); iter.hasNext();)
            {
                FlexSession session = (FlexSession)iter.next();
                if (session instanceof HttpFlexSession)
                    session.invalidate();
            }
            
            // Return an error to the client.
            DuplicateSessionException e = new DuplicateSessionException();
            e.setMessage(ERR_MSG_DUPLICATE_SESSIONS_DETECTED);
            throw e;
        }*/
return flexClient;

3.ソースのビルドのため、Antをダウンロード
Apache Ant 1.7.1をダウンロード。

4.ソースのビルドのため、Ant-CONTRIBをダウンロード
Ant-CONTRIB 1.0b3をダウンロード。
展開してできるlibフォルダ内に有るant-contrib-1.0b3.jarをantフォルダのlib内にコピペ。

5.環境変数としてANT_HOMEを設定

6.環境変数PATHに%ANT_HOME%\binを追加

7.ビルド実行
BlazeDSのソースコードを展開したフォルダから
#ant clean
#ant make
を実行。
ビルド失敗orz
BUILD FAILED
C:\blazeds-src-3.2.0.3978\build.xml:73: The following error occurred while executing this line:
build.xml:156: Unable to rename old file
(C:\blazeds-src-3.2.0.3978\lib\flex-messaging-common.jar) to temporary file
編集したコードが含まれるflex-messaging-common.jarが権限が足りず変更できないらしい。はぅー。
参考:Adobe Forums: build blazeDS source code

結局Windows環境では権限をフルコントロールにしてもビルドエラーがどないもこないも解決できなかったので、Macでビルド。
ビルド成功!ほー(^-^;)

新しく出来た、flexから始まる5つのjarをGAE/JのプロジェクトのWEB-INF/libにコピペ。

8.通信テスト
GAE/J上にデプロイし、Airクライアントと通信。
通信成功ぉ~~!やたっ!

Windows環境でビルドに困る人のために、修正したコードでビルド済みのjarファイル5つを公開しま~す。この5つのjarをGAE/JのプロジェクトのWEB-INF/libにコピペしてGAE/Jへデプロイすればビルドせずにリモーティング通信できると思います。
元ソース:
blazeds-src-3.2.0.3978.zip

修正後jarファイル:
flex-messaging-common.jar
flex-messaging-core.jar
flex-messaging-opt.jar
flex-messaging-proxy.jar
flex-messaging-remoting.jar