2014年2月7日金曜日

[force.com] 変更セットとかパッケージが到着するのを監視して通知する方法

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
変更セットやパッケージがなかなかデプロイしたいインスタンスに到着しなくてイライラやきもきしたことはないでしょうか?

何度も⌘R(またはF5)を押して時間を無駄にしないでいい方法を見つけました。

Auto Refresh Plus(以後ARP)というChrome拡張を使うと、指定した秒数で自動的にページをリロードしてくれて、指定したキーワードがページ内に現れたら通知をしてくれます!!!

まずは、Chrome Web Storeから、ARPをインストールします。
https://chrome.google.com/webstore/detail/auto-refresh-plus/oilipfekkmncanaajkapbpancpelijih

設定画面で、キーワードをウォッチする機能、Page Monitorを有効にします。
気づきやすい様に通知時に音も鳴る様にしておきます。

準備はこれで完了です。

使い方


sandboxから、本番環境に変更セットを送ります。

本番環境で、「受信変更セット」のページを開きます。まだ来てないですね。ここでARPの出番です。

あまり負荷をかけない様に1分間隔くらいにして、Page Monitorのキーワードには変更セット名を入れ、スタートします。

もうこれで監視がスタートしたので、他の作業とか進めちゃいます。

(待ってる間に、このblogを書いています…)

(書き終わってしまいました…)

あ、通知が来ました。
これで時間を無駄にしなくてすみますね!

※同じやり方でキーワードを変えれば、パッケージがなかなか来ないとかでも監視ができます



2013年12月24日火曜日

リードの「取引の開始」をSalesforce1アプリにパブリッシャーアクションで追加する

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
Salesforce1に変わっても相変わらずリードの取引先変換が無い!!!とIdeaExchangeでも取り上げられています。

ちょっと前に「カレンダーをSalesforce1に追加する方法」を見たのでインスパイアされてパブリッシャーアクションを簡単に作ってみました。

Visualforceページを作る

こんな感じで、先ほどのカレンダーのやつまるパクリです。
{!id}でリードのIDを埋め込んで、isdtp=nvでヘッダなどを隠してます。

リードにアクションを追加

上のVisualforceページを指定します。

レイアウトに追加

リードのレイアウトに作成したアクションをドラッグ&ドロップして追加。

iPhoneのSalesforce1アプリから使ってみる

標準の画面を無理矢理埋め込んだだけなので画面サイズがアレですが…とりあえず使える様になりました!

※ネタが無かったのでForce.com Advent Calendar 2013不参加予定でしたが急遽プチネタができたので遡っての参加です

2013年9月8日日曜日

Salesforce Developer Conference Tokyo 2013 (Mini Hackレポート)

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
Salesforce.comの開発者向けイベント、Salesforce Developer Conference Tokyo 2013行ってきました。

去年のCloudforceでもあったMini Hack、今回は豪華賞品を揃えて全4問で再登場したの挑戦しました。なんと4問提出で、抽選で1名なんとDreamforceツアーご招待。

4問の課題は以下の内容:

とりあえず簡単そうなものから…と思って課題流し読みして、Aからやろうと思ったらTreaure Dataのアドオン追加がなぜかエラー…。Kからやることに。

課題K「Heroku Platform APIをつかってHerokuを操作しよう!」

要は、最近公開されたPlatform APIを試してみてね、という課題Platform APIのチュートリアル通りにコマンドラインからcurlでAPIの動作を確認すればOK。
一部、チュートリアルのコマンド例そのままだとContent-Type: application/jsonヘッダが足りなくてエラー出てたので報告しときました。
Heroku使った事あれば簡単すぎますね。

まだTreasure Dataのアドオンが追加できなかったので次はH。

課題H「Mobileアプリケーションを作ってみよう!」

こちらも数ヶ月前にリリースされたMobile Packを試してみてね、という課題。Mobile Packを使用して取引先の一覧を表示するモバイル用ページを作ればOK。
jQuery mobile, AngularJS, Backbone.js, Knockout, appery.io, Sencha, Xamarin, Codiqaと多数のフレームワークに対応してる様です(半分くらい知らない)。
とりあえず普段使っているAngularJSでやろうとサンプルコードを落としてきて動かしてみるが、app.jsとかを静的リソースに突っ込むのが面倒そうで、jQuery mobileに変更(@a_kurataniさんがjQuery mobileでやったら簡単だったと言ってたのでw)。
サンプルコードの取引先責任者の一覧表示するVisualforce Pageを覗いてみるとJSに直にSOQLが…。オブジェクト名と項目名だけ変えておしまいですね。

@mino0123さんのツイートを見て、アドオン追加が復活してるっぽかったのでAを再開。

課題A「Treasure Dataアドオンを使ってみよう!」

Treasure DataのHerokuアドオンを試してみてね、という課題
とりあえず課題シートの指示通りにデモアプリをgithubからcloneして、コマンドラインからアドオン追加、プラグイン追加、デプロイをしてみます。
デモアプリをブラウザで開いても、Hello World!が出るだけで動きが分からないのでソースを追ってみると、アクセスの都度裏でTDにログを送る様です。
「サンプルアプリを修正して画面の表示を変更してください」って画面の修正だけで本当にいいのか気になりながら、変更するところはタイトルくらいしか無かったのでHello World! をHoge Hoge! みたいに書き換えて聞いてみたらこれでOKw(TD関係ないじゃん)
ちなみにRubyを全然書いたことがないので初めて.erbファイルをさわった(デモアプリはsinatra)。

3問でMBA狙いにしようかと思ったけど時間あったから4問目も挑戦。

課題C「オフラインHack - SmartStore」

Mobile SDKを使ってオフライン対応のアプリを作れ、という課題と勝手に解釈。
SmartSyncがBackbone.js使われてるとか基調講演で聞いたので、ちょっと難易度高いかなーと思って思い込んで避けてたやつ。
Mobile SDKは前に落として試してたけど、念のため最新版をgit pull。
iOS版のサンプルアプリをXcodeからとりあえず動かしてみて、問題を良く読んでみると…「提出方法:サンプルの動作が画面で確認できたら、お声掛けください」
始める前に、終わってた。
@yonet77さんがサンプルコードでビルドエラーが…て困ってたのでちょっと見てみたら、最近iOS7対応したときに見覚えのある定数が。github上のmasterがすでにiOS7対応されてて、β版Xcodeじゃないとヘッダ足りなくて動かないので、β版Xcode5持ってない場合は古いリビジョン取り直さないとダメという罠

4問解いておいて、Dreamforceにするか、MBAにするかギリギリまで迷ったけど、@yonet77さんがDreamforceに1人目で申し込んだので、このままだと確定で面白くないなーということで同じくDreamforceに申し込んだら、なんと抽選当たってしまいました!

Developer Conference参加自体も直前で決めたのに、なんか申し訳ない…

最後に、傾向と対策

  • なんか難しそうだけど課題の意図はとりあえず手を動かしてさわってみてね、という所だと思うのでチュートリアルレベル。とにかくやってみる
  • git, herokuは使えて当たり前? 環境は整えておく
  • ノートパソコンの電源アダプタは持ってくる(電池切れた…て人がちらほら)

課題カードが無くなるの早かった割には、参加者少なかったんじゃないかな…
あと提出方法、応募方法が分かりにくかったという声も…
SDUG(Salesforce Developer User Group)に参加してると最新情報に強くなれますよー、とちょっと宣伝で締めにします。



2012年12月4日火曜日

[force.com] CSV添付のメールをスケジューリングして送る

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

とある案件で毎日CSVをメールに添付して送る要件があり、実装できるまでの顛末のメモです。 問題は、添付するCSVの文字コードがShift_JIS、改行コードがCRLFじゃないといけないこと…

1. まずはControllerとVisualforceでSJISのCSVを吐き出してみる

public class CSVController {
    public String getCRLF(){return '\r\n';}
    public List<Account> accounts{get;set;}
    public CSVController() {
       accounts = [select id, name from Account];
    }
 }

{!a.id},{!a.name}{!CRLF}

こんな感じで、/apex/CSVPage でSJISのCSVがダウンロードできた。簡単すぎる。

2. メール添付して送ってみる

スケジュールApexでスケジューリングする予定なのでScheduableインタフェースを実装して、こんな感じで。

global class CSVMail implements Schedulable {
    global void execute(SchedulableContext sc) {
        Messaging.SingleEmailMessage msg = new Messaging.SingleEmailMessage();
        msg.setSubject('テスト');
        msg.setPlainTextBody('テスト');
        msg.setToAddresses(new String[]{'pomu0325@gmail.com'});
       
        Messaging.EmailFileAttachment att = new Messaging.EmailFileAttachment();
        att.setFileName('テスト.csv');
       
        // 添付CSVをVisualforceページから読み込む
        PageReference page = new PageReference('/apex/CSVPage');
        att.setBody(page.getContent());
        msg.setFileAttachments(new Messaging.EmailFileAttachment[]{att});

        Messaging.sendEmail(new Messaging.SingleEmailMessage[]{msg});
    }
}

開発者コンソールで、new CSVMail().execute(null); とかすればメール送信成功! あとはスケジュール設定するだけと思って「Apexをスケジュール」しても、エラーで送られないんですね…

PageReferenceのリファレンス見てみると、「This method can't be used in: … Scheduled Apex …」って使えないじゃん…どうしよう。

3. 外部のサーバで文字コードだけ変換する

Apexで文字コード変換するのが簡単にはいかなさそうなので…外部サーバで文字コード変換する為だけのapiの様なものを置いて、コールアウトで使ってみる作戦。

ScalaのUnfilteredてフレームワークで、こんな感じでデプロイしてみた。
※自分は某G社のクラウドに置きましたがherokuに置くとかいいですね!

  object UtilPlan {
    def intent = Intent {
      case r @ POST(Path("/utils/nkf")) & QueryParams(p) =>
        val encoding = p("encoding").headOption getOrElse "MS932"
        val payload = new String(Body.bytes(r), "utf-8")
        
        Ok ~> ResponseBytes(payload.getBytes(encoding))
    }
  }

これを使って、Apex Classはこんな感じに。 実行前に「セキュリティのコントロール>リモートサイトの設定」で外部サーバのURLの追加も忘れずに。

global class CSVMail implements Schedulable {
    global void execute(SchedulableContext sc) {
        Messaging.SingleEmailMessage msg = new Messaging.SingleEmailMessage();
        msg.setSubject('テスト');
        msg.setPlainTextBody('テスト');
        msg.setToAddresses(new String[]{'pomu0325@gmail.com'});
       
        Messaging.EmailFileAttachment att = new Messaging.EmailFileAttachment();
        att.setFileName('テスト.csv');
       
        /*
        // 添付CSVをVisualforceページから読み込む
        PageReference page = new PageReference('/apex/CSVPage');
        att.setBody(page.getContent());
        */
        // 添付CSVを文字列で生成して外部サーバで文字コード変換
        String csv = CSVMail.csv([select id, name from Account]);
        HttpRequest req = new HttpRequest();
        req.setEndpoint('https://ひみつ/utils/nkf');
        req.setMethod('POST');
        req.setBody(csv);

        Http h = new Http();
        HttpResponse res = h.send(req);
        att.setBody(res.getBodyAsBlob());
       
        msg.setFileAttachments(new Messaging.EmailFileAttachment[]{att});
        Messaging.sendEmail(new Messaging.SingleEmailMessage[]{msg});
    }
   
    private static String csv(List<Account> accounts) {
        String csv = '';
        for (Account a : accounts) {
            csv += a.id + ',' + a.name + '\r\n';
        }
        return csv;
    }
}

これもまた、開発者コンソール等からの実行だとOKだけど、スケジュールするとコールアウトがダメ… と思ったけど@future(callout=true)を付ければ非同期実行でコールアウト呼べることが判明したので@futureなメソッドにメール送信の処理を移動して完成!

global class CSVMail implements Schedulable {
    global void execute(SchedulableContext sc) {
        CSVMail.sendCSV();
    }
   
    @future(callout=true)
    private static void sendCSV() {

        Messaging.SingleEmailMessage msg = new Messaging.SingleEmailMessage();
        msg.setSubject('テスト');
        msg.setPlainTextBody('テスト');
        msg.setToAddresses(new String[]{'pomu0325@gmail.com'});
       
        Messaging.EmailFileAttachment att = new Messaging.EmailFileAttachment();
        att.setFileName('テスト.csv');
       
        /*
        // 添付CSVをVisualforceページから読み込む
        PageReference page = new PageReference('/apex/CSVPage');
        att.setBody(page.getContent());
        */
        // 添付CSVを文字列で生成して外部サーバで文字コード変換
        String csv = CSVMail.csv([select id, name from Account]);
        HttpRequest req = new HttpRequest();
        req.setEndpoint('https://ひみつ/utils/nkf');
        req.setMethod('POST');
        req.setBody(csv);

        Http h = new Http();
        HttpResponse res = h.send(req);
        att.setBody(res.getBodyAsBlob());
       
        msg.setFileAttachments(new Messaging.EmailFileAttachment[]{att});
        Messaging.sendEmail(new Messaging.SingleEmailMessage[]{msg});
    }
   
    private static String csv(List<Account> accounts) {
        String csv = '';
        for (Account a : accounts) {
            csv += a.id + ',' + a.name + '\r\n';
        }
        return csv;
    }
}

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をする場合の注意点など色々。"が参考になります。

    2010年12月31日金曜日

    [Lift] Lift2.2でCookieが無い場合にjsessionidがリンクに付加されない

    このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
    #658 Supressing URL rewriting for jesessionid from Lift の修正により、デフォルトでjsessionidが付かないように動作が変わっています。

    Boot.scalaで、
    LiftRules.encodeJSessionIdInUrl_? = true
    
    してやればOK。

    Liftで携帯サイト作ってる人とかはアップデートの際に要注意!

    2010年12月30日木曜日

    [Scala] dispatchでContent-Length付きのmultipartなPOSTする方法

    このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
    とある写真投稿サービスのAPIにdispatch(0.7.8)を使って実装しようとした時のこと。APIのレスポンスでContent-Lengthが無いって怒られました。

    dispatch-mimeを使うとmultipart/form-data形式でPOSTができるのですが、普通に実装するとContent-Lengthを送ってくれないようです。

    ソースコメントにも書いてあるし。
    Note that when using an InputStream generator, chuncked encoding will be used with no Content-Length header

    Fileが引数の方を使えば良さげだけどGAEからだとFile使えません。dispatchのソースとapache-httpclientのjavadoc眺めて工夫したらできました。

    import dispatch._
    import dispatch.Http._
    import dispatch.mime.Mime._
    import org.apache.http.entity.mime.content.ByteArrayBody
    
    var req = ... //普通にdispatchのRequest作る
    req = req next req.add(paramName, new ByteArrayBody(data, contentType, filename)) //ByteArrayBodyを指定
    

    ※ただし、ByteArrayBodyはhttpclient 4.1(現在BETA1)から追加されるクラスなので4.1のjarを使わないとダメ(dispatch 0.7.8のdependencyはhttpclient 4.0になってた)。
    sbtならこんな感じで。
    val httpmime = "org.apache.httpcomponents" % "httpmime" % "4.1-beta1"
    

    2010年9月6日月曜日

    [Lift][GAE] Scala座01でLTしてきました

    このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
    第1回Scala座に参加してきました。実はスタッフとして少々お手伝いもしたのですが大した事ないので(ry

    収穫は…
    • いろいろと勉強不足だなぁと刺激を受けた(FP的な意味で)
    • Twitter等で気になっていた人々にリアルで会えた
    • ワラビモチ!←マイブーム
    といったとこでしょうか。

    「Lift on GAE/J」ネタでLTさせてもらったのですが、先にまとめてしまうと、「LiftはGoogle AppEngineには向いてない」てことです。詳しくは「Lift on GAE/J」のスライド見てください。

    LTやろうと思ったきっかけは、自分でLiftも(Scalaも)GAEもよく分かってないうちにLift+GAEという組み合わせを選んでしまったのですが、いろいろと苦労したので… 最近TwitterでLiftをGAEで動かしてみるとかのtweetを時々見かけ、危険だよー、ということを広めたかっただけです。

    ただ、勘違いしないで欲しいのは、Liftがダメと言っている訳ではなく、LiftはフルスタックのWebフレームワークとして、慣れれば生産性よくAJAX/Cometを使ったリッチなWebアプリを作れる素晴らしいフレームワークだと思っています。クラウドという性質上、GAEには様々な制限があるので、GAE上で動かしたいのであれば他のもう少し軽量なフレームワークが向いていると思います(まだあまり試せていませんが…)。

    Scala座の企画をして下さった@keisuke_nさん、Scalaを勉強していく上で参考にさせていただいたり助けていただいたりで本当にいろいろとお世話になっている@yuroyoroさん、@kmizuさん、@takedasoftさん、会場・懇親会でお話させていただいたみなさま、本当にありがとうございました。ぜひ第2回でもよろしくお願いします!

    2010年5月15日土曜日

    [OAuth] 携帯でTwitterのOAuthを無理やりやってみた

    このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
    2011-07-04追記: 今はTwitterのOAuth画面もガラケー対応されてます。自動でリダイレクトはされないのでユーザによるクリックは相変わらず必要ですが。

    モバイルフォースクエアの開発初期に、OAuthでTwitterの認証をやろうとしていた時に試した結果です。手元にあったAU,docomoの端末で確認しています。

    AU(W61CA)でTwitterのOAuthをやってみた

    ログイン画面 承認画面
    特に問題なし! 自動でリダイレクトはされないが、リンクを手動でクリックすれば動作はOK!

    docomo Cookie対応機種(N-07A)でTwitterのOAuthをやってみた

    ログイン画面 承認画面
    文字化けがひどいが、ID・パスワード欄っぽいところに入れて1つ目のボタン(ログイン)を押せば承認画面に飛ぶ。 自動でリダイレクトはされないが、リンクを手動でクリックすれば動作はOK!

    docomo Cookie非対応機種(SH704i)でTwitterのOAuthをやってみた

    ログイン画面 承認画面
    両方とも送信ボタン…。1つ目を押せば承認画面に飛ぶ。 文字化けがひどいが、リンクを手動でクリックすれば動作はOK!

    まとめ

     docomoだと文字化けがひどいが、OAuthの機能的には動作する。ただし最後のリダイレクトを手動でクリックしないといけないのでユーザにこれを強いるのは難しい。
    ※実際、OAuth開始前の画面に注意書きを入れたにもかかわらず、最後のリダイレクトのクリックが分からず、トークンが取得できていないユーザ多数いた
     PIN入力方式にすればよいが、PC・スマートフォンでも同じコンシューマキーを使いたいので、PC・スマートフォンでのユーザビリティが落ちるので却下。PCでOAuth認証だけ先にさせるのも携帯のみのユーザのことを考えると却下。
    →結果、xAuthを使うことにした。

    [OAuth] 携帯でfoursquareのOAuthを無理やりやってみた

    このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
    モバイルフォースクエアの開発初期に、OAuthでfoursquareの認証をやろうとしていた時に試した結果です。手元にあったAU,docomoの端末で確認しています。

    AU(W61CA)でfoursquareのOAuthをやってみた

    ログイン画面 承認画面
    日本語文字化け… 文字化け&Denyが押せないが、動作はOK!

    docomo Cookie対応機種(N-07A)でfoursquareのOAuthをやってみた

    ログイン画面 承認画面


    デザインは崩れるが、動作はOK! デザインは崩れるが、動作はOK!

    docomo Cookie非対応機種(SH704i)でfoursquareのOAuthをやってみた

    ログイン画面→表示が崩れ、ログインボタンを押しても同じ画面が繰り返し表示されるだけ…。
    ※当時はログインできていた記憶が…、サーバ側の実装が変わったかもしれないです


    まとめ

    Cookie対応機なら文字化けしたりデザインは崩れるが、OAuthの処理自体は正常。文字化けがひどい状態ではサービス提供できないので、foursquareの中の人に日本の携帯ブラウザ事情を説明し、Auth Exchange採用。

    2010年4月27日火曜日

    [Scala][GAE] dispatchのOAuthをGAEで使う

    このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
    dispatchが内部でcommons-httpclientを使っていて、GAEでの禁止クラスを使っているのでそのままだとエラーになった。ので無理やりimplicit defでURLFetchのHTTPRequestに変換することで回避してます。こんな感じ。

    import dispatch._
    import com.google.appengine.api.urlfetch._
    import java.net.URL
    
    object DispatchHelper {
      /**
       * convert dispatch.Request -> String
       */
      implicit def r2s(r: Request) = {
        r.host.get + r.req.getRequestLine.getUri
      }
    
      /**
       * convert dispatch.Request -> com.google.appengine.api.urlfetch.HTTPRequest
       */
      implicit def dispatch2gae(r: Request): HTTPRequest = {
       val isPost = r.req.getMethod == "POST"
       val g = new HTTPRequest(new URL(r), if (isPost) HTTPMethod.POST else HTTPMethod.GET, FetchOptions.Builder.withDeadline(10))
       r.req.getAllHeaders.foreach(h => {g.addHeader(h); println(h)})
       r.req match {
        case p: org.apache.http.client.methods.HttpPost =>
         val payload = new java.io.ByteArrayOutputStream
         p.getEntity.writeTo(payload)
         g.setPayload(payload.toByteArray)
         println(p.getEntity.getContentType)
        case _ =>
       }
       g
      }
    }
    

    こんな感じでimplicit defを宣言してimport DispatchHelper._すれば、dispatchでリクエストを作って、URLFetchService.fetch()の引数にそのまま渡せばOK。

    dispatchの解説はこちら

    2010-06-26追記

    2chで質問受けてコードが一部足りなかったので追記しました。