2011年12月18日日曜日

Titanium Salesforce module(SalesForce Toolkit for Appcelerator)の使い方 (2)

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
前回のサンプルアプリをちょっと時間が経ってから動かしてみると、

[ERROR] In the error handler looking for a 401, and have a 401

[ERROR] Handleing the 401 error...

とかエラーが出てしまい動きません…

force.comからOAuth2で取得したaccess tokenには有効期限があるのでrefresh tokenを使って再度取得する必要があるのですが…そこが動作していないようです。
force.comのOAuthについて詳しくはこちらで

エラーのコールバックも呼んでくれないので有効期限切れたかどうかも検出できず…

なんとかhackできないかと、こんな感じでモジュールの中身をダンプしてみます。

var FDC = require('com.salesforce');
for (var i in FDC.ForceOAuth) {
 Ti.API.debug(i + ':' + FDC.ForceOAuth[i]);
}

最終的にREST APIの呼び出しはここに来るようです。
[DEBUG] makeRestCall:function (path, callback, error, method, payload, retry) {var restUrl=Ti.Network.decodeURIComponent(fa.instanceUrl)+'/services/data'+path;var xhr=Ti.Network.createHTTPClient();xhr.onload=function(){Ti.API.info("REST Response: "+this.responseText);var data="";if(this.responseText){data=this.responseText;}

callback(data);};xhr.onerror=function(e){Ti.API.error("XHR, error handler..."+"\nDbDotCom.REST.OAuth.refreshToken: "+fa.refreshToken+"\nretry: "+retry+"\n e: "+e.error+"\nXHR status: "+this.status);if(!fa.refreshToken||retry){error(e.error);}else{Ti.API.error("In the error handler looking for a 401, and have a "+xhr.status);if(xhr.status===401){Ti.API.error("Handleing the 401 error...");exports.refreshAccessToken(function(oauthResponse){Ti.API.error("Refresh response... "+oauthResponse);fa.makeRestCall(path,callback,error,method,payload,true);},error);}else{Ti.API.error("Not a 401 error, re-throwing...");error(e);}}};if(fa.usePostBin===true){restUrl="http://www.postbin.org/135onm5";}

xhr.open(method||"GET",restUrl,true)

Ti.API.info("Rest url: "+restUrl);xhr.setRequestHeader("Authorization","OAuth "+Ti.Network.decodeURIComponent(fa.accessToken));xhr.setRequestHeader("Content-Type","application/json");xhr.send(payload);}

出てるログから、exports.refreshAccessToken()の呼び出しの中でエラーが起きてコールバックまで戻って来ないようです。

よく考えたら、OAuth2のrefresh tokenによるaccess token再取得時にはclient secretが必要なはずなのに、モジュールのパラメータなどでどこにもセットしていないのでrefreshできるわけないですね… 未実装なんでしょうか?
※追記:secret要らない仕様に変わってました
ダンプしたソースを参考に、こんな感じでhackしてみました。
※モジュール内で定義されてるobjectのプロパティは動的に書き換えられない?&スコープ的にアクセスできない変数があったので結構無理矢理

使い方は、requireした後にこのファイルをincludeしてパッチを当て、ForceOAuthの代わりにForceOAuth2を使うようにします。ForceOAuth2.openのパラメータにはclient idとclient secretを渡す様にします。

var FDC = require('com.salesforce');
Ti.include('fdc-patch.js');
FDC.ForceOAuth2.open('CLIENT_ID');

パッチしたポイントは2つ。refreshAccessTokenをclient secretを使用して動作する様にしたのと、REST APIのパスの固定部分に /data が含まれていたのを /apexrest が呼べる様に /services までとしたこと。

Winter '12でリリースされたApex RESTを使って公開したAPIのURLは、$instance_url/services/apexrest/... となるので、FDC.ForceOAuth2.makeRestCall('/apexrest/myapi', callback) の様な感じで使える様になります。

marketplaceのモジュールのページには、"This toolkit is maintained by the community and sponsored by salesforce.com. Salesforce.com does not officially support this product."とか書いてあるんですが、パッチとか提供したい場合どこに連絡すればいいんでしょうか… githubとかにソース上がってればforkするのに…

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

2011年12月8日木曜日

Titanium Salesforce module(SalesForce Toolkit for Appcelerator)の使い方

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
最近、Titanium Mobileでforce.comのREST APIを使用したiPhoneアプリを作ってます。

Titanium Mobileについての基本的な知識やTitanium Studioの使い方はある程度知ってる前提で書いているので、Titaniumについて知りたい方は「Titanium Mobileで作る! iPhone/Androidアプリ」などを参考にどうぞ。

Titaniumにはmarketplaceがあり、ソフトウェア部品としてのモジュールや、アプリのテンプレートなどをダウンロードできます(有償のもアリ)。
うまいこと目的に合ったモジュールを見つけて利用すれば開発スピードは向上する…はず…

Salesforce用のモジュールも用意されているので、今回の開発にはこれを利用しました。が、ドキュメントが大して無かったりで取っ掛かり苦労したのでメモとして残しておきます。
今回は、モジュールを使うまでの準備と、サンプルコードの実行まで。

2011年9月20日火曜日

foursquare global hackathon @ Tokyo

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
NYC, SF, Paris, Tokyoで同時開催のfoursquare global hackathon、東京会場のオーガナイザ兼参加者として9/17-18の2日間約42時間ぶっつづけで楽しんできました。

東京会場では参加者15名、応募作品9と他会場と比べて少ない数字でしたが、みんなでワイワイおやつ食べたり深夜居残り組はみんなで銭湯行ったり、いい雰囲気でした。
NYC,SF,Parisの中継映像をスクリーンに映して、音声無しながらもParisがエッフェル塔切り抜いたのカメラの前に貼って遊んでたのに東京タワーで対抗?してみたりw
Togetterまとめ

いきなりですが東京会場内での作品の投票結果発表!
日本時間9/23の正午まで、http://fshackathon.appspot.com/で一般投票が行われているので気に入った作品にぜひ投票をお願いします(何作品でも投票できます)!

1位(会場得票数4): photosquare
作者:中継カメラ設置等いろいろ手伝ってくれた@koogawaさん
内容:近くのvenueに投稿されてる写真をスライドショー表示してくれるiPhone/iPad用アプリ。アプリ審査があるのでまだダウンロードできませんが、http://twitvideo.jp/06TpCから映像見ることができます。
投票する

2位(会場得票数3):The Journalist
作者:今回素晴らしい会場を提供してくださったGaiaXの@aomushi510さんと、直前に@aomushi510さんに誘われて急遽参加のTomoyuki Hisadaさんのチーム
内容:4sqのチェックイン履歴を使用して、自分の行った場所の記事を投稿できる、ソーシャルニュース的なサービス。新聞的なデザインや、細かいところの動きがよくできていて完成度高くてびっくり!
http://journalist.azu.sh/で見れる…はずが残念ながら今エラー出てます…
投票する

同着2位(会場得票数3):4sqlist(仮)
作者:pomu0325(すいません自分です。)
内容:サブベニュー(駅のホームとか、モール内の店舗とか)をグループ化して探しやすくするiPhone/iPad用WEBアプリ。時間が無くて最低限の機能しか作り込めませんでした… 同じアイデアで作り始めた@ikawamotoさんとは違い、手動でサブベニューの情報を編集するアプローチです。http://4sqlist.appspot.com/
投票する

以下、順不動での掲載です。

チーム名:curly
作者:@takochuuさん、@sunny_510さんのチーム
内容:venue毎に、そこにチェックインした人のつぶやきが見えたりランキングが見れたりするサービス。デザインもよくまとまってましたが、公開はまだ…かな?
投票する

Packed Venues
作者:大阪から来てくれた@ikawamotoさん
内容:住所が同じベニューをまとめて表示するiPhoneアプリ。動かしているところの動画はこちら。住所表記のブレが解決できれば自動でまとめられるアプローチの方がいいですね!
投票する

Squarecount
作者:meetup主催等いつも手伝ってくれてる@jayjpn
内容:4sqへのチェックイン回数を表示するだけw。エンジニアじゃないのに締め切りぎりぎりまで頑張ってました。http://squarecount.appspot.com/index.html
投票する

4sq Scouter
作者:自分と同じく携帯用4sq「じゃぽすく」を作っている@jpfoursquareさん
内容:スカウターと言ったらおなじみ、例のアレ風に「強さ」を数値化して表示してくれます。数値には、「複雑な足し算」が使用されているとのことですw http://scouter.jpfoursquare.com/から試せます。
投票する

fourSquare PopUp Utility
作者:The Journalistもチームメンバーで作っているTomoyuki Hisadaさん
内容:4sqのユーザやvenueの情報をマウスオーバーで表示してくれるjsライブラリ。ページ内のfoursquareへのリンクを自動で変換してくれるみたいです。twitter anywhereの4sq版、と言えば分かる人には分かる。使い方等
投票する

Complete Japanese fried chicken delis in MusashiKoyama
作者:@Suger1008 さん
内容:むさこ(武蔵小山)の唐揚げ屋さんを制覇! むさこ愛にあふれるスマートフォン用アプリをTitanium Mobileで作られていました。公開はまだですが解説等はこちら
投票する

応募があったのは以上9作品ですが、他にも時間が足りなかったり、既に作っているものを4sq対応したりと各自ガリガリがんばっていました。

最後に、深夜含め2日連続と無理な条件にも関わらずすてきな会場を提供してくださった、ガイアックスさん、本当にありがとうございました!!!
ハッカソンなどのイベントでよく会場提供をしているそうなので、またお邪魔させていただきます!

2011年3月29日火曜日

[Lift] 画像ファイルにjsessionidを付けない

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
takedasoftさんがUrl Rewrite Filterを使った方法を書いてましたが、Boot.scalaでLiftRule.urlDecorateにルール追加するだけで簡単にできたのでメモ。

val ImgPtn = """(.*\.(?:png|gif|jpg));jsessionid=.+""".r
LiftRules.urlDecorate.append {
  case ImgPtn(url) => url
}

※Lift 2.2からデフォルトではjsessionid付かなくなったのはここに書いてある通りです。

2011年3月22日火曜日

[Scala] sbt jetty-runでhttpsを使用したい

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

方法1: カスタムjetty.xmlを使う

sbt標準だとjetty.xmlを1から書けばOK。
override def jettyConfiguration = new CustomJettyConfiguration {
  override def jettyConfigurationXML = List("/path/to/jetty.xml")
}

この方法だとjetty.xmlにConnectorとかHandlerとか全部自分で書かないといけないので面倒…
参考:jetty.xmlのリファレンス

方法2: sbtにちょっと手を入れてjetty設定用のhookを使う

sbtのソースをcheckoutしてhookが設定できるようにする


参考:Build - simple-build-tool -

sbtのソースをcheckout
$ git clone git://github.com/harrah/xsbt.git
$ cd xsbt

変更したのは下の2行だけ。jetty-runが呼ばれてデフォルトのhttpポートのConnectorの設定直後にcallbackで設定した関数が呼ばれるようにする。
diff --git a/sbt/src/main/scala/sbt/WebApp.scala b/sbt/src/main/scala/sbt/WebApp.scala
index f5a3d09..997294e 100644
--- a/sbt/src/main/scala/sbt/WebApp.scala
+++ b/sbt/src/main/scala/sbt/WebApp.scala
@@ -105,6 +105,8 @@ trait DefaultJettyConfiguration extends JettyConfiguration
        def parentLoader: ClassLoader
        def jettyEnv: Option[File]
        def webDefaultXml: Option[File]
+
+       def callback: Option[AnyRef => Any] = None
 }
 abstract class CustomJettyConfiguration extends JettyConfiguration
 {
diff --git a/sbt/src/main/scala/sbt/jetty/LazyJettyRun.scala.templ b/sbt/src/main/scala/sbt/jetty/LazyJettyRun.scala.templ
index 263afa6..4ca4b0e 100644
--- a/sbt/src/main/scala/sbt/jetty/LazyJettyRun.scala.templ
+++ b/sbt/src/main/scala/sbt/jetty/LazyJettyRun.scala.templ
@@ -54,6 +54,7 @@ private object LazyJettyRun${jetty.version} extends JettyRun
                                case c: DefaultJettyConfiguration =>
                                        import c._
                                        configureDefaultConnector(server, port)
+                                       c.callback.foreach(_(server))
                                        val webapp = new WebAppContext(war.absolutePath, contextPath)
                                        webDefaultXml.foreach{webDefaultXml:File => webapp.setDefaultsDescriptor(webDefaultXml.toString)}

sbtのビルド

$ sbt update generate-loader-compat proguard "project Simple Build Tool" publish-local
※generate-loader-compatしないとjetty6用のクラスが一部生成されなかった。(jettyは6->7でパッケージ名が変わったので、sbtのソースではテンプレート化して6,7,7.2用のscalaソースが吐かれるようになってる

ssl用証明書の準備

JDKのkeytoolを使用。とりあえずオレオレ証明書で。
$ keytool -keystore src/test/resources/keystore -alias jetty -genkey -keyalg RSA
$ keytool -selfcert -validity 1024 -keystore src/test/resources/keystore -alias jetty
※keystoreのパスワードを聞かれるので入力します

sbt プロジェクト側の準備

build.propertiesの sbt.version=0.7.5.RC0 にしてsbt実行、先に標準の0.7.5.RC0を入れておく。

ビルドしたsbt/target/scala_2.7.7/sbt_2.7.7-0.7.5.RC0.jar をプロジェクトのproject/boot/scala-2.7.7/org.scala-tools.sbt/sbt/0.7.5.RC0/sbt_2.7.7-0.7.5.RC0.jar に上書き。

project/build/MyProject.scala の設定。callbackでjettyのServerインスタンスにSslSocketConnectorを追加。ClassLoaderが違うので普通に書くとClassCastExceptionが発生するのでリフレクションで。
  override def jettyConfiguration: JettyConfiguration =
    new DefaultJettyConfiguration {
      def classpath = jettyRunClasspath
      def jettyClasspath = MyProject.this.jettyClasspath
      def war = jettyWebappPath
      def contextPath = jettyContextPath
      def classpathName = "test"
      def parentLoader = buildScalaInstance.loader
      def scanDirectories = Path.getFiles(MyProject.this.scanDirectories).toSeq
      def scanInterval = MyProject.this.scanInterval
      def port = jettyPort
      def log = MyProject.this.log
      def jettyEnv = jettyEnvXml
      def webDefaultXml = jettyWebDefaultXml
      override def callback = Some((server: AnyRef) =>  {
        val cl = server.getClass.getClassLoader
        val ssl = cl.loadClass("org.mortbay.jetty.security.SslSocketConnector").newInstance.asInstanceOf[{
          def setPort(p: Int): Unit
          def setMaxIdleTime(t: Int): Unit
          def setKeystore(s: String): Unit
          def setPassword(s: String): Unit
          def setKeyPassword(s: String): Unit
          def setTruststore(s: String): Unit
          def setTrustPassword(s: String): Unit
        }]
        val addConnector: java.lang.reflect.Method = server.getClass.getMethod("addConnector", cl.loadClass("org.mortbay.jetty.Connector"))

        ssl.setPort(8443)
        ssl.setMaxIdleTime(30000)
        ssl.setKeystore(info.projectPath + "/src/test/resources/keystore")
        ssl.setPassword("password")
        ssl.setKeyPassword("password")
        ssl.setTruststore(info.projectPath + "/src/test/resources/keystore")
        ssl.setTrustPassword("password")
        addConnector.invoke(server, ssl)
      })
  }
※上記はjetty6の場合で、jetty7の場合はパッケージ・クラス名がちょっと違います。

これで、jetty-runで8080でhttp、8443でhttpsが起動します。

2011年2月9日水曜日

[Scala][sbt]DefaultWebProjectでwarではなくjarも作りたい

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
DefaultWebProjectだとsbt packageタスクでwarが作られますが、jarも作りたい時があった場合、こんな感じで。
lazy val jar = packageTask(packagePaths, jarPath, packageOptions).dependsOn(compile) describedAs "Creates a jar file."
sbt jarでjarファイルが作られます。

2011年2月7日月曜日

[GAE] bulkloaderをGAE/Jで使う

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
python版appengine-sdkを使ってcsvファイルからデータをDatastoreにロードすることができます。

準備

web.xmlにremote_api用のServletの設定を書く

<servlet>
  <servlet-name>remoteapi</servlet-name>
  <servlet-class>com.google.apphosting.utils.remoteapi.RemoteApiServlet</servlet-class>
</servlet>
<servlet-mapping>
  <servlet-name>remoteapi</servlet-name>
  <url-pattern>/remote_api</url-pattern>
</servlet-mapping>
security-constraintも追加しておいた方が良さげ。
<security-constraint>
  <web-resource-collection>
    <url-pattern>/remote_api</url-pattern>
  </web-resource-collection>
  <auth-constraint>
    <role-name>admin</role-name>
  </auth-constraint>
</security-constraint>

ロード設定用のyamlファイルを書く

python_preamble:
- import: base64
- import: re
- import: google.appengine.ext.bulkload.transform
- import: google.appengine.ext.bulkload.bulkloader_wizard
- import: google.appengine.api.datastore
- import: google.appengine.api.users

transformers:

- kind: Scores
  connector: csv

  connector_options:
    encoding: ms932

  property_map:
    - property: __key__
      external_name: id
      import_transform: int

    - property: name
      external_name: name

    - property: score
      external_name: score
      import_transform: transform.none_if_empty(float)
  • キーにidではなく文字列(name)を使用する場合は、import_transform: intは不要。
  • import_transformに他に指定できる例
    日付: transform.import_date_time('%Y/%m/%d %H:%M')
    固定値: import_transform: lambda x:1
    数値変換: import_transform: lambda x:int(x)*100
    ※lambdaは引数の:の後にスペースを空けるとダメな様です

    ロード用CSV
    id,name,score
    1,Alice,5.0
    2,Bob,2.5
    3,Caroline,
    

    ロード用コマンド

    appcfg.py upload_data --filename=test.csv --config_file=test.yml --url=http://{appid}.appspot.com/remote_api --application={appid} --kind=Scores -v
    
    ※{appid}はアプリケーションIDに置き換える

    security-constraintの設定をしている場合、毎回管理者アドレス/パスワードの入力が必要です。
    ローカルのdev_appserverだとエラーが出て動きませんでした…

    こんな感じに反映されます。
    ※1万件だと約500秒(8分ちょっと)くらいかかりました(CPU Timeを無料Quotaの10%ほど消費します)

    その他注意点

  • slim3を使っている場合、filterの設定があると、"ProtocolBufferDecodeError: corrupted"の様なエラーが出てしまうので一時的にコメントアウトして回避しました。(※他にいい方法があれば教えて下さい…

    2/11追記: bulkload用にfilterをコメントアウトしたversionをデプロイしておいて、--url=http://latest.bulkload.{appid}.appspot.com/remote_api みたいに指定するといいかも

    3/22追記:slim3ではなくscenic3の方の設定でした。matcherで/remote_apiを除外したらOK。

    参考: Ikai Lan says / Using the bulkloader with Java App Engine
  • 2011年1月15日土曜日

    [Blogger] スマートフォン用テンプレートを有効にする

    このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
    いつの間にかβ版としてスマートフォン用テンプレートの機能が追加されてたので有効にしてみた。

    draft.blogger.com の方からログインするとβ版機能が使えます。

    有効にするには、設定 > メールとモバイル > モバイル テンプレートを表示する で はい を選ぶだけ。スマートフォンの場合は自動でリダイレクトしてくれるようです。

    ガラケーからもurlの最後に?m=1を付けるとモバイル用テンプレートで表示してくれましたが、ページ遷移にjavascriptが使われているようで残念ながら記事一覧以降進めませんでした…。

    2011年1月12日水曜日

    [Lift] Liftで処理したくないリクエストの設定

    このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
    web.xmlで全てのリクエストがLiftで処理されるようになっているので、Lift以外のサーブレット等で処理したい場合に困ります…
    <filter-mapping>
      <filter-name>LiftFilter</filter-name>
      <url-pattern>/*</url-pattern>
    </filter-mapping>
    

    Boot.scalaのdef bootにこんな感じで書けばOK。
    LiftRules.passNotFoundToChain = true
    LiftRules.liftRequest.append {
      case Req("_ah" :: _, _, _) => false
      case Req("remote_api" :: _, _, _) => false
    }
    

    ※filter-mappingで除外ルールが書ければいいんだけど…

    2011年1月1日土曜日

    [Scala] sbt-appengineなプロジェクトをIntelliJ IDEAでデバッグ

    このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
    @teaplanetさんにヒントいただいてIDEAのsbt consoleでdev-appserver-startできるようになったので、IDEA上でブレークポイントで止めたりできるように設定してみた。

    class MyProject(info: ProjectInfo) extends AppengineProject(info) {
      //... 中略
      //APPENGINE_SDK_HOME設定の代わり
      override val appengineSdkPath = Path.fromFile("/Users/pomu0325/dev/appengine-java-sdk-1.4.0")  
      
      //dev-appserver-startの際のJVMオプション
      override val devAppserverJvmOptions = List("-Xdebug", "-Xrunjdwp:transport=dt_socket,server=y,address=2011,suspend=y") ++ super.devAppserverJvmOptions 
    }
    

    この設定でsbt consoleからdev-appserver-startするとリモートデバッガで接続できる状態で起動するので、後はIDEAからデバッガ接続する。

    IDEAのメニューからRun -> Edit Configurations -> [+] -> Remote を選んで、Nameは適当に、PortにdevAppserverJvmOptionsのaddress=xxxxで指定したポートを入れる(↑の例だと2011。適当に開いてるポート設定すればOK)。

    設定したら、Run -> Debug... で選んでデバッグ開始するとdev-appserverのJVMにアタッチされてブレークポイントで止まってくれる。

    ※Before launchの"Run sbt action"でdev-appserver-startが指定できればsbt consoleからいちいちdev-appserver-startしなくても一発で起動できそうだけど一覧に出てこないし、直接指定してもダメでした…

    参考:sbt-appengineの基本的な設定等は"sbtでGAEをする場合の注意点など色々。"が参考になります。