PureScript で DOM を操作

PureScript の下記ライブラリを使って簡単な DOM 操作を試してみました。

ソースは http://github.com/fits/try_samples/tree/master/blog/20160125/

はじめに

PureScript を使って実装するものと同等の処理を JavaScript で書いてみました。 id で指定した DOM ノードの textContent を変更するだけの簡単な処理です。

sample.js
var Sample = {
    updateContent: (id, content) => {
        var node = document.getElementById(id);

        if (node) {
            node.textContent = content;
        }
    }
};

下記の HTML で実行してみます。 (PureScript の方は updateContent の呼び出し部分が少し異なります)

index.html
<!DOCTYPE html>
<html>
<body>
    <h2 id="d"></h2>

    <script src="sample.js"></script>
    <script>
        Sample.updateContent('d', 'sample javascript');
    </script>
</body>
</html>
実行結果

Web ブラウザで表示した結果は以下の通りです。

f:id:fits:20160125204319p:plain

purescript-dom の場合

pulp init でプロジェクトを作成し、pulp dep installpurescript-dom をインストールします。

なお、pulpgulp を事前にインストール (npm install) しておきます。

purescript-dom インストール
> pulp init
・・・

> pulp dep install purescript-dom --save

src/Main.purs を編集し updateContent 関数を実装します。

以下のように型まわりに注意が必要です。

  • (a) document 関数は Eff (dom :: DOM | eff) HTMLDocument を返す
  • (b) getElementById 関数の引数は ElementId と NonElementParentNode
  • (c) getElementById 関数は Eff (dom :: DOM | eff) (Nullable Element) を返す
  • (d) setTextContent 関数の引数は String と Node

(a) の結果の HTMLDocument を getElementById の引数へそのまま使えなかったので htmlDocumentToNonElementParentNode 関数で変換しています。

(c) の結果の Nullable はそのままだと使い難いので toMaybe 関数で Maybe 化し、Element も setTextContent の引数に使えなかったので elementToNode 関数で変換しています。

src/Main.purs
module Main where

import Prelude
import Control.Monad.Eff

import Data.Maybe
import Data.Nullable (toMaybe)

import DOM
import DOM.HTML.Types
import DOM.Node.Types
import DOM.HTML (window)
import DOM.HTML.Window (document)
import DOM.Node.NonElementParentNode (getElementById)
import DOM.Node.Node (setTextContent)

updateContent :: forall eff. String -> String -> Eff (dom :: DOM | eff) Unit
updateContent id content = do
    win <- window
    doc <- document win
    node <- getElementById (ElementId id) $ htmlDocumentToNonElementParentNode doc
    case (toMaybe node) of
        Just x -> setTextContent content (elementToNode x)
        _      -> return unit

今回は、gulp を使って pulp browserify を実行するように以下のような gulpfile.js を用意しました。

Sample.updateContent で関数を実行できるように --standalone を指定しています。

gulpfile.js
var gulp = require('gulp');
var child_process = require('child_process');

var pulpCmd = (process.platform == 'win32')? 'pulp.cmd': 'pulp';
var destFile = 'sample.js'

gulp.task('pulp_package', () => {
    // pulp browserify の実行
    var res = child_process.spawnSync(pulpCmd, ['browserify', '--standalone', 'Sample', '-t', destFile]);

    // 実行結果の出力
    [res.stdin, res.stdout, res.stderr].forEach( x => {
        if (x) {
            console.log(x.toString());
        }
    });
});

gulp.task('default', ['pulp_package']);

gulp コマンドを実行すると sample.js が生成されます。

ビルド例 (gulp で pulp browserify を実行)
> gulp

下記の HTML で実行してみます。

ここで、Sample.updateContent はカリー化されており function(id) { return function(content) { return function __do() { ・・・ } } } となっている点に注意。

index.html
<!DOCTYPE html>
<html>
<body>
    <h2 id="d"></h2>

    <script src="sample.js"></script>
    <script>
        Sample.updateContent('d')('sample purescript-dom')();
    </script>
</body>
</html>
実行結果

f:id:fits:20160125204344p:plain

purescript-simple-dom の場合

同じ様にして purescript-simple-dom をインストールします。

purescript-simple-dom インストール
> pulp init
・・・

> pulp dep install purescript-simple-dom --save

src/Main.purs を編集し updateContent 関数を実装します。

purescript-dom と比べると余計な型変換が不要なのでシンプルです。

src/Main.purs
module Main where

import Prelude
import Control.Monad.Eff

import DOM

import Data.Maybe
import Data.DOM.Simple.Window (document, globalWindow)
import Data.DOM.Simple.Element (getElementById, setTextContent)

updateContent :: forall eff. String -> String -> Eff (dom :: DOM | eff) Unit
updateContent id content = do
    doc <- document globalWindow
    node <- getElementById id doc

    case node of
        Just x -> setTextContent content x
        _      -> return unit

purescript-dom と同じ様に gulp で sample.js を生成しました。(gulpfile.js は同じ内容です)

HTML は以下の通りです。

index.html
<!DOCTYPE html>
<html>
<body>
    <h2 id="d"></h2>

    <script src="sample.js"></script>
    <script>
        Sample.updateContent('d')('sample purescript-simple-dom')();
    </script>
</body>
</html>
実行結果

f:id:fits:20160125204359p:plain

virtual-dom のイベント処理

仮想 DOM を扱うための JavaScript 用ライブラリ virtual-dom では ev-xxx (例. ev-click) でイベント処理を扱えるようになっていますが、実際に機能させるには dom-delegator が必要なようです。

virtual-dom は bower install が可能で Web ブラウザ上で直接使えるファイル (dist/virtual-dom.js) を用意してくれていますが、今のところ dom-delegator はそうなっていません。(dom-delegator は npm install する)

名称 bower install Web ブラウザで直接使用
virtual-dom
dom-delegator × ×

そこで、今回は下記ツールを用いて virtual-dom で ev-click が機能するようにしてみます。

事前準備

Gulp・Bower・Browserify をインストールします。

インストール例
> npm install -g gulp bower browserify

virtual-dom のイベント処理サンプル

virtual-dom で ev-click を実施するサンプルを作成します。

ソースは http://github.com/fits/try_samples/tree/master/blog/20160107/

(1) virtual-dom インストール

virtual-dom を Bower でインストールします。

bower init と virtual-dom インストール例
> bower init
・・・

> bower install virtual-dom --save
・・・

(2) dom-delegator インストール

dom-delegator は npm でインストールします。 (dom-delegator は Bower でインストールできないため ※)

※ dom-delegator の git リポジトリを bower install する事は可能ですが、
   その場合は依存ライブラリを手動でインストールする事になります
npm init と dom-delegator インストール例
> npm init
・・・

> npm install dom-delegator --save
・・・

(3) gulp-flatten インストール

Gulp でファイルをコピーする際にディレクトリ構成をフラット化するため gulp-flatten パッケージをインストールしておきます。

gulp-flatten インストール例
> npm install gulp-flatten --save-dev

Gulp で以下のように bower_components/virtual-dom/dist/virtual-dom.js を js ディレクトリへコピーすると、js/virtual-dom/dist/virtual-dom.js が作られます。

Gulp によるコピー例
gulp.task('js-copy', () => {
   gulp.src('bower_components/*/dist/*.js')
        .pipe(gulp.dest('js'));
});

今回は、js/virtual-dom/dist/virtual-dom.js では無く js/virtual-dom.js へコピーしたかったので gulp-flatten を使いました。

(4) Gulp ビルド定義作成と実行

以下のような処理を行う gulpfile.js を作成します

  • (a) bower_components/virtual-dom/dist/virtual-dom.js を js/virtual-dom.js へコピー
  • (b) dom-delegator を Browserify で処理して js/dom-delegator.js へ出力 (Web ブラウザ上で DOMDelegator という名称で扱えるように standalone も設定)
gulpfile.js
var fs = require('fs');

var gulp = require('gulp');
var browserify = require('browserify');

var flatten = require('gulp-flatten');

// (a)
gulp.task('js-copy', () => {
    gulp.src('bower_components/*/dist/*.js')
        .pipe(flatten()) // gulp-flatten でディレクトリをフラット化
        .pipe(gulp.dest('js'));
});

// (b)
gulp.task('browserify', () => {
    browserify({
        require: 'dom-delegator',
        standalone: 'DOMDelegator'
    }).bundle().pipe(fs.createWriteStream('js/dom-delegator.js'));

});

gulp.task('default', ['js-copy', 'browserify']);

js ディレクトリを手動で作成した後、gulp コマンドを実行します。

gulp 実行例
> mkdir js
> gulp

[00:19:17] Using gulpfile ・・・\virtual-dom\gulpfile.js
[00:19:17] Starting 'js-copy'...
[00:19:17] Finished 'js-copy' after 15 ms
[00:19:17] Starting 'browserify'...
[00:19:17] Finished 'browserify' after 17 ms
[00:19:17] Starting 'default'...
[00:19:17] Finished 'default' after 6.16 μs

これで下記のファイルが作成されます。

  • js/virtual-dom.js
  • js/dom-delegator.js

(5) virtual-dom の ev-click サンプル作成と実行

Gulp で生成した js ファイルを使い virtual-dom で ev-click を扱う単純なサンプルを作成し動作確認してみます。

virtual-dom は概ね以下のようにして使います。

  • (1) virtualDom.diff 関数で 2つの VNode (VirtualNode) の差分を VPatch (VirtualPatch) として抽出
  • (2) virtualDom.patch 関数で VPatch の内容を実際の DOM へ反映
index.html
<!DOCTYPE html>
<html>
<script src="js/virtual-dom.js"></script>
<script src="js/dom-delegator.js"></script>
<body>
    <h1>virtual-dom click sample</h1>

    <div id="ct"></div>

    <script>
        (function() {
            // ev-xxx イベントの有効化 (dom-delegator)
            new DOMDelegator();

            var h = virtualDom.h;
            var create = virtualDom.create;
            var diff = virtualDom.diff;
            var patch = virtualDom.patch;

            var index = 1;
            var tree = h();

            var render = function(v) {
                // 仮想 DOM の作成
                return h('div', [
                    h('button', 
                        {
                            // click イベントの設定
                            'ev-click': function(ev) { update(index++) }
                        },
                        ['countUp']
                    ),
                    h('br'),
                    'counter: ' + String(v)
                ]);
            }

            var update = function(v) {
                var newTree = render(v);

                // tree と newTree の差分を実際の DOM へ反映
                patch(document.getElementById('ct'), diff(tree, newTree));

                tree = newTree;
            };

            update(index++);
        })();

    </script>
</body>
</html>

Web ブラウザで実行し countUp ボタンを押下すると counter の数値がカウントアップする事を確認できました。

Web ブラウザ表示例

f:id:fits:20160107023648p:plain

virtual-dom イベント処理の仕組み

最後に、virtual-dom と dom-delegator がどのようにイベント処理を行っているのか簡単に書いておきます。

まず、VNode の作成時に ev- で始まるプロパティは EvHook へ変換されます。(virtual-hyperscript/index.js の transformProperties)

patch 関数で実際の DOM へ反映する際に、対象の DOM ノードへ特殊なプロパティ __EV_STORE_KEY@7 ※ を追加して EvHook の内容 (イベント名とイベント処理関数) を設定します。 (ev-store の機能)

※ プロパティ名は ev-store の index.js で定義されており、
   ev-store 7.0.0 では __EV_STORE_KEY@7 となっています
__EV_STORE_KEY@7 が設定されている DOM ノード例

f:id:fits:20160107023717p:plain

dom-delegator は "blur", "change", "click" 等の多数のイベントを document.documentElement で listen するように処理し、イベント発生時に対象 DOM ノードの __EV_STORE_KEY@7 プロパティの内容をチェックして、該当するイベントが設定されていればその処理関数を実行します。

つまり、上記のようなイベント処理を自前で行えば dom-delegator を使わなくても ev-click を機能させる事が可能です。

index2.html (dom-delegator を使わずに ev-click を有効化する例)
<!DOCTYPE html>
<html>
<script src="js/virtual-dom.js"></script>
<body>
    <h1>virtual-dom click sample2</h1>

    <div id="ct"></div>

    <script>
        (function() {
            // dom-delegator の代用処理
            document.addEventListener('click', function(ev) {
                var store = ev.target['__EV_STORE_KEY@7'];

                if (store && store[ev.type]) {
                    // 該当するイベント処理関数の実行
                    store[ev.type](ev);
                }
            }, true);

            var h = virtualDom.h;
            ・・・
            var render = function(v) {
                return h('div', [
                    h('button', 
                        {
                            'ev-click': function(ev) { update(index++) }
                        },
                        ['countUp']
                    ),
                    h('br'),
                    'counter: ' + String(v)
                ]);
            }
            ・・・
        })();

    </script>
</body>
</html>

pulp を使った PureScript の開発

PureScript 用のビルドツールpulp があります。

pulp を使えば PureScript v0.7 から多少面倒になったビルドや実行が比較的容易になります。

pulp インストール

Node.js の npm で purescript (コンパイラ) と pulp をインストールします。

pulp インストール例
> npm install -g purescript pulp

今回インストールした PureScript コンパイラpulp のバージョンは以下の通りです。

なお、PureScript コンパイラに関しては https://github.com/purescript/purescript/releases/ から各 OS 用のバイナリを直接取得する方法もあります。

npm でインストールしたものも実際は node_modules/purescript/vendor ディレクトリへ配置された各 OS 用のバイナリファイル (例. psc.exe) を使っているようです。

pulp を使った開発

今回作成したソースは http://github.com/fits/try_samples/tree/master/blog/20160105/

プロジェクトの作成

任意のディレクトリ内で pulp init を実行すると、必要最小限のファイルが生成されます。

その際に Bower を使って PureScript の主要ライブラリ (以下) を自動的に取得しますので、git コマンドを使えるようにしておく必要があります。

  • purescript-console
  • purescript-eff
  • purescript-prelude
プロジェクト作成例
> pulp init

* Generating project skeleton in ・・・
bower cached        git://github.com/purescript/purescript-console.git#0.1.1
bower validate      0.1.1 against git://github.com/purescript/purescript-console.git#^0.1.0
bower cached        git://github.com/purescript/purescript-eff.git#0.1.2
bower validate      0.1.2 against git://github.com/purescript/purescript-eff.git#^0.1.0
bower cached        git://github.com/purescript/purescript-prelude.git#0.1.3
bower validate      0.1.3 against git://github.com/purescript/purescript-prelude.git#^0.1.0
bower install       purescript-console#0.1.1
bower install       purescript-eff#0.1.2
bower install       purescript-prelude#0.1.3

purescript-console#0.1.1 bower_components\purescript-console
└── purescript-eff#0.1.2

purescript-eff#0.1.2 bower_components\purescript-eff
└── purescript-prelude#0.1.3

ディレクトリ・ファイル構成は以下のようになります。

  • bower_components
    • purescript-console
    • purescript-eff
    • purescript-prelude
  • src/Main.purs
  • test/Main.purs
  • .gitignore
  • bower.json

デフォルトで用意されている src/Main.purs の内容は以下の通りです。

src/Main.purs
module Main where

import Prelude
import Control.Monad.Eff
import Control.Monad.Eff.Console

main :: forall e. Eff (console :: CONSOLE | e) Unit
main = do
  log "Hello sailor!"

ライブラリの追加には pulp dep install <ライブラリ名> を実行します。 そうすると bower install が実施されます。

bower.json の依存パッケージ設定へエントリを追加するには --save オプションを付けます。

purescript-tuples の追加例
> pulp dep install purescript-tuples --save

・・・
bower install       purescript-control#0.3.2
bower install       purescript-invariant#0.3.0

purescript-tuples#0.4.0 bower_components\purescript-tuples
└── purescript-foldable-traversable#0.4.2

purescript-foldable-traversable#0.4.2 bower_components\purescript-foldable-traversable
├── purescript-bifunctors#0.4.0
└── purescript-maybe#0.3.5

purescript-maybe#0.3.5 bower_components\purescript-maybe
└── purescript-monoid#0.3.2

purescript-bifunctors#0.4.0 bower_components\purescript-bifunctors
└── purescript-control#0.3.2

purescript-monoid#0.3.2 bower_components\purescript-monoid
├── purescript-control#0.3.2
└── purescript-invariant#0.3.0

purescript-control#0.3.2 bower_components\purescript-control
└── purescript-prelude#0.1.3

purescript-invariant#0.3.0 bower_components\purescript-invariant
└── purescript-prelude#0.1.3

ビルドと実行

Main.purs を以下のようにタプルを使った処理に書き換えて実行してみます。

src/Main.purs
module Main where

import Prelude
import Control.Monad.Eff
import Control.Monad.Eff.Console
import Data.Tuple

main :: forall e. Eff (console :: CONSOLE | e) Unit
main = do
  let t = Tuple "two" 2
  print t
  
  let r = t >>= \x -> Tuple " + 1" (x + 1)
  print r

pulp run を実行するとビルドした後に処理を実施します。
ビルドだけを実施したい場合は pulp build を使います。

実行
> pulp run

* Building project in ・・・\20160105\purescript
psc: No files found using pattern: src/**/*.js
* Build successful.
Tuple ("two") (2)
Tuple ("two + 1") (3)

ビルド結果は output ディレクトリへ生成されます。

パッケージング

pulp browserify を実行すると Browserify を使って output ディレクトリ内のファイルをパッケージングしてくれます。

browserify によるパッケージング
> pulp browserify > sample.js

* Browserifying project in ・・・\20160105\purescript
* Project unchanged; skipping build step.
* Browserifying...

パッケージングしたファイル (sample.js) の実行結果は以下の通りです。

実行結果
> node sample.js

Tuple ("two") (2)
Tuple ("two + 1") (3)

Web ブラウザで実行する事もできます。

index.html
<!DOCTYPE html>
<html>
<script src="sample.js"></script>
</html>

f:id:fits:20160105001028p:plain

備考 - pulp を使わない場合

最後に pulp を使わない場合のビルド・実行方法も書いておきます。

まずは、Bower を使って purescript-console 等の必要なライブラリを手動でインストールします。

ライブラリのインストール例
> bower install purescript-console --save

src/Main.purs を psc コマンドでビルドするには以下のようにします。

psc によるビルド例
> psc src/Main.purs bower_components/purescript-*/src/**/*.purs --ffi bower_components/purescript-*/src/**/*.js

bower_components 内の .purs ファイルと --ffi オプションで bower_components 内の .js ファイルを指定します。

ビルド結果はデフォルトで output ディレクトリへ生成されます。(-o オプションで変更する事も可能)

実行する場合は、NODE_PATH 環境変数へ output ディレクトリを設定し、node コマンドで require('Main/index.js').main() を実行します。

実行例
> set NODE_PATH=output
> node -e "require('Main/index.js').main()"

Tuple ("two") (2)
Tuple ("two + 1") (3)

Google Cloud Print を Web API で操作 - Unirest 使用

リフレッシュトークンを使って Google Cloud Print を Web API で操作してみます。

以前、「Google アカウントで Google API を利用 - google-api-services-gmail」 では Apache HTTPClient を使いましたが、今回は Unirest を使っています。

ソースは http://github.com/fits/try_samples/tree/master/blog/20151222/

はじめに

事前準備としてリフレッシュトークンを取得しておきます。

今回は、「Google アカウントで・・・」 にて発行したクライアント ID をそのまま使用します。

(1) コードの取得

下記パラメータを付け https://accounts.google.com/o/oauth2/auth へ Web ブラウザ等でアクセスし API の利用を許可する事でコードを取得します。

パラメータ
response_type code
client_id クライアント ID の client_id 値(※1)
scope https://www.googleapis.com/auth/cloudprint
redirect_uri oob (※2)
(※1)クライアント ID 発行時にダウンロードした JSON ファイルの client_id 値

(※2)リダイレクトしない場合の設定。
       何らかのアプリケーション内から実行する際は
       それに合わせたリダイレクト先を指定する

例えば、以下のような URL へアクセスします。

URL 例
https://accounts.google.com/o/oauth2/auth?redirect_uri=oob&response_type=code&client_id=xxx.apps.googleusercontent.com&scope=https://www.googleapis.com/auth/cloudprint

Google アカウントでログインすると以下のような画面が表示されますので、「許可」ボタンを押下します。

f:id:fits:20151221233721p:plain

コードが表示されるのでコピーしておきます。

f:id:fits:20151221233746p:plain

(2) リフレッシュトークンの取得

次に、下記パラメータを https://www.googleapis.com/oauth2/v3/token へ POST し、リフレッシュトークンを含んだ JSON を取得します。

パラメータ
code (1) で取得したコード
client_id クライアント ID の client_id 値
client_secret クライアント ID の client_secret 値
grant_type authorization_code
redirect_uri oob (※1)
(※1)(1) の場合と同様です。

       ただし、以下のスクリプトでは
       urn:ietf:wg:oauth:2.0:oob を設定しています

Groovy スクリプト化すると以下のようになります。

get_refresh-token.groovy
@Grab('com.mashape.unirest:unirest-java:1.4.9')
import com.mashape.unirest.http.Unirest
import groovy.json.JsonSlurper

addShutdownHook {
    Unirest.shutdown()
}

def json = new JsonSlurper()

def conf = json.parse(new File(args[0])).installed
def code = args[1]

def res = Unirest.post('https://www.googleapis.com/oauth2/v3/token')
    .field('code', code)
    .field('client_id', conf.client_id)
    .field('client_secret', conf.client_secret)
    .field('grant_type', 'authorization_code')
    .field('redirect_uri', conf.redirect_uris[0])
    .asJson()

println res.body

クライアント ID 発行時にダウンロードした JSON (ここでは client_secret.json) を第1引数、(1) で取得したコードを第2引数へ指定して実行します。

取得したリフレッシュトークン JSON は後で使うので保存しておきます。 (ここでは token.json へ保存)

実行例
> groovy get_refresh-token.groovy client_secret.json 4/9ic・・・ > token.json

プリンタ一覧の取得

リフレッシュトークンを使う場合は、以下のような手順で API を呼び出します。

  1. リフレッシュトークンからアクセストークンを取得
  2. アクセストークンを使って API を実行

リフレッシュトークンからアクセストークン取得

アクセストークンを取得するには、下記パラメータを https://www.googleapis.com/oauth2/v3/token へ POST します。

パラメータ
client_id クライアント ID の client_id 値
client_secret クライアント ID の client_secret 値
grant_type refresh_token
refresh_token リフレッシュトークンの値 (※1)
(※1)リフレッシュトークン取得で保存した JSON の refresh_token 値

この処理は共通的に使用するため、Groovy の BaseScript として定義しました。

TokenScript.groovy
import com.mashape.unirest.http.Unirest
import groovy.json.JsonSlurper

abstract class TokenScript extends Script {
    // アクセストークンの取得
    def accessToken(String clientId, String clientSecret, String refreshToken) {
        def res = Unirest.post('https://www.googleapis.com/oauth2/v3/token')
            .field('client_id', clientId)
            .field('client_secret', clientSecret)
            .field('grant_type', 'refresh_token')
            .field('refresh_token', refreshToken)
            .asJson()

        res.body.object
    }
}

API 実行 (プリンタの一覧取得)

アクセストークンは HTTP ヘッダーへ Authorization: Bearer <アクセストークン> のように設定して使います。(例. Authorization: Bearer ya26.pw・・・)

クラウドプリンタ一覧は https://www.google.com/cloudprint/search へ GET すれば取得できます。 (q や type パラメータを付けて条件検索することも可能)

get_printers.groovy
@Grab('com.mashape.unirest:unirest-java:1.4.9')
import com.mashape.unirest.http.Unirest
import groovy.json.JsonSlurper
import groovy.transform.BaseScript

// TokenScript を BaseScript へ設定
@BaseScript TokenScript baseScript

addShutdownHook {
    Unirest.shutdown()
}

def json = new JsonSlurper()

def conf = json.parse(new File(args[0])).installed
def token = json.parse(new File(args[1]))

// アクセストークン取得
def newToken = accessToken(
    conf.client_id,
    conf.client_secret,
    token.refresh_token
)

// API の実行
def printers = Unirest.get('https://www.google.com/cloudprint/search')
    .header('Authorization', "${newToken.token_type} ${newToken.access_token}")
    .asJson()

println printers.body

実行結果は以下の通りです。
まだプリンタを登録していないためデフォルトの 「ドライブに保存」 しかありません。

実行結果 (出力結果は加工済、実際は改行・字下げは無し)
> groovy get_printers.groovy client_secret.json token.json

{
    "request":{・・・},
    "printers":[{
        "isTosAccepted":false,
        "displayName":"ドライブに保存",
        "capsHash":"",
        "description":"ドキュメントを Google ドライブで PDF として保存します",
        "updateTime":"1370287324050",
        "ownerId":"cloudprinting@gmail.com",
        "type":"DRIVE",
        "tags":["save","docs","pdf","google","__google__drive_enabled"],
        "proxy":"google-wide",
        "ownerName":"Cloud Print",
        "createTime":"1311368403894",
        "defaultDisplayName":"ドライブに保存",
        "connectionStatus":"ONLINE",
        "name":"Save to Google Docs",
        "id":"__google__docs",
        "accessTime":"1316132041869",
        "status":""
    }],
    "success":true,
    ・・・
}

また、https://www.google.com/cloudprint/list?proxy=<プロキシ> へ GET すると、指定プロキシへ属するプリンタ一覧を取得できます。

印刷処理 (Google Drive へ保存)

最後に、「ドライブに保存」 プリンタへファイルを印刷してみます。 (実際は Google Drive へファイル保存)

印刷は、下記の必須パラメータ(他にもパラメータあり)を multipart/form-datahttps://www.google.com/cloudprint/submit へ POST します。

パラメータ
printerid プリンタ ID (※1)
title 印刷タイトル
ticket 印刷設定 (※2)
content 印刷するファイルの内容
(※1)今回は __google__docs を使用
(※2)CJT (Cloud Job Ticket) フォーマットで印刷オプションなどを指定
submit_file.groovy
@Grab('com.mashape.unirest:unirest-java:1.4.9')
import com.mashape.unirest.http.Unirest
import groovy.json.JsonSlurper
import groovy.json.JsonBuilder
import groovy.transform.BaseScript

@BaseScript TokenScript baseScript

addShutdownHook {
    Unirest.shutdown()
}

def ticketBuilder = new JsonBuilder()

// ticket の内容
ticketBuilder (
    version: '1.0',
    print: {}
)

def json = new JsonSlurper()

def conf = json.parse(new File(args[0])).installed
def token = json.parse(new File(args[1]))
def file = new File(args[2])

// アクセストークンの取得
def newToken = accessToken(
    conf.client_id,
    conf.client_secret,
    token.refresh_token
)

// ticket の JSON 文字列化
def ticket = ticketBuilder.toString()

// 印刷
def res = Unirest.post('https://www.google.com/cloudprint/submit')
    .header('Authorization', "${newToken.token_type} ${newToken.access_token}")
    .field('printerid', '__google__docs')
    .field('title', file.name)
    .field('ticket', ticket)
    .field('content', file)
    .asJson()

println res.body

PDF ファイルを POST した結果は以下の通りです。
Google Drive で確認すると sample1.pdf ファイルが保存されていました。

実行結果 (出力結果は加工済、実際は改行・字下げは無し)
> groovy submit_file.groovy client_secret.json token.json data/sample1.pdf

{
    "request":{・・・},
    "success":true,"xsrf_token":"AIp・・・",
    "message":"印刷ジョブが追加されました。",
    "job":{
        "ticketUrl":"https://www.google.com/cloudprint/ticket?format=xps&output=xml&jobid=0db・・・",
        "printerName":"",
        "errorCode":"",
        ・・・
        "title":"sample1.pdf",
        "tags":["^own"],
        ・・・
        "printerid":"__google__docs",
        ・・・
        "contentType":"application/pdf",
        "status":"DONE"
    }
}

また、試しに以下のような拡張子のファイルを POST してみたところ、自動的に PDF へ変換され Google Drive へ保存されました。 (例えば sample1.html は sample1.html.pdf で保存)

  • .html
  • .docx
  • .odt

Android エミュレータの hosts ファイルを編集

Android エミュレータの hosts ファイルを編集する方法です。
以下のような方法が考えられます。

  • (a) shell で hosts へ行を追加
  • (b) pull と push で hosts を変更

(a) shell で hosts へ行を追加

行を追加するだけなら以下のように shell を使うだけです。

adb remount しておかないと /system/etc/hosts ファイルを更新できずに Read-only file system のようなエラーメッセージが表示される点に注意。 (既に adb remount 済みの場合は不要)

hosts への行追加
> adb remount
remount succeeded

> adb shell "echo 10.0.2.2 server1 >> /system/etc/hosts"

/system/etc/hosts の内容を確認してみます。

hosts の内容確認
> adb shell cat /system/etc/hosts

127.0.0.1                   localhost
10.0.2.2 server1

10.0.2.2 はエミュレータから接続する際のホスト OS の IP アドレスなので、ping してみます。

ping の実行
> adb shell ping server1

PING server1 (10.0.2.2) 56(84) bytes of data.
64 bytes from server1 (10.0.2.2): icmp_seq=1 ttl=255 time=0.000 ms
・・・

remount に関して

adb remount する事で以下のように /system が rw で再マウントされます。

adb remount 前 (ro)
/dev/block/mtdblock0 /system ext4 ro,relatime,data=ordered 0 0
adb remount 後 (rw)
/dev/block/mtdblock0 /system ext4 rw,relatime,data=ordered 0 0

(b) pull と push で hosts を変更

次は hosts ファイルを編集する方法です。

エミュレータには vi 等のエディタが用意されていないようなので、pull で hosts ファイルを取得して、ホスト OS 側で hosts ファイルを編集し、push でエミュレータへ反映します。

まずは adb pull で hosts ファイルを取得します。

hosts を pull
> adb pull /system/etc/hosts

ホスト OS のカレントディレクトリへ pull された hosts ファイルを編集します。

hosts 変更例
127.0.0.1           localhost
10.0.2.2            server1

変更した hosts を push します。
まだ一度も remount していない場合は remount する必要があります。

hosts を push
> adb remount
・・・

> adb push hosts /system/etc/

Swift のビルド環境を Docker で構築

Ubuntu をベースとした Swift のビルド環境を Docker で構築してみました。

使用したソースは http://github.com/fits/try_samples/tree/master/blog/20151207/

Docker イメージ作成

ubuntu:15.10 のイメージをベースに Swift をセットアップしました。

clang 等の必要なパッケージをインストールした後、swift-2.2-SNAPSHOT-2015-12-01-b-ubuntu15.10.tar.gz をダウンロードして解凍し、環境変数 PATH へ swift-2.2-・・・/usr/bin のパスを追加するだけです。

また、clang をインストールしただけだと swiftc の実行時に /usr/bin/ld: warning: libicuuc.so.55, needed by /swift-2.2・・・/usr/lib/swift/linux/libswiftCore.so, not found (try using -rpath or -rpath-link) のようなメッセージが出力されたので、libicu-dev もインストールするようにしています。

Dockerfile
FROM ubuntu:15.10

ENV SWIFT_PACKAGE swift-2.2-SNAPSHOT-2015-12-01-b-ubuntu15.10

RUN apt-get update
RUN apt-get -y upgrade

RUN apt-get -y install curl clang libicu-dev

# swift のパッケージをダウンロード
RUN curl https://swift.org/builds/ubuntu1510/swift-2.2-SNAPSHOT-2015-12-01-b/$SWIFT_PACKAGE.tar.gz -o $SWIFT_PACKAGE.tar.gz

RUN tar zxf $SWIFT_PACKAGE.tar.gz
RUN rm -f $SWIFT_PACKAGE.tar.gz

RUN apt-get clean

# 環境変数 PATH の設定
ENV PATH /$SWIFT_PACKAGE/usr/bin:$PATH

docker build で Docker イメージを作成します。

Docker イメージの作成
$ docker build --no-cache -t sample/swift:0.1 .

・・・
Successfully built ・・・

動作確認

作成した Docker イメージを使って以下のようなサンプルソースを実行してみます。

/vagrant/work/sample.swift
let a = 11
let b = 2

print("result = \(a * b)")

まず、sample/swift:0.1 イメージを使って Docker コンテナを起動します。

Docker コンテナの起動
$ docker run --rm -it -v /vagrant/work:/work sample/swift:0.1 bash
・・・
# cd /work

コンテナ内で sample.swift を実行します。

直接実行
# swift sample.swift

result = 22
ビルドしてから実行
# swiftc sample.swift
# ./sample

result = 22

Objective-C のビルド環境を Docker で構築

CentOS をベースとした Objective-C のビルド環境を Docker で構築してみました。

使用したソースは http://github.com/fits/try_samples/tree/master/blog/20151130/

Docker イメージ作成

今回は clang と GNUstep を使う事にします。

バージョンは古くなってしまいますが、どちらも yum でインストールするようにしました。

make をインストールしておかないと GNUstepgnustep-config コマンドが機能しない (結果が空になる) のでご注意ください。

また、gcc-objc をインストールしておかないと、Objective-C のソースをビルドする際に必要なヘッダーファイルを参照できず、fatal error: 'inttypes.h' file not found のようなエラーが発生しました。

Dockerfile
FROM centos

RUN yum -y update

RUN yum -y install epel-release
RUN yum -y install clang make gcc-objc gnustep-base-devel

RUN yum clean all

docker build で Docker イメージを作成します。

Docker イメージの作成
$ docker build --no-cache -t sample/objc:0.1 .

・・・
Successfully built ・・・

動作確認

以下のようなサンプルソースをビルドしてみます。

/vagrant/work/Sample.h
#import <Foundation/NSObject.h>
#import <Foundation/NSString.h>

@interface Sample : NSObject {
    NSString* _name;
}

@property (nonatomic, copy) NSString* name;

- (void)log;

@end
/vagrant/work/Sample.m
#import <Foundation/Foundation.h>
#import "Sample.h"

@implementation Sample : NSObject

@synthesize name = _name;

- (void)log {
    NSLog(@"%@", _name);
}

- (void)dealloc {
    [_name release];
    [super dealloc];
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Sample* s = [[Sample new] autorelease];

        s.name = @"test";

        [s log];
    }

    return 0;
}

まずは sample/objc:0.1 イメージを使って Docker コンテナを起動します。

Docker コンテナの起動
$ docker run --rm -it -v /vagrant/work:/work sample/objc:0.1 bash

コンテナ内で clang を使ってビルドします。

コンパイラオプションに関しては以下で妥当なのかは不明ですが、一応ビルドには成功しました。 (場合によっては -objcmt-migrate-all 等の指定も必要かもしれません)

ビルド
# cd work

# clang `gnustep-config --objc-flags` `gnustep-config --objc-libs` -lgnustep-base -I /usr/lib/gcc/x86_64-redhat-linux/4.8.3/include Sample.m -o Sample

clang: warning: argument unused during compilation: '-shared-libgcc'

一応、それぞれのオプションを指定しなかった場合に発生したエラーをまとめておきます。

オプション指定 指定しなかった場合のエラー例
`gnustep-config --objc-flags` 無し(指定しなくてもビルドは成功)
`gnustep-config --objc-libs` undefined reference to symbol 'objc_getProperty'
-lgnustep-base undefined reference to `NSLog'
-I /usr/lib/gcc/・・・/include fatal error: 'objc/objc.h' file not found

ビルド結果の Sample を実行してみると一応動作しました。

実行例
# ./Sample

2015-11-30 20:15:00.121 Sample[32] test

なお、clang -v の実行結果は以下のようになりました。

clang -v
# clang -v

clang version 3.4.2 (tags/RELEASE_34/dot2-final)
Target: x86_64-redhat-linux-gnu
Thread model: posix
Found candidate GCC installation: /usr/bin/../lib/gcc/x86_64-redhat-linux/4.8.2
Found candidate GCC installation: /usr/bin/../lib/gcc/x86_64-redhat-linux/4.8.3
Found candidate GCC installation: /usr/lib/gcc/x86_64-redhat-linux/4.8.2
Found candidate GCC installation: /usr/lib/gcc/x86_64-redhat-linux/4.8.3
Selected GCC installation: /usr/bin/../lib/gcc/x86_64-redhat-linux/4.8.3