2013年5月26日日曜日

SalesforceからiPhoneへPush notificationを送る

とんかつ専門店のご飯が妙に美味いのはなぜ?
上の写真はまったく関係ありません。

週末、何かネタを探していたら、www.parse.comというサービスに関する記事が見つかりました。最近ではPush Notification代行サーバもいろいろあるのですが、これは余計な機能がないぶんシンプルで安い。

そんなわけで、chatter上に「$go message」と書くと、triggerが作動してForce.comのhttp calloutからparse.comのAPIを叩き手元のiPhoneにメッセージが届く、というのを作ってみました。

■まず、Push Notificationを試す■

試すと言いますか、こちらの記事を参考に、いや、そのまま実行してみます。

Remote Push Notification ASPサービスを試す

Parse、一発で動いてPushされました。いやー、いままでこんなに簡単にPushできたのって初めてかもしれない。10分か15分くらいで動いてしまいました。



■FeedItemのTriggerからParseのREST APIを呼ぶ■

さて、ここからはForce.com上での作業。TriggerからREST APIを呼ぶことはできないので、Triggerからバッチを起動、バッチからcall outする作戦。

忘れないうちに、まずはお約束のリモートサイトの設定。
  管理者設定>セキュリティのコントロール>リモートサイトの設定
これでhttps://api.parse.com/を登録しておきます。

■Batchを作る■

Developer ConsoleからClass CallParseを定義、IterableなBatchApexでParse.comのAPIを叩きます(コードは末尾)。

■Triggerを作る■

例によってDeveloper ConsoleからFeedItem上のキーワード $go を検出してBatch Apexを呼び出すコードを書きます(コードは末尾)。

■動かない■

一応、Salesforce上で
  管理者設定>監視>デバッグログ
を設定してから、Chatter上にメッセージを書いてみる。

…動かない。デバッグログを見ると、

05:50:02.039 (39869000)|CALLOUT_REQUEST|[29]|System.HttpRequest[Endpoint=https://api.parse.com/1/push, Method=POST]
05:50:02.175 (175302000)|HEAP_ALLOCATE|[EXTERNAL]|Bytes:661
05:50:02.175 (175393000)|CALLOUT_RESPONSE|[29]|System.HttpResponse[Status=Bad Request, StatusCode=400]
05:50:02.175 (175420000)|HEAP_ALLOCATE|[29]|Bytes:130
05:50:02.175 (175438000)|SYSTEM_METHOD_EXIT|[29]|System.Http.send(ANY)

と出ていました。ちくしょう、どうせ一発で動くと思ってねぇですよ←涙目 なので、Apex側にコードを入れて
    System.debug(res.getBody());
    System.debug(res.getStatus());
    System.debug(res.getStatusCode());
検証してみたところ
    16:43:08.153 (153194000)|USER_DEBUG|[36]|DEBUG|{"code":107,"error":"This endpoint only supports Content-Type: application/json requests, not : application/json."}
という結果が。

ソースを改めて見なおしたら、

    req.setHeader('Content-Type : ', 'application/json');

なんてバカなコーディングを発見。直したら無事動作。




以下コードを張っておきます。iPhone側はparse.comのサンプルを貼っただけです。

■Trigger■


trigger KickFromChatter on FeedItem (before insert) {

    FeedItem[] feeds = Trigger.new;
    List<FeedItem> arrayFeedItem = new List<FeedItem>();
    
    for (FeedItem feed : feeds) {
        String strBody = feed.Body;
        if (strBody != null && strBody.contains('$go')) {
            arrayFeedItem.add(feed);
        }
    }
    
    if (arrayFeedItem.size() > 0) {
        CallParse batch = new CallParse();
        batch.arrayFeedItem = arrayFeedItem;
        
        Database.executeBatch(batch);
    }
}


■Batch Apex■


global class CallParse implements Database.batchable<FeedItem>, Database.AllowsCallouts {
    
    global List<FeedItem> ArrayFeedItem;
    
    global Iterable<FeedItem> start(Database.BatchableContext info) {
        Iterable<FeedItem> i = arrayFeedItem;
        
        return i;
    }
    
    global void execute(Database.BatchableContext info, List<FeedItem> feeds) {
        for (FeedItem feed : feeds) {
            String str = feed.Body;
            String strPayload = '{"channels" : [""], "data" : {"alert" : "' + str + '"}}';
            Blob blobPayload = Blob.valueOf(strPayload);
            
            // Instantiate a new http object
            Http h = new Http();
            
            HttpRequest req = new HttpRequest();
            req.setEndpoint('https://api.parse.com/1/push');
            req.setMethod('POST');
            
            req.setHeader('X-Parse-Application-Id', 
                                   'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa');
            req.setHeader('X-Parse-REST-API-Key',
                                   'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb');
            req.setHeader('Content-Type',
                                   'application/json');
            
            req.setBody(strPayload);
            
            // Send the request, and return a response
            HttpResponse res = h.send(req);
            
            System.debug(res.getBody());
            System.debug(res.getStatus());
            System.debug(res.getStatusCode());
        }
    }
    
    global void finish(Database.BatchableContext info) {
    }
    
}


2013年5月8日水曜日

Force.com:添付ファイルの全文検索

ChatterにMS-Officeのファイルをアップロードし、ブラウザでログインして「検索」から探すとファイルの中身をちゃんと探してくれるのですが、APIからFINDを使ってもヒットせず苦労していましたが、解決策が見つかったのでメモ。

■ContentVersionにあった■

Chatterにアップロードしたファイルを全文検索で探し出したい。

とりあえず、ファイルがどこに格納されているかをコンソールでSOQLで探したところ、ContentVersionにそれらしいレコードが見つかりました。その結果を踏まえてEnterprise/WSCで以下のSOSLを実行したところ、うまく検索してくれました。

  FIND {語句} RETURNING ContentVersion (Id, FirstPublishLocationId, IsLatest, PathOnClient)

ここで、IsLatest = trueが最新版を示すフラグ、PathOnClientはファイル名です。FirstPublishLocationIdには関連するオブジェクトのIDが入っているので、たとえばChatterからアップロードした場合には、
  SELECT Id, Body FROM FeedItem WHERE Id = cvFirstPublishLocationId
てなことを書けば探し出せます。

■試行錯誤■

何もパラメータなしに
  FIND {語句} 
とすると一応全ファイルが対象になるはず。そうでなくとも
  FIND {語句} RETURNING FeedItem (Id, Body, CreatedDate)
などと書けばヒットする、はず。

でも、どっちもダメでした。

それとPDFもダメでした。日本語のPDFはほぼ全滅、英語版PDFもヒットしないことの方が多いという状況です。いろいろな文書ファイルで試してますが、Office 2007ファイルについては今のところ漏れなくヒットします。

マカーの私としては、PDFが対象外ってのはちょっと困るのですが…。

以上、ご参考になれば幸いです。

2013年5月6日月曜日

「倉橋浩一」

でググると、今のところは私の記事がトップに来るけど、日本大学に同姓同名の人がいるな。Facebookでは同じ読みの人が沢山出てくる。

そろそろ何かアウトプットしないといけないなぁ…。

■太っていてびっくり■
OOエンジニアの輪! ~ 第 23 回 倉橋 浩一 さんの巻 ~

■バカというのは治らない病気■
倉橋浩一@WebObjectsは、YS-11を心より愛す|【Tech総研】

■懐かしい…。■
じつはWebObjectsで飯食ってます(連載一覧)

■私の前が林信行さんで次が竹林賢さんってスゲエ■
林檎いとしや Vol.66 倉橋浩一 人生への投資効果

2013年4月18日木曜日

apex:actionSupportについて

小さなネタですが。昨日ひさしぶりにVisualforceを使いました。

apex:pageBlockTableの中でapex:actionSupportを使ったのですが、event="OnRowClick"に反応してくれません。apex:commandLinkに置き換えてactionで呼び出されるメソッドに問題がないことは確認済ですが、それ以前の問題として全然イベントに反応している様子が見られない。

よくよくReferenceを見なおしたらOnRowClickではなくonRowClickとある。

…動きました。えー、case sensitiveなのかよー …と八つ当たりした午後3時でした。

関係ないけど、Apexの大文字と小文字の扱いにところどころ違和感を覚える。まぁ昔と違って、

    public String fullname {get; set;}

と書くとgetter/setterがうまく認識されない、というバグだか仕様は治ったみたいだけど。

2013年4月4日木曜日

Fitbit買ったとです

サラリーマンに転職して3年で10kg太りました…せっかく35kg落としたのに。

基本的に「短期的に成果が出ないとイヤ」な性格ですが、50歳すぎるとちょっとやそっとじゃ痩せない。なので、「短期的な成果」はともかく「日々の運動記録の蓄積」を記録してそれをモチベーションにつなげようと思います。

というわけで、Fitbit oneを買いました。

でもさすがにローラー練習(エアロバイク)には対応していないはず。一応Polarを持っているので、
  • Fitbitでの日々の活動量
  • Withingsで体重と血圧自動記録
  • Polarからの心拍と自転車方面データ
このあたりを統合して表示できるサービスがないもんかと物色してみました。友達のssuwaさんからFitbit対応アプリのリストを教えてもらったので、一つ一つ検索してみたのですが…結局、希望する条件をすべてクリアしているのはMicrosoft HealthVaultでした。そんな予感はしたんですけどね…。

Microsoftは開発ツールだけは妙によくできているのに、アプリもサービスも何か違うんだよなぁ…。

で、結局、FitbitにPolarの結果(消費カロリーと運動時間)を手入力して現在に至る。友達2人しか登録してないんですが…それでも順位が下がると燃えますね。

歩いててもつい遠回りしたりひと駅手前で降りてしまうw

田町〜五反田、直線距離は近いけど、崖のような坂がある

まぁ週7万歩以上☓2週間たってもまだ痩せないわけですが。

2013年4月3日水曜日

JavaFXはじめました

最近JavaFXで遊んでます。

昔の惨状を覚えているので近づいていなかったのですが、FlashはECMAScriptなActionScriptがやっぱり好きになれないので他に何か良いクロスプラットフォームのRich Client開発環境ないか探していたらJavaFX 2.0の記事がヒットしまして。

調べてみたら、GUI builderもそれなりに使えるし、見た目も素のFlex程度には作れるし、まだ試してないけどChartもわりと種類がある。

コードがAndroidっぽくなってしまうところがアレですが。とりあえずNetBeansを使って試しています。ちょっとバギーだけど…Eclipseより見た目が良いし、コード補完も神レベルのXcodeには及ばないもののEclipseよりは絞りこまれているので楽。Threadなんかも慣れ親しんだJavaなので新しく覚えなおさなくて良い。

最大の問題は、調べ物をしようとすると外貨取引がサイドバーに並ぶことぐらいかな。

FX違いだってばよ。

2013年2月23日土曜日

Flashの画像を差し替える

ソースリストなど一切失われてしまって手元にあるのはSWFだけ。その上のビットマップを張り替える必要が生じて、いろいろ試行錯誤しました。

  • swfmill
  • swfmill on Moutain Lion
  • 画像差し替えのために
  • どうやってPNGをエンコードするか
  • Mountain Lion上で動かして見る



■ swfmill ■

というオープンソースのツールがあります。これはSWF<->XMLを相互に変換できるという素晴らしいものです。コマンドラインツールでソースとMac版 /  Windows版が配布されています。Windows版はdylibも含んでいて、インストーラを動かせばすぐ使えるようになるはずです(私の環境ではdylibをインストールする時にアンチウィルスさんに怒られる)。

Macでは以下のような作業が必要になります。
  1. サイトからMac版バイナリをダウンロード&解凍
  2. ターミナル上で必要なdylibを用意して、swfmillを実行
…と書くと簡単なんですが。

■ swfmill on Mountain Lion■

配布されているのはコマンドラインツールのバイナリです。前述の通りlibfreetype6などのdylibが/usr/local/libにインストールされている必要があります。いろいろ試行錯誤しましたが、結果としてこんな感じ
  1. http://xquartz.macosforge.org/landing/からXQuartzをダウンロード&インストール
  2. MacPortsをインストール
  3. ターミナル上で準備
    1. MacPortsを最新版にアップグレード
      • sudo port self update
    2. 必要なdylibを読み込むためにportを実行
      • sudo port libfreetype +universal
      • sudo port libpng +universal
    3. portが読み込みディレクトリとswfmillが想定するディレクトリが異なるのでシンボリックリンクを置く
      • sudo ln -s /usr/X11/lib/libfreetype.6.dylib /usr/local/lib/
      • sudo ln -s /usr/X11/lib/libpng12.0.dylib /usr/local/lib/
    4. swfmill起動してdylibが足りないというエラーが出たら、2のようにdylib名称の尻尾を取ってportし、3のようにしてシンボリックリンクを置く
これで使えるようになるはずです。

swfmillの使い方は、またターミナル上での作業ですが、
  • SWFからXMLへの変換
    • swfmill swf2xml source.swf result.xml
  • XMLからSWFへの変換
    • swfmill xml2swf source.xml result.swf
と実行するだけです。他にも沢山機能があるようですが、今回使ったのはこれだけ。

蛇足ですがswfmillをダウンロード&解凍したディレクトリの中で作業するのであれば、
  • ./swfmill
とする必要があります。

変換処理を確認するためにSWFからXMLに変換したものをそのままSWFに変換しなおして動作確認してみてください。私の場合は多少SWFのサイズが変わったものの、動作が変わらなかったので一応OKとしました。

■画像差し替えのために■

XMLをエディタなどで開きます。PNG/GIF画像はDefineBitsLossless2のタグ内にbase64でエンコードされています。私の場合、対象画像のwidthから一発で目的の画像を探し当てることが出来たのですが、そういうヒントがない場合はバイナリのサイズなどから虱潰しに試してみるしかないかもしれないですね…みなさんのご健闘を祈ります。さて、あとは、差し替える画像をテキストに変換するだけです。

■どうやってPNG画像をエンコードするか■

ここは悩みました…。JPEGは比較的カンタンで、単純にBase64に変換してその文字列を<data></data>タグで挟めば良いらしいです。でも、GIF / PNGに関しては、技術的な解説記事が出ているし、Rubyでのライブラリもあるんですが…「ドラッグ&ドロップとは言わないが、せめてコマンドライン一発で変換するツール」みたいなのは見当たりません。

SFWMILLの解説ページにXMLに新しい画像ファイルを取り込む方法は書いてあるのですが、いろいろタグの書き方を変えてみたもののうまくいきません。楽をするためならどんな苦労をすることも厭わないので、帰りの電車で眉間に縦ジワ寄せつつ考えていたら…ひらめいた。画像を入れたSWFを作って、それをswfmillで変換すれば良いということにw

  1. 元の画像をswfmillでXMLに変換しておく
    • swfmill swf2xml Source.swf Source.xml
  2. FlashBuilderを起動、新しいFlashプロジェクトを作る
  3. 画像ファイルをプロジェクトに追加
  4. デザインビューでImageを貼り、さっきの画像ファイルをEmbed
  5. あとは何もいじらないで、リリースビルド
  6. bin-releaseの中の <project-name>.swfをXMLに変換
    • swfmill swf2xml image.swf image.xml
  7. 変換したXML(image.xml)をエディタで開き<data>タグを探す。
  8. 新しい<data><data>....</data></data>を古いものの上にコピペ
  9. swfmillでXMLをSWFに変換
    • swfmill xml2swf Source.xml Result.swf

■Mountain Lion上で動かして見る■

そういえばコントロールパネルからapacheを起動できなくなったんですね。これまたターミナルから、

で起動させて、ブラウザ上で
    http://localhost/
とタイプし「It works!」が表示されれば起動は成功。あとはさっき出来上がったswfを
    sudo cp test.swf /Library/WebServer/Documents/
などとコピーして、ブラウザでURLを入力します。
    http://localhost/test.swf
これで思った通りに画像が差し替わっていれば出来上がりです。

それにしても…オレもいろんなことやるなぁ…。

2013年2月21日木曜日

iOS 6にすると起動しない

サマリー

  • 現象:起動しない
  • 調査
  • 解決策1:エミュレータを削除
  • 解決策2:デバッガを変更

夏休みの宿題は8月下旬に始める子供だった私は、例によってリリース直前になって「そういえばiOS 6で試してなかった」ということに気づいて慌てるのだった(完)。



■現象:起動しない■

Xcode 4.5上でiOS 5.1用に作っていたアプリをiOS 6で起動しようとしたら、エミュレータが起動した直後、スプラッシュも出ないうちにアプリが即死します。デバッグコンソールすら出てくれません。

■調査■

XcodeのView->Debug Area->Activate Consoleでログを見るとそこには
    error: failed to attach to process ID 0
というエラーメッセージが出ていました。これをそのままGoogle先生で検索すると、日本語のページと、困った時のStackoverflowさんが2件。経験上、Stackoverflowさんを先に見ます。

■解決策1:エミュレータを削除■

まずその1(「error: failed to attach to process ID 0」)。結果的にはこれが大正解でした。

iOSシミュレータを終了してから、
  • ホーム下のLibrary/Application Support/iPhone Simurator/<対象バージョン>/Applications内の中にあるファイルを全て削除
これだけです。

なお、それ以前にiOSシミュレータの「コンテンツと設定をリセット...」も試したのですが、は効きませんでした。というかリセットしたり言語設定を切り替えるとそのままハングすることがあったのですが、削除してからは問題なしです。

■解決策2:デバッガを変更■

次がこれ(「Failed to Attach to Process ID Xcode」)。デバッガをLLDBからGDBに変更します。
  • XcodeのProduct->Edit Scheme
  • スキーマ編集画面で左側のバーから「Run YourApp.app Debug」を選択し、右側の「Info」タブを選ぶ
  • DebuggerをGDBに切り替える
  • Product->Clean

これでもまだアプリが立ち上がらない場合には、さっきのDebugger選択画面でLaunchを「Wait for YourApp.app to launch」に切り替えて、エミュレータ上でアプリのアイコンをタップしてみてください。それでもダメならDebugger=None。

ただ、一度起動するとLLDBに戻してもうまくいくという報告もあるので(「[Objective-C]iOSシミュレータでのデバッグができなくなった」)、iOSシミュレータでバージョンを切り替えたりした時にエミュレータの設定がおかしくなったのが原因ではないかなと思っています。

これでもうまく行かない場合は、対象アーキテクチャからarm7sを外す(「iPhone 5からはarmv7sアーキテクチャ」)というのもあるようです。特に外部のライブラリがarm7s非対応の場合はこれしかありません。私は先にこっちを試したのですが、変化ありませんでした。

2013年2月14日木曜日

AndroidのソフトキーボードでEditViewが隠れる場合

AndroidでEditTextがソフトキーボードに隠れてしまう件。

シングルラインであれば、AndroidManifest.xmlの該当するactivityの中に
    
    android:windowSoftInputMode="adjustPan"

を追加すれば解決することが多いですが、マルチラインの場合には
   
    android:windowSoftInputMode="stateVisible | adjustPan"

と書かないと最初の1行目しか見えません。よくある「ScrollViewを使う」等のwork aroundも試したのですが、結局、これが一番よく効きました。上記はScrollViewなしでも効いてます。

ただ機種依存やその他の設定による影響もいろいろあるようなので(この記事が参考になりました→「Android キーボードの出現と同時に画面がスクロールしないときに」)、あくまでも私のところでは…という話です。

ってかデフォルトをadjustPanにしといてくれよ>Google

DDMS post-create initでフリーズ

何か今日は沢山のBad Know-howが収集できて嬉しいorz

ADT/Eclipseを普通に終了してからMac(Lion)を再起動、再度ADTを起動したらstatusに

  DDMS post-create init

という表示が出てそのままフリーズしてしまった。Eclipseを強制終了して再起動してもダメ。

ググったら、StackOverflowでwork aroundが見つかりました。ありがたやありがたや。Eclipseがしっかり終了しているのを確認し(生き残っている場合には強制終了)、ターミナルでWorkspaceへ移動し、以下のコマンドを実行します。

  rm .metadata/.lock

これでEclipseは起動しました。いやはや。