2012年4月7日土曜日

Androidアプリを書いて見た。



会社でAndroidアプリを書いてみました。PhoneGapなどに頼らないで、nativeなforce.com対応アプリ。書いたと言っても、Force.comのテンプレートをベースにして、Chatterとコメントを表示するだけですが。環境を整えるのに2時間、Chatterとコメントが出るようになるまで1時間。

で、感想:

  • エミュレータが超重い
  • APIなどが単純でわかりやすい
  • エミュレータが糞重い
  • やっぱりEclipseはJavaで使う分には開発環境だな
  • エミュレータが馬鹿重い
  • iPhoneのInterfaceBuilder最強(今はXcodeに統合されてるけど)
  • エミュレータが劇重い
ってところですかね。「また別のAPI習得するのは私の脳には無理だからPhoneGapにしとこう」と思ってたんですが、いつまでも好きになれないJavaScriptよりも手に馴染んだJavaで単純なAPIを相手にしている方がラクかなとも思ってます。

ツールの圧倒的な操作性の良さとAPIの美しいiOS、単純なAPIとクラッシュしても必ずエラーが出てくれるAndroid。自宅ではiPhone、会社ではAndroid…まぁ良い感じかな。

で、問題のAndroidのエミュレータですが…会社のPC(Core i2, 2GB,HD)が終了しなくなってしまうのは何故。

とにかくエミュレータには困ったもんです。Android x86入れたくても会社のPC(しつこいが今時Core i2, 2GB,HD)じゃ無理す。7980円の有線LAN仕様Android買うかなw

追記(2012/04/10):
在宅勤務用のCore i5, 3GB, Windows7だとエミュレータは何事もなかったかのように無事終了してくれます。今後Android開発はこっちでやろうっと。

2012年3月28日水曜日

iPhone対応Force.comアプリをiPadでも


■iPhoneアプリをiPad対応に■

Force.com iOS SDKのnativeテンプレートは、iPhone / iPad両方に対応しています。

ただ、iPad版はMaster / Detailが前提。しかし私の作ったFourChatterはChatterがソースなもんで必ずしもきっちりMaster / Detailがあるわけではなくテンプレートのままの形態では動かすことができません。というわけで、当初はdisabledしてました。

しかしそのままというのも何だし、このところまったく開発が止まってしまっているのも何なので久しぶりに手をいれてiPad対応にしてみることにしました。


■経緯■

新しくiPad用のStoryboardを作り、プロジェクトに登録。 iPhone用のStoryboardを別ウィンドウで開いておいて、見比べながらviewをぽちぽち置いていく地味な作業を約30分。とりあえず、iPad上でもRoot Viewが動くようになりました。

で、StoryboardにCommentViewを追加したら…見事にハマりました。

…ってか、このブログハマってばっかりだよなorz


■症状■

症状としては、RootViewだけでは動いていたのにCommentViewをStoryboardに追加しただけでRootViewすら出てこなくなりました。SIGABRTです。そこから約2時間、Assembler上をシングルステップで追いかけてみましたが、どーにも見つかりません。念のためiPhoneで試してみると、動きます。 まぁこの場合、動かなかったら泣きますけどね。


■解決■

最終的な原因は、テンプレートのAppDelegate.mにありました。ここで「iPadだったらsplitViewをどうたら」という処理をしています。そうです、そんなものはとっくの昔に消しちまいました。ということで、この辺をコメントアウトして無事動くようになりました。

で…リリースしようと思ったんですが…FourChatterのウリは「iPhoneの向きを変えるだけでさくっとキーワードを切り替えてChatterを検索できる」っていうところにあります。しかし、iPadをぶんぶん回すとバカみたいという問題があります。私のiPad(初代)は回転ロックしちゃってるしね…。

というわけで、やっぱりiPhoneにはiPhone、iPadにはiPadに向いたGUIがあるよなぁ…というのが結論です。

さて、どうしましょう。タブで切り替えるか、任意の個数のキーワードを登録してタップで切り替えられるようにするか…。ただ任意の個数ってことになると「Four」Chatterっていうアプリ名が意味不明になるわな(4方向のfourです)。

--

でも今四十肩が痛くて新しいGUIのあるべき姿、みたいなことが考えられない。

…これを「四十肩ぐらいでw 言い訳にも程があるww」と思ったヒトは、本当の四十肩の怖さを知らないのだよ。寝返り打つたびに目が覚めるし、何かにつまずいてうっかり手を撞こうものならその場にしゃがみ込むほど痛いし。消炎鎮痛剤効かないし。今も何もしてないのに痛いし。

……以下、記事本体よりも長くなりそうなので省略。とにかく痛いのだ。

この記事を、とび職なのに四十肩でも仕事を休まなかった亡父に捧げます。

四十肩には効かないけど便利なので愛用

2012年3月18日日曜日

MySQLとDatabase.com

■予告と違いますが■

以前からぼちぼち書いていたこっちの記事が先に出来上がったので公開します。


■そのまま比較するのはいくら何でもアレですが■

今まで15年ぐらいMySQL使っていたと思うのですが、今日はじめてMySQL Workbenchを使ってみました。例によってマニュアル読まないのでとっつきにくかったけど、馴染んでしまえば楽勝。やっぱローカルサーバはサクサク動いていいですわ。

Salesforceは、というか、やっぱりWebアプリって、どうしてもサクサク感が足りない。日本にデータセンターできてWebでのレスポンスはよくなったものの、それでも「query」っていうレスポンス感ではないように思います。

Amazon RDSなどクラウド上で動いているMySQLサービスなんてのもあるけど、あれはどうなんでしょ。使ったことのある方いらっしゃいます? まぁ素のqueryとユーザ権限などてんこ盛りにかぶさったForce.com APIを同列に比べてはいけませんけども。


■Force.comからmySQLへ■

先日、Force.com APIで動かしていたFlashアプリをMySQLに移行するというお仕事をしました。一応、互換レイヤを作っておいてサクっと移行のはずですが予期せぬデータ変換トラブルが出るのは業界のお約束でございます。「お約束」ではすまない、とても大変なことになったのですが、その辺は長いので省略。関係者の皆様に改めて御礼とお詫びを申し上げる次第でございます。

その経験を元に、FlashでForce.comあるいはmySQLをアクセスしまくる方法、について簡単にまとめてみたいと思います。


■使用ライブラリ■

以下のライブラリが必要です。swcをダウンロードしてAIRプロジェクトのlibsに入れます。



■Connection/AsyncResponder■

割と似てます。当然Force.comではサーバ指定とポート指定は不要でid, passwordを渡してやればつながります。

mySQL:
mysql = new com.maclema.mysql.Connection("<サーバ>", 3306, "<接続名>", "<ユーザ>", "<パスワード>");
mysql.addEventListener("connect", handleConnected);
mysql.addEventListener("ioError" , errorConnected);
mysql.connect();

..

private function handleConnected(e:Event):void {
    trace( "connection success" );
}

..

private function errorConnected(error:Event):void {
    trace( "connection error" );
    trace( e.toString() );
}

Force.com:
var lr:LoginRequest = new LoginRequest();
lr.username = "<force.com id>";
lr.password = "<password><signature>";

lr.callback = new com.salesforce.AsyncResponder(loginHandler, faultHandler);
force.login(lr);

..

private function loginHandler(result:LoginResult, inObject:Object):void {
    trace("login success");
}

..

private function faultHandler(result:Fault):void {
    trace(result.faultcode);
    trace(result.faultstring);
}

何かするごとにAsyncResponderで成功時と失敗時のコールバック関数を渡してやるのも同じです。

Force.com/mySQLを一本化する場合に一つ問題があります。それは、asSQLはmx.rpc.AsyncResponderを使いますが、Force.comは同じ名前のcom.salesforce.AsyncResponderを使うという点。名前が違うだけなら、宣言と生成でフルパス指定すれば済むんですが、困ったことにコールバック関数の引数の数が違います。もちろん1本のソースコードでmySQLとforce.comに対応、なんてことをやらなければ全然問題ないんですけども。


■Fetch■

MySQL:
var st:Statement = mysql.createStatement();
st.executeQuery("SET NAMES 'UTF8'");
st.sql = "SELECT Id, Name FROM SomeTable__c";
var token:MySqlToken = st.executeQuery();
token.addResponder(new mx.rpc.AsyncResponder(queryHandlerSomeTable, fault, token));

..


private function queryHandlerSomeTable(data:Object, token:Object):void {
    var rs:ResultSet;
    rs = ResultSet(data);
    while( rs.next() ) {
        trace(rs.getString("Id"));
        trace(rs.getString("Name"));
    }
}


Force.com:
strQuery = "SELECT Id, Name FROM SomeTable__c";
force.query(strQuery, new com.salesforce.AsyncResponder(queryHandler, faultHandler));


..


private function queryHandlerSomeTable(result:QueryResult):void {
    if (result != null && result.records != null && result.records.length > 0) {
        for each (var item:SObject in result.records) {
            trace(item.Id);
            trace(item.Name);
        }
    }
    if (!result.done) {
        force.queryMore(result.queryLocator, 
                        new com.salesforce.AsyncResponder(queryHandlerSomeTable,
                                                          faultHandler));
    }
}



単純なfetchなら話は簡単でSOQLとSQLもまったく同じになります。問題はリレーションを使う場合です。force.comはオブジェクト型なので  Account.Name って書くだけで取引先名を引っ張ってこれますが(WebObjectsもそうだったなぁと遠い目)、mySQLはJoinで表を結合してAccount.Name には適当なエイリアス名を付けて参照する必要があります。このへん需要があれば詳しく書きますのでコメントください(ない、というオチが寂しい)


■Update/Insert

需要があれば書きます。


■共通の落とし穴■

fetchした結果はどっちもObject型みたいなもんに入って返って来ます。で、どっちもSQL/SOQLのattribute名とObjectから引っ張り出す時のkeyが間違っていると、fetchしたのにnullだったのか、SELECT文に書き忘れてnullだったのか、すぐにはわかりません。大文字と小文字を間違えてもダメです。SELECT文に書いた通りのkeyと完全に一致しないとダメです。Objectにkeyがあるかどうかを確認して、あるはずのkeyがなかったらエラーを出す、というような処理をはさみましょう。

まぁね…当たり前のことなんだけどね…。


■Force.comでのはまりどころ■

Flashとは違ってAIRで使う場合はデフォルトで同期がONになっています。ので、Fetch後に自動的にForce.com APIに対して問い合わせをします。しかし、相手はForce.com、頻繁に問い合わせするもんであっという間に1日のアクセス制限回数を超えてしまいます。ログイン前にでも以下の行を実行してキャッシュ切ってください。

force.doCache = false;


■asSQLの落とし穴■

update文でprimary keyの値を指定しますが…その値の型が間違っていた場合には、「success側のコールバック関数にfailが帰ってくる」という何が何だかわからない現象が起こります。これ気づくのに2時間ぐらいかかりました…だってねぇ…まさかねぇ…。

2012年3月12日月曜日

ご無沙汰しております

サラリーマンが忙しくて、ぜんぜん手が付けられませんでした。

とりあえず「SalesforceエンジニアのためのFlash入門」という需要があるんだか無いんだかまったくわからない連載をスタートさせようと思います。

どうぞよろしくお願いします。

2012年1月31日火曜日

Desktopを見るのが怖い…

いえね、昨日終業間際にDataLoaderを使って急ぎのインポート作業をやったんですけど、なかなかうまくいかなくてですね。

対象オブジェクトは3つ。何だかんだで20回ぐらいリトライしてようやくうまく行ったのです。で、そのままPCをシャットダウンしまして。

Desktopにアイコンが100個ぐらい並んでいるんだろうなー…やだなー…。

--

ところで、開発サンドボックスってレポートは移行してくれないんでしたっけ。昨日それで30分くらい右往左往してました。Force.com IDEでDeploymentすると「Success」で終わるのにフォルダに現れてくれない。foldersも一緒にデプロイしてもダメ。

本筋と関係ないので諦めましたが…。

2012年1月18日水曜日

Salesforce mobile SDK for iOS +1

■テンプレート以外で作業する場合のTips 1■

SFAuthorizingViewControllerにこんなのが定義されてますが。

    @property (nonatomic, retain) IBOutlet UIView *oauthView;  

これ、使われていません。テンプレートについてる「Authorizing」の画面があまりにもスゴいので当然置き換えを考えるわけですが、置き換えたxibでちょっと小細工をしたものの、どうも思ったとおりの動作をしてくれない。何だかんだ調べたら、oauthViewは全然使われていませんでした。

普通にviewをFile's Ownerにしてください。

Batch Apex / Mailでハマった件

香港にて
お疲れ様です。

Batch Apexに限らないんだけど…arrayに入れたSingleMailMessageをまとめてどーんと送る時、その中に1通でも不達アドレスが含まれていると一通も送られないでエラーになりますね。

昨日はそれでハマりました。

Contactに関連付けたemailアドレスにメールを送って、それが正常に配信されないとそのアドレスには「確認しろよコラ」ってフラグが立ちます。その状態で同じアドレスにメールを送ろうとすると送信前にエラーになる。まぁそれは当たり前。問題は、不達アドレスだと検出されるタイミングが前回送信から数十分〜数時間かかるってところで、いつ判定されるかわかんない。なので大量に送信してたりすると、エラーがたまたま次回の送信Batchにひっかかってコケてました。

報告をするのにエラーの経緯を再現しなきゃいけないんだけど、そんな神業なタイミングをどーしろとw

それにしても、疲れてるとレアケースの推測がダメだわ。

今日は、送信前にそのContact / emailアドレスが不達だと判断する方法を探さなければ…。


2012-01-29追記:
EmailBouncedDate / EmailBouncedReasonがnullじゃなかったら不達ですね…。

2011年12月31日土曜日

Salesforce mobile SDK正式版(GA)

■βが取れました■

まぁとりあえず、めでたいです。今後はドキュメントなども充実してくれますよーに。



■では試してみましょう■

というわけで、例によってGitHubから最新版を落としてきます。もしXcodeが立ち上がっていたら、念のため終了しといてください。なお、私の環境ではGitHub for Macでの同期がうまく働きませんでした。ので、zipを落としてきます。

SalesforceMobileSDK-iOS

落としたら、zipを解凍したディレクトリに移り、 ./install.shを実行します。例によって数分後、BUILD SUCCESSFULが出たら無事終了です。

さて、Xcode 4.2を起動します。New Projectを選ぶと、以前よりちょっと派手なテンプレートアイコン「Hyblid Force.com App」と「Native Force.com REST App」が追加されています。今回はサクっと Native を選びます。


次の「Choose options for your new project」でProduct Nameに適当なプロジェクト名、iTunes connectなどに登録したあなたの会社ID、Consumer KeyにはあなたのSalesforce/Database.com組織のリモートアクセスキー、Redirect URLにはその戻り先URLを入力します。

Xcode 4.2だとその下の「Use Automatic Reference Counting」をonにしたいのですが、残念ながらMobile SDKが対応していないのでここはチェックを外してください。外さないとビルドエラーの塊になります。すべて入力したらNextをクリックし、プロジェクト保存先を指定し、プロジェクトを作ります。

以前はここでOther linker flagを設定したりFrameworkを追加する一手間が必要だったのですが、さすがはβでないGA、さくっとそのままcmd+Rで実行できます。起動すると、黄土色の「Authorizing」画面が表示され、その後Salesforce.comのOAuth認証画面になります。ID, Passなどを入力すると、その組織のユーザが10名分表示されます。


■βとの差異■

βはAppDelegateに認証関連の処理を細々と書かなければいけませんでしたし、「認証中です」の画面も自分でイチから作ってやらなければいけませんでした。でも、1.0では、その辺の処理はSFAuthorizingViewControllerとSFNativeRestAppDelegateに実装されていて、AppDelegateにはごく僅かなコードを書くだけで処理してもらえます。本来のAppDelegateに書きたい処理と分離することができて、非常にすっきりしました。

ViewControllerに関しては元々テンプレートが作ってくれるコードは非常に単純なので、あんまりRESTがどうこう考えないで実装することができます。


■そして、ハマる(お約束)■

以下は余談です。

とりあえず、プロジェクトの構造などを検証するために今度はMobile SDKのテンプレートを使わずにアプリを作ってみることにしました。
  1. Single View Appテンプレートでプロジェクトを作る
  2. さっきNative Force.com REST Appで作ったプロジェクトからDependenciesフォルダとNativeRestAppフォルダをコピー
  3. 必要なFrameworkを追加
  4. AppDelegateとViewControllerに必要なコードをコピペ
  5. cmd+Rでビルド&RUN
アプリは無事シミュレータ上で起動し、ドドメ色の「Authorizing」から認証を得て…オチますorz

これが3日ほど前でした。それから他の作業もしつつ、いろいろ原因究明したんですが、どうやっても同じところで落ちます。以前のパターンで同じようなエラーが出た時にはdelegateのスペルが間違っていたのが原因だったので、今回も何かdelegateを抜かしているのではないかと思い、ソースをにらめっこする日々でした。でも治らない。

その辺のウダウダを書いてもしょうがないので、例によって最終的に解決に至った課程のみ書きます。
  • エラーが出ているのは、viewDidLoadの下記の行
[[SFRestAPI sharedInstance] send:request delegate:self];
  • SIGABRTで止まった時点でのスタックトレースを見る←最初にやっとけ
  • [SFRestRequest description」でググる→SFRestRequest.mのソースがヒット
SFRestRequest.m
  • ソースに以下の記述を発見
  • _queryParamsはNSDictionaryなので当然JSONRepresentationなどというメソッドはない。遅まきながら、ここで「カテゴリ」が実装されてないことに気づく

原因は…上にヒントが書いてあります。

Other Linker Flagsに-ObjC -all_loadがセットされてない

でした。お粗末さまでした…。

言い訳をするとね…以前これらのLinker Flagsがセットされていない時にはビルドエラーで止まったのよ…。

2011年12月17日土曜日

FlashでPDF・続き

■続き、っていうか本番■

前回はFetchしてデータを一覧表示するところまででした。

FlashでPDF

「そもそもなんでFlashなのか?」という話が抜けていました。
  1. クライアントサイドの有り余るリソース(メモリ, CPU)を使える
  2. きめ細かく罫線などを制御できる
  3. Force.comでは実現不可能なグラフも作れる
  4. Rich GUIで余計な苦労を負わないで済む
  5. PDF添付メールやファイル出力の制約が少ない
PDF生成そのものに関しては「セルその他の描画を細かく制御できるので、基本的にどんな帳票でもおk」ってのがFlash / purePDFでPDFの長所の一つです。クライアントサイドでは数GBのメモリも当たり前なので、ガバナーの制約を気にしないで好きなことできるのもメリットです。「数万件のデータをドーンと読み込んできてループまわしながら集計し、役員会に出せるような美しい罫線の入ったPDFを作る」ようなケースで使えます。

あとグラフ。Flashのグラフは制約が少ないので複雑なグラフを作りたい場合にはForce.comよりもラクできます。

また最近はWebアプリケーションであってもrichなGUIが求められる案件が増えてきました。Force.comでも売掛入金の消し込み画面で設定した条件が間違っていたらボタンをdisableしたり、jQueryで関連するデータをアニメーション表示したりお客様の多彩なご要望に苦労する日々でございます。

でもFlashはrich GUIの本家です。

先日もあるコンサルタント会社から「100近い検索条件から案件を抽出する検索画面」という依頼を受けましたが、設定した条件が埋もれてしまわないようわかりやすく表示するのもFlashなら自由自在です(簡単です、と書かないところに私の誠実さを感じてください)

 そうそう、それにFlashなら開発の途中でオブジェクトの項目を変更する必要が生じても「それAPEX/VFで使ってるから変更できねぇよw」とForce.comから怒られることもありません(笑)。

■purePDFで■

FlashでPDFを生成するためのライブラリでメジャーなのはAlivePDFとPurePDF。今回はPurePDFを使います。PurePDFはJavaのPDFライブラリiTextをFlashに移植したもので、私はiTextには散々痛い目にあったお世話になったことがあるので、まぁ学習コストがかからないだろう、と。

使ってみたら日本語が文字化けたりしてまた散々痛い目にあったのですが、そういう経緯は全部スキップします。ここからライブラリをダウンロードします。

Player10.0 でも purePDF で日本語PDFを作る

FlashBuilder上のプロジェクトのlibsフォルダには前回force-air.swcを追加しましたが、今回ダウンロードしたpurePDF.swcとpurePDFont.swcも追加します。

APIなどに関してはFlashとJavaとで実装上の違いはありますが、iTextの方がサンプルも多く用意されていて便利です。


■解説■

ソースは最後に貼ります。基本的な処理の流れとしては

  1. サイズと出力先(バッファ先)を指定してDocumentを生成する
  2. ベースフォントを作る
  3. ベースフォントにサイズなどを指定してフォントを生成する
  4. テキスト、イメージ、別のテーブルなどをセットしたセルを作る
  5. セルをテーブルにセットする
  6. テーブルをドキュメントにセットする
  7. 必要ならページ送り(document.newPage)

では、実際の処理を見ていきます。

>queryHandler
前回見たようにarrayDataにfetchしたデータを溜めていきます。result.doneになったらfetch完了ですので、dataGridにarrayDataをセットし、pdfの生成を始めます。

PdfWriterでバイトストリームをセットしつつ用紙サイズを設定します。ここではPageSize.A4でA4縦を指定していますが、PageSize.A4.rotate(90)と書くとA4横になります。

>initPdfFont
ベースフォントを生成します。いろいろなフォントを試してみたのですが、結局これ平成明朝しか使えませんでした。ゴシックも指定できるのですが、日本語フォント部分は同じで英数字部分が変わるのみでした。

>createPdf
実際に使用するサイズのフォントを生成してからPDFを生成していきます。vecという配列を用意していますが、これはTableのrowをどういう比率で生成するかを指定するためのものです。値が合計いくつになってもかまわないので、私はお客さんからもらってきたサンプル帳票に物差しあててその数字を使ったりしていますw

あとは文字列を元にしてセルを作ってテーブルにせっせと詰め込んでいくだけです。終わったらnewPageでページを閉じて、最後に document.close(); でドキュメントを閉じます。

>cellFromStringFontAndAlignment
セルを生成しています。関数名がやたらと長いのは私が長年MacOS系プログラマだったからです。生成したcellオブジェクトにはデフォルトで罫線がついてます。top, border, right, leftごとに太さ、色などを制御することができます。逆にいえば、セルを作る時点で罫線のことを考えないと意図した表になりません。まぁこれがメリットになるかデメリットになるかは状況次第っすね。

>savePdf
保存ダイアログを表示して、ファイル名や出力先を指定し、ファイルを保存します。

>formatDateMeasure
ここがFlashの困ったちゃんで、DataGridにDateやNumberを表示する際の書式指定でいちいちコールバック関数などを書かないといけないのです。VisualBASICが懐かしいぜ。

>convertToLocaltime
RESTで帰ってくる日付はGMTなのでこれを日本時間に変換してやります。60000はtimezoneOffsetで取ってくる時差が分単位なのでこれをミリセカンド単位に換算するためです。

>formatDecimal
3桁ごとのカンマ区切り、もっと簡単な方法あったら教えてください^^;


■注意■

今回サンプルとして掲載したソースにはID / Passwordが文字列定数として埋め込まれていますが、実際にFlashで作る場合この方法は避けてください。ダンプしてソースをリバースエンジニアリングすることが可能ですので。

FlashとしてForce.com環境上で使うのであればセッションを引き継いで認証できるのでID / Passwordは不要ですし、AIRでデスクトップアプリとして配布する場合はGUIからID / Passwordを入力してもらいpreferenceとしてPC上に保存すればOKです。


■Flash業界のみなさんへ■

データベースにお悩みではありませんか?

force.com / database.comの世界にどうぞ。database.comなら最高にセキュアでスケーラブルなRESTデータベースが3アカウント, 10万レコード, 月5万トランザクションまで無料で使えます。サーバのセットアップもバックアップも不要です。

お願い:ソースでお気づきの通り私はFlash初心者です。いろいろツッコミどころ満載だと思いますので、ご指導ご鞭撻のほどよろしくお願いいたしますm(_ _)m。


■おまけ■

個人事業としてiPhoneアプリを書いてます。

現職でForce.comに携わる前はWebObjectsというフレームワークで仕事をしていましたが、WO屋から見るとApexはイマイチ美しくありません(もちろんマルチテナントとしての安全性安定性などを確保するための現実的な実装という面もあると思いますが)

そんな私にとってForce/Database/Chatter.comをRESTでアクセスするiOSアプリはほぼ理想郷です。


■最後に■
このブログはForce.com Advent Calendarに参加しています。


■ソース■

というわけでソースです。

<?xml version="1.0" encoding="utf-8"?>
<s:WindowedApplication xmlns:fx="http://ns.adobe.com/mxml/2009" 
        xmlns:s="library://ns.adobe.com/flex/spark" 
        xmlns:mx="library://ns.adobe.com/flex/mx"
        xmlns:salesforce="http://www.salesforce.com/"
        applicationComplete="init()" height="560" width="800">
 <fx:Declarations>
  <salesforce:AIRConnection id="force"/>
 </fx:Declarations>

 <fx:Script>
  <![CDATA[
   import com.salesforce.AsyncResponder;
   import com.salesforce.objects.LoginRequest;
   import com.salesforce.results.Fault;
   import com.salesforce.results.LoginResult;
   import com.salesforce.results.QueryResult;
   
   import mx.collections.ArrayCollection;
   import mx.formatters.DateFormatter;
   
   import org.purepdf.Font;
   import org.purepdf.colors.RGBColor;
   import org.purepdf.elements.Element;
   import org.purepdf.elements.Phrase;
   import org.purepdf.elements.RectangleElement;
   import org.purepdf.pdf.PageSize;
   import org.purepdf.pdf.PdfDocument;
   import org.purepdf.pdf.PdfPCell;
   import org.purepdf.pdf.PdfPTable;
   import org.purepdf.pdf.PdfWriter;
   import org.purepdf.pdf.fonts.BaseFont;
   import org.purepdf.pdf.fonts.cmaps.CJKFontResourceFactory;
   import org.purepdf.pdf.fonts.cmaps.CMapResourceFactory;
   import org.purepdf.resources.BuiltinCJKFonts;
   import org.purepdf.resources.CMap;
   import org.purepdf.resources.ICMap;
   import org.purepdf.utils.IProperties;
   import org.purepdf.utils.Properties;

   
   
   [Bindable]
   private var arrayData:ArrayCollection = null;
   private var dateTimeFormatter:DateFormatter = null;
   
   private const MaginLeft:int    = 20;
   private const MarginRight:int  = 20;
   private const MarginTop:int    = 30;
   private const MarginBottom:int = 30;
   
   private function init():void 
   {
    dateTimeFormatter = new DateFormatter();
    dateTimeFormatter.formatString = "YYYY-MM-DD HH:NN:SS";

    var lr:LoginRequest = new LoginRequest();
    status = "login by id/password";
    
    lr.username = "youraccount@your.domain";
    lr.password = "password"+"securitytoken";
    
    lr.callback = new AsyncResponder(loginHandler, faultHandler);
    force.login(lr);
   }  
   
   private function loginHandler(result:LoginResult):void 
   {
    if (result.userInfo != null) {
     status = "login success, query";
     
     force.autoSyncEnabled = false;
     force.doCache = false;
     
     var strQuery:String = "Select DateMeasure__c, High__c, Low__c, Beat__c From BloodPressure__c order by DateMeasure__c desc";
     
     force.query(strQuery, new AsyncResponder(queryHandler, faultHandler));
    }
   }
   
   
   
   private function queryHandler(result:QueryResult):void
   {
    if (result != null && result.records != null && result.records.length > 0) 
    {
     if (arrayData == null)
      arrayData = new ArrayCollection();
      arrayData.addAll(result.records);
    }
    
    if (!result.done)
    {
     trace("query again");
     force.queryMore(result.queryLocator, new AsyncResponder(queryHandler));
    }
    else
    {
     status = "query result";
     force.syncConnection
     
     dataGrid.dataProvider = arrayData;
     
     var byteArray:ByteArray = new ByteArray();
     
     var pdfWriter:PdfWriter =  PdfWriter.create(byteArray, PageSize.A4);
     var document:PdfDocument  =  createPdf(pdfWriter, initPdfFont());
     savePdf(byteArray, "血圧報告.PDF");
     
     document  = null;
     pdfWriter = null;
     byteArray = null;
    }
   }
   
   private function faultHandler(result:Fault):void
   {
    trace(typeof(result));
    trace(result.faultcode);
    trace(result.faultstring);
   }
   
   
   private function initPdfFont() : BaseFont
   {
    var map: ICMap = new CMap( new CMap.UniJIS_UCS2_H() );
    CMapResourceFactory.getInstance().registerCMap( BaseFont.UniJIS_UCS2_H, 
                                                    map );
    
    var prop: IProperties = new Properties();
    prop.load( new BuiltinCJKFonts.HeiseiMin_W3() );
    
    CJKFontResourceFactory.getInstance()
                          .registerProperty(
        BuiltinCJKFonts.getFontName( BuiltinCJKFonts.HeiseiMin_W3 ), prop);
    
    var bf: BaseFont = BaseFont.createFont( 
        BuiltinCJKFonts.getFontName( BuiltinCJKFonts.HeiseiMin_W3 ), 
        BaseFont.UniJIS_UCS2_H, BaseFont.NOT_EMBEDDED, true );
    
    return bf;
   }
   
   private function createPdf(inWriter:PdfWriter, inBf:BaseFont) : PdfDocument 
   {
    var fontHeader:org.purepdf.Font 
        = new org.purepdf.Font( org.purepdf.Font.BOLD, 12, -1, null, inBf );
    var font:org.purepdf.Font       
        = new org.purepdf.Font( -1.   -1,              10, -1, null, inBf );

    var document:PdfDocument = inWriter.pdfDocument;
    document.open();
    document.setMargins(MaginLeft, MarginRight, MarginTop, MarginBottom);
    
    var table:PdfPTable = null;
    var cell:PdfPCell = null;
    
    var lc:int = 99;
    var lpp:int = 40;
    var i:int = 0;
    var n:int = arrayData.length;
    var monthBreak:int = 0;
    
    var vec:Vector.<Number> = new Vector.<Number>;
    vec.push(50);
    vec.push(25);
    vec.push(25);
    vec.push(25);
    
    while (i < n) {
     // 行を取り出し
     var line:Object = arrayData[i++];
     var dateMeasure:Date = convertToLocaltime(line.DateMeasure__c);
     
     // 改ページ
     if (lc >= lpp) {
      if (table != null) {
       document.add(table);
       document.newPage();
       table = null;
      }
      lc = 0;
     }
     
     //ヘッダ
     if (table == null) { 
      table = new PdfPTable(vec);
      table.addCell(cellFromStringFontAndAlignment("測定日時", fontHeader));
      table.addCell(cellFromStringFontAndAlignment("最高血圧", fontHeader));
      table.addCell(cellFromStringFontAndAlignment("最低血圧", fontHeader));
      table.addCell(cellFromStringFontAndAlignment("心拍",     fontHeader));
     }
     
     table.addCell(cellFromStringFontAndAlignment(
                       dateTimeFormatter.format(dateMeasure), font));
     table.addCell(cellFromStringFontAndAlignment(
                       formatDecimal(line.High__c), font, Element.ALIGN_RIGHT));
     table.addCell(cellFromStringFontAndAlignment(
                       formatDecimal(line.Low__c),  font, Element.ALIGN_RIGHT));
     table.addCell(cellFromStringFontAndAlignment(
                       formatDecimal(line.Beat__c), font, Element.ALIGN_RIGHT));
     lc++;
    }
    
    if (table != null) {
     document.add(table);
     table = null;
     
     document.newPage();
    }
    
    document.close();
    
    return document;
   }
   
   private function cellFromStringFontAndAlignment(
                                        inStr:String, 
                                        inFont:org.purepdf.Font, 
                                        inAlignment:int = Element.ALIGN_CENTER)
                                      : PdfPCell {
    var str:String = (inStr == null) ? "" : inStr;
    
    var cell:PdfPCell = PdfPCell.fromPhrase(new Phrase(str, inFont));
    cell.horizontalAlignment = inAlignment; 
    
    return cell;
   }
   
   private function savePdf(inByteArray:ByteArray, inFileName:String) : void
   {
    var fr:FileReference = new FileReference();
    fr.save(inByteArray, inFileName);
   }

   private function formatDateMeasure(inObj:Object, 
                                      inDataGridColumn:DataGridColumn)
                                    : String
   {
    return (inObj != null && inObj.DateMeasure__c != null) 
           ? dateTimeFormatter.format(convertToLocaltime(inObj.DateMeasure__c))
           : "";
   }
   
   private function convertToLocaltime(inStr:String):Date {
    var date:Date = DateFormatter.parseDateString(inStr);
    var time:Number = date.getTime() - date.timezoneOffset * 60000;
    date.setTime(time);
    
    return date;
   }
   
   private function formatDecimal(inDecimal:Number):String
   {
    if (isNaN(inDecimal))
     return "";
    else {
     var pattern:RegExp = /(\d)(?=(\d{3})+(?!\d))/g;
     return String(inDecimal).replace(pattern, "$1,");
    }
   }

  ]]>
 </fx:Script>

 <mx:DataGrid id="dataGrid" top="10" left="23" right="23" bottom="10" 
              editable="false" enabled="false">
  <mx:columns>
   <mx:DataGridColumn headerText="測定日時" dataField="DateMeasure__c" 
                      labelFunction="formatDateMeasure" textAlign="center"/>
   <mx:DataGridColumn headerText="最高血圧" dataField="High__c" 
                      textAlign="center"/>
   <mx:DataGridColumn headerText="最低"  dataField="Low__c"  textAlign="center"/>
   <mx:DataGridColumn headerText="心拍"  dataField="Beat__c" textAlign="center"/>
  </mx:columns>
 </mx:DataGrid>



</s:WindowedApplication>

2011年12月13日火曜日

FlashでPDF

■別に専門というわけではないけれど■

最近は変わりつつありますが、Webアプリにとって「カチッ」とした帳票というのはやや鬼門で、皆さんかなり苦労を強いられてきました。

私自身を振り返ってみてもPDF関連の実装が割と大きな比重を占めています。前職のWebObjectsでもiTextというJavaのライブラリを使っていましたし、現職Force.com屋でもほぼすべての案件にPDF生成が含まれている状況です。

これまでForce.com上で部門別部課別集計、罫線の各種アレンジ、複数ページ請求書などいろいろな帳票を作ってきましたが、日本ビジネス特有のきめ細かい帳票要求については、以下の2点:

  • 日本語フォントが1種類しか使えない
  • ひらがなカタカナの拗促音がbaselineに揃わない

を除けば、特に不自由なく生成できるので、ネタとしては面白くありません。


■FlashでPDF■

さて、Flash toolkit for Force.comつかっている人います? 日本のBBSを見てもほとんど話題が出ていないですし、海外の掲示板でもFlashBuilder for Force.comが中心になってしまったようで、大変寂しい思いをしています。toolkitの開発もすっかり止まってしまいましたし、このままfade outしちゃうんですかね…。

というわけで、この機会に少しでも開発者が増えることを願って、Flash+Force.comでのPDF生成について書きます。今回はFlash+Force.comについて、次回はFlash上でのPDF生成について書きます。


■Flashでの開発■

Force.com Toolkit for Adobe AIR and Flex

何よりFlashがないと話しになりません。とりあえず30日間無料でお試しができますので、Adobeから「FlashBuilder 4.6 Standard Edition」をダウンロードしてきてください。

余談ですが、4.5からはiPhone/Androidアプリも書くことができます。試してみたらホントに15分くらいでForce.com対応iPhoneアプリを書けて大笑いしたんですが、GUIがネイティブと異なりすぎることとメモリをバカ食いするので、今のところ本気では使っていません。

さて、お次は、FlashからForce.comをアクセスするためのライブラリが必要です。これは、このページの「download toolkit」から落としてきます。

では、FlashBuilderを起動してください。ユーザ登録の手続きなどは端折ります。起動遅いっす。私の嫌いなEclipse様がベースなので諦めてください。

ファイル>新規プロジェクト>Flexプロジェクト

適当な場所に適当な名前でプロジェクトを作ります。とりあえず「Advent」にしました。アプリケーションの種類をデスクトップにする以外はデフォルトのままでOKです。

できたプロジェクトにForce.comのライブラリを追加します。さっきダウンロードしてきたライブラリの中から「force-air.swc」をプロジェクトのlibsにドラッグ&ドロップしてください。

パッケージエクスプローラの中から

Adbent>src>(default package)>Advent.mxml

を開きます。ソースの先頭にあるs:WindowedApplicationとfx:Declarationsを以下のソースと置き換えてください。

<s:WindowedApplication xmlns:fx="http://ns.adobe.com/mxml/2009" 
 xmlns:s="library://ns.adobe.com/flex/spark" 
 xmlns:mx="library://ns.adobe.com/flex/mx"
 xmlns:salesforce="http://www.salesforce.com/"
 applicationComplete="init()" height="560" width="800">
 
 <fx:Declarations>
  <salesforce:AIRConnection id="force"/>
 </fx:Declarations>


その後にはコード部分のタグが来て

 <fx:Script>
  <![CDATA[


   
  ]]>
 </fx:Script>

最後にデータを表示するためのグリッドが来て最初のタグを閉じます。

 <mx:DataGrid id="dataGrid" 
   top="10" left="23" right="23" bottom="10" 
   editable="false" enabled="false">
  <mx:columns>
      <mx:DataGridColumn headerText="id"  dataField="Id"/>
      <mx:DataGridColumn headerText="氏名" dataField="Name"/>
      <mx:DataGridColumn headerText="電子メール" dataField="Email"/>
  </mx:columns>
 </mx:DataGrid>

</s:WindowedApplication>

以上が基本的なソース構造です。で、「CDATA[」の次の行からコードを書いていきます。

■まずはインポートとグローバル変数■

 import com.salesforce.AsyncResponder;
 import com.salesforce.objects.LoginRequest;
 import com.salesforce.results.Fault;
 import com.salesforce.results.LoginResult;
 import com.salesforce.results.QueryResult;
   
 import mx.collections.ArrayCollection;

 [Bindable]
 private var arrayUser:ArrayCollection = null;


■初期化/ログイン■

冒頭部分にあるapplicationCompleteから呼び出される初期化ルーチン、ここでログイン処理をします。昔ながらの処理でOAuthなどはありませんorz。なお、これはデスクトップアプリなのでIDとPasswordでログイン処理を行いますが、FlashとしてVisualforceに埋め込む場合にはセッションidなどからそのまま認証を引き継ぐことができるようになっています。


 private function init():void 
 {
  var lr:LoginRequest = new LoginRequest();
  status = "login by id/password";
  
  lr.username = "your.account@your.domain";
  lr.password = "password" + "token";
  
  lr.callback = new AsyncResponder(loginHandler);
  force.login(lr);
 }  


■ログイン成功■

ログインが成功すると、この関数が呼び出されます。ここではすぐに馴染み深いSOQLを呼び出していますが、検索処理などを行う場合には、ここでGUIの「検索ボタン」をenableにする、などの処理を書きます。

 private function loginHandler(result:LoginResult):void 
 {
  if (result.userInfo != null) {
   var strQuery:String = "Select Id, Name, Email From User";
     
   force.query(strQuery, 
        new AsyncResponder(queryHandler, 
             faultHandler));
  }
 }


■Query成功■

Query成功時にこのコードが呼び出されます。ここは1回に最大2000レコードずつ渡されます。つまり逆にいえば、数万件のレコードがヒットするようなQueryを発行しても、ここでarrayUserにひとまとめにすることができる、ということです。まぁ遅いですけどね。それにFlashは基本的にシングルタスクなので何も配慮しないで数万件のPDF帳票を出力しようとするとUIが一切操作できなくなります。不憫な奴。

で、最後にdataGridに読み込んだデータを一括してセットすれば一丁上がり。

 private function queryHandler(result:QueryResult):void
 {
  if (result != null && result.records != null 
   && result.records.length > 0) 
  {
   if (arrayUser == null)
    arrayUser = new ArrayCollection();
   arrayUser.addAll(result.records);
  }
    
  if (!result.done)
  {
   trace("query again");
   force.queryMore(result.queryLocator, 
     new AsyncResponder(queryHandler));
  }
  else
  {
   status = "検索または実行開始をどうぞ";
   force.syncConnection
     
   dataGrid.dataProvider = arrayUser;
  }
 }


■エラー処理■

SOQLに誤りがあった場合などにこの関数が呼び出されます。

 private function faultHandler(result:Fault):void
 {
  trace(typeof(result));
  trace(result.faultcode);
  trace(result.faultstring);
 }

■基本編終了■

以上でForce.com + Flexの基本はおしまいです。次回はPDF生成編です。それまでよろしかったらSOQLを書き換えたりDataGridのdataFieldをいじったりして遊んでみてください。なお、"Select Id, Name, CurrentStatus From User"などと書いてもAPIが古いのでこのライブラリでは対応していません。あしからず。

--

この記事はForce.com Advent Calendarに参加しています。

Force.com Advent Calendar 2011 : ATND

PhoneGapかiOS SDKについて書くと予告していたのにFlashになったのは準備していたネタが土壇場になって不調だったからさ。以前書いた記事もあるし。

PhoneGap 1.2.0 + Salesforce SDK(hyblid)

以上、50歳と2日のくらはしがお送りしました。

#Bloggerさんがインデントをつぶしてしまったけど、まぁ読めるのでご勘弁