2011年12月10日土曜日

Using Kensington Expert Mouse with Ubuntu 11.10

自宅ではKensington Expert Mouseを使ってるんだけど、普段左手でマウスを使うようにしてるのでボタンのマップをいじる必要がある。それで結構前に色々調べた結果、以下のコマンドをやれば理想的なボタン配置(左上:右クリック、右上:右クリック、左下:ミドルクリック、右下:左クリック、反時計回りスクロール:下降、時計回りスクロール:上昇)になることまではわかった。

/usr/bin/xinput set-button-map "Kensington      Kensington Expert Mouse" 2 3 1 5 4 3 3 3 3 3 3 3

あとはこれを~/.xinitrcに記述して起動時に適切なボタンにマップされるところまでは良かった。

USB抜きさしすると初期化される

ただこのやり方には一つ穴があって、マウスをUSBから抜きさしすると適用したマップが初期化されてしまう、というもの。当時のUbuntuはHALを無くすとかなんとかでハードウェアまわりのソフトウェア環境がゴタゴタしてて、色々調べたけどいまいちこの問題を解消する方法がわからなかった。で、昨日久しぶりに調べてみたらあっさり解決したのでここにメモっておく。

  1. lsusbコマンドを実行して、Kensingtonマウスの項目にあたる部分をひかえておく。
    $ lsusb
    ...
    Bus 004 Device 008: ID 047d:1020 Kensington Expert Mouse Trackball
    ...
    
  2. /usr/share/X11/xorg.conf.d/90-kensington-expert.confというファイルを作り、中身を以下のように書く。 IdentifierとMatchUSBIDはlsusbで出力されたものを使う。
    Section "InputClass"
            Identifier      "Kensington Expert Mouse Trackball"
            MatchUSBID      "047d:1020"
            Option "ButtonMapping" "2 3 1 5 4 3 3 3 3 3 3 3"
    EndSection
    
  3. 一旦ログアウトして、再度ログインする。

以上おしまい。もっと早く調べておけば良かった。

2011年11月12日土曜日

Setting up Google App Engine Environment (Go) for Ubuntu 11.10

Ubuntu 11.10でGoogle App Engine (Go)の環境をセットアップするのに手間取ったのでメモ。

Google App Engine SDKの取得

いまの最新版SDK1.6.0を落としてセットアップ。パスも適当に通す。

$ cd ~/lib
$ wget http://googleappengine.googlecode.com/files/go_appengine_sdk_linux_386-1.6.0.zip
$ unzip go_appengine_sdk_linux_386-1.6.0.zip
$ PATH=$PATH:~/lib/google_appengine

Python 2.5のセットアップ

Google App Engineの開発用ローカルサーバー(dev_appserver.py)を正常に動作させるにはまだPython 2.5が必要なため、インストールする。ただUbuntu 11.10はもうPython 2.5を切りすててしまっているため、外部のリポジトリから入手する必要がある。

$ sudo add-apt-repository ppa:fkrull/deadsnakes
$ sudo apt-get update
$ sudo apt-get install python2.5 python2.5-dev

PILのセットアップ

PILがインストールされていないとdev_appserver.pyの起動時に"Could not initialize images API; you are likely missing the Python "PIL" module."というエラーが出るため、PILもインストールする。

$ wget http://effbot.org/downloads/Imaging-1.1.7.tar.gz 
$ tar xzf Imaging-1.1.7.tar.gz 
$ cd Imaging-1.1.7/
$ sudo python2.5 setup.py install

これであとは下のような感じでdev_appserver.pyを起動すればいける。

$ python2.5 ~/lib/google_appengine/dev_appserver.py server/ 

Python 2.7でやろうとすると

そもそもなんでこんなことを今さらやったかというと、Python 2.7でdev_appserver.pyを実行すると、ときたま落ちるから。試しにChannel APIを使おうとしたら↓のエラーで死ぬようになってしまったため、やはり正規のPython 2.5を入れる必要がでた、ということ。

INFO     2011-11-12 14:29:41,093 dev_appserver.py:2753] "POST /_ah/channel/connected/ HTTP/1.1" 404 -
----------------------------------------
Exception happened during processing of request from ('0.1.0.10', 80)
Traceback (most recent call last):
  File "/usr/lib/python2.7/SocketServer.py", line 284, in _handle_request_noblock
    self.process_request(request, client_address)
  File "/usr/lib/python2.7/SocketServer.py", line 311, in process_request
    self.shutdown_request(request)
  File "/usr/lib/python2.7/SocketServer.py", line 459, in shutdown_request
    request.shutdown(socket.SHUT_WR)
AttributeError: 'ChannelPresenceConnection' object has no attribute 'shutdown'
----------------------------------------
ERROR    2011-11-12 14:29:41,128 dev_appserver_main.py:664] Error encountered:
Traceback (most recent call last):

  File "/home/masato/lib/google_appengine/google/appengine/tools/dev_appserver_main.py", line 657, in main
    http_server.serve_forever()

  File "/home/masato/lib/google_appengine/google/appengine/tools/dev_appserver.py", line 3527, in serve_forever
    self.handle_request()

  File "/home/masato/lib/google_appengine/google/appengine/tools/dev_appserver.py", line 3490, in handle_request
    self._handle_request_noblock()

  File "/usr/lib/python2.7/SocketServer.py", line 287, in _handle_request_noblock
    self.shutdown_request(request)

  File "/usr/lib/python2.7/SocketServer.py", line 459, in shutdown_request
    request.shutdown(socket.SHUT_WR)

AttributeError: 'ChannelPresenceConnection' object has no attribute 'shutdown'

Now terminating.

2011年9月17日土曜日

Goで○○する方法 part 1

GDD 2011のチャレンジ問題をGoで解こうとコードを書いたのはいいけれど、結構前から存在してたらしいGCのバグに気付かずなんでメモリ切れになっちゃうんだろうなーと激しく時間のムダをしてしまった。で、結局C++に書きなおした。Goはデバッガもいまいちまだ整備されてないし、やっぱここぞというときは信頼できる枯れた言語と実行環境でやるのが重要っすね。

さてそんな感じにGDD 2011をやってて思ったけど、Goはやっぱり致命的にサンプルコードが足りない気がした。単純にファイルを開いてテキストを一行ずつ読む、という基本的なことすらやり方がよくわからなかったりする。言語の名前が検索しづらい、というのもそれに拍車をかけてる。

ということで、そんな基本的なコードをいくつか紹介していくことにする。

コマンドライン引数をパースして値を取得する方法

プログラム書いてるときに良く書くコードとして、コマンドライン引数をパースしてそれを取り込む、というものがあげられる。更にはhオプション付きで実行されたらヘルプドキュメントを出力する、といったことも良くやる。Goにはその辺を簡単にしてくれるflagパッケージというものが存在している。

以下はGDD 2011のチャレンジ問題を解く(はずだった)プログラムの一部で、コマンドライン引数をパースしている。

func main() {
    var inputFile *string = flag.String("in", "", "Path to the input file")
    var outputFile *string = flag.String("out", "", "Path to the output file")
    flag.Parse()
    logger.Println("Input file path:", *inputFile)
    logger.Println("Output file path:", *outputFile)
    ...
}

このコードのおかげで、以下のように引数が指定できるようになる。

$ ./SlidePuzzle -in puzzle.txt -out output.txt
Input file path: puzzle.txt
Output file path: output.txt

もし変なオプションを指定したとしたら、ちゃんとヘルプまで表示してくれる。

$ ./SlidePuzzle -aardvark
flag provided but not defined: -aardvark
Usage of ./SlidePuzzle:
  -in="": Path to the input file
  -out="": Path to the output file
$

そんな便利なパッケージです。みなさん使いましょう。

テキストファイルを開いて一行づつ読む方法

前項の方法でファイルパスを得たら、今度はそのファイルを開いて読んでいきたいと思うのが普通の人。ということでそのやり方となるのが下の方法。

    inFile,err := os.Open(*inputFile)
    if err != nil {
        logger.Println(err)
        return
    }

    defer func() {
        inFile.Close()
    }()

    reader := bufio.NewReader(inFile)

    // Get the maxinum number of moves allowed
    numberOfHands, err := reader.ReadString('\n')
    if err != nil {
        logger.Println(err)
        return
    }

    var maxLeft, maxRight, maxUp, maxDown int
    fmt.Sscanf(numberOfHands, "%d %d %d %d\n", &maxLeft, &maxRight, &maxUp, &maxDown)
    logger.Printf("Left: %d Right: %d Up: %d Down: %d\n", maxLeft, maxRight, maxUp, maxDown)

入力となるのが下のテキスト。

72187 81749 72303 81778

手順としては以下のようにやっている。

  1. ファイルを開く
  2. ファイルがちゃんと閉じられるようdeferで指定する
  3. 開いたファイルからbufio.Readerを生成する
  4. bufio.Readerで一行読む
  5. 読みこんだ一行をfmt.Sscanfでパースする

上のやり方はわりと普通だけど、一点注意が必要なのはbufio.Readerを生成するところ。ここはGoのインタフェースの仕組みがわかってないと「?」となる。

インタフェースを活用したパッケージ

Goはインタフェースさえ実装されてれば何でもカモン、というスタンスの言語だけど、それはもちろんライブラリにも適用されている。たとえばbufio.NewReader()のインタフェースは下の通り。

func NewReader(rd io.Reader) *Reader

で、io.Readerのインタフェースは以下の通り。

type Reader interface {
    Read(p []byte) (n int, err os.Error)
}

従来のオブジェクト指向言語に慣れてると「io.Readerというインタフェースを実装したインスタンスをbufio.NewReader()に渡せばいい」と考えてしまって、それはある意味正しいんだけど、Goだと少し注意が必要。なぜなら、Goではどの型がどのインタフェースを実装しているかが明示されていないから。

実際、os.Openから返ってくるFileという型のドキュメントを見ると、そこにはio.Readerというインタフェースの名前は一切出てこない。書いてあるのはこのFileという型が実装している各種の関数だけ。でもよくよく見ると、実はFileがio.Readerのインタフェースを満たしていることがわかる。下はFile.Read()のインタフェース。

func (file *File) Read(b []byte) (n int, err Error)

これ、io.Readerが定義しているインタフェースそのまま。なので、Fileはbufio.NewReaderにそのまま渡して問題無いということ。このように、いまのドキュメントだとどの型がどのインタフェースを実装しているのかがわかりにくいため、パッケージ同士がどう連携するかがつかめなくてちょっとしたことをするにも意外と苦労することになる。

まとめ

  • flagパッケージでコマンドライン引数をパースすると楽。
  • 各種パッケージ間の連携はインタフェースによってのみ定義されており、ドキュメント上のどこかに明示されているわけではない。なので、パッケージ間で連携させるような処理を書く場合はどの型がどのインタフェースを実装しているか注意してドキュメントを読む必要がある。

2011年8月7日日曜日

Creating an original REST API using Google App Engine and Go (7)

前回でDELETEのAPIができたので、今回は最後になるPUTを実装する。

コメント更新用のAPI

従来の作りを踏襲して以下の形にする。

http://localhost:8080/1/remark/put/[コメントのID]

サーバーサイド

実装してみたところ、PUTが一番めんどうなことになっていた。DELETEのときと一緒で、PUT用のAPIとかが存在しないので、手動で色々やる必要がある。

func init() {
    http.HandleFunc("/", errorHandler(root))
    http.HandleFunc("/1/remark/post/", errorHandler(postRemark))
    http.HandleFunc("/1/remark/get/", errorHandler(getRemark))
    http.HandleFunc("/1/remark/delete/", errorHandler(deleteRemark))
    http.HandleFunc("/1/remark/put/", errorHandler(putRemark))
}
...

// Handle a put request of remarks
func putRemark(w http.ResponseWriter, r *http.Request) {
    c := appengine.NewContext(r)
    switch r.Method {
    case PUT:
        c.Infof("Putting Remark\n")
        c.Infof("Request: %s\n", r)
        c.Infof("Requested URL: %s\n", r.RawURL)
        c.Infof("Requested URL path: %s\n", r.URL.Path)

        // check if we got the updated remark
        buf := bytes.NewBuffer(nil);
        _, err := buf.ReadFrom(r.Body)
        if err != nil {
            http.Error(w, err.String(), http.StatusInternalServerError)
            return
        }
        bodyStr := buf.String()
        c.Infof("body: %s\n", bodyStr)
        values, err := http.ParseQuery(bodyStr)
        if err != nil {
            http.Error(w, err.String(), http.StatusInternalServerError)
            return
        }
        remarkString, present := values[REMARK_FIELD]
        if !present {
            msg := "Required field does not exist\n"
            c.Infof(msg)
            http.Error(w, msg, http.StatusBadRequest)
            return
        }

        // Check if we were requested for a specific remark
        id, foundID := getRemarkID(r.URL.Path)
        if !foundID {
            msg := "Specified remark does not exist\n"
            c.Infof(msg)
            http.Error(w, msg, http.StatusNotFound)
            return
        }
        query := datastore.NewQuery(REMARK_KIND).Filter("Time=", id)
        
        // check if any results exist
        count, err := query.Count(c)
        if err != nil {
            http.Error(w, err.String(), http.StatusInternalServerError)
            return
        }
        
        if count == 0 {
            msg := "Specified remark does not exist\n"
            c.Infof(msg)
            http.Error(w, msg, http.StatusNotFound)
            return
        }
        
        // update remark of given id
        itr := query.Run(c)
        for {
            var remark Remark
            key, err := itr.Next(&remark)
            if err == datastore.Done {
                break
            } else if err != nil {
                http.Error(w, err.String(), http.StatusInternalServerError)
                return
            }
            c.Infof("Updating id:%d remark:%s", key.IntID(), remark.Content)

            store := Remark {
                Content : remarkString[0],
                Time : datastore.SecondsToTime(key.IntID()), 
            }

            _, err = datastore.Put(c, key, &store)
            if err != nil {
                http.Error(w, err.String(), http.StatusInternalServerError)
            }
        }
    default:
        msg := "Invalid request\n"
        c.Infof(msg)
        http.Error(w, msg, http.StatusBadRequest)
    }
}

POSTのときと違ってParseForm()が使えないので、自分でBodyを読んでhttp.ParseQuery()を呼んで、という風な手順を踏まないといけくなっている。ただリクエストをパースする部分さえできてしまえば、あとは他のAPIとだいたい同じ。

クライアントサイド

クライアントサイドは今までAPIごとに用意してきたクライアントを一つに統合して、引数で実行を制御するように変更した。

package main

import (
    "http"
    "fmt"
    "os"
    "bytes"
    "flag"
    "strconv"
)

// print the body of the given response
func printResponse(r *http.Response) {
    fmt.Println(r.Status)
    bufSize := 256
    buf := make([]byte, bufSize)
    for {
        read, err := r.Body.Read(buf)
        if read == 0 && err == os.EOF {
            fmt.Println("Finished reading")
            break
        } else if err != nil {
            fmt.Printf("Error during read: %s",err)
            break
        }
        // convert the buffer to a string and print it
        fmt.Println(string(buf[0:read]))
    }
    r.Body.Close()
}


func main() {
    id := flag.Int64("id", 0, "Set the id of the remark")
    method := flag.Int("method", 0, "Set the method to use. GET: 0, POST:1, DELETE:2, PUT:3")
    remark := flag.String("remark", "", "Set the remark string")
    flag.Parse()

    client := new(http.Client)
    remarkUrlRoot := "http://localhost:8080/1/remark/"

    switch (*method) {
    case 0: // GET
        getUrl := remarkUrlRoot + "get"
        r, err := client.Get(getUrl)
        if err != nil {
            fmt.Printf("Error: %s",err)
            return
        }
        printResponse(r)
    case 1: // POST
        postUrl := remarkUrlRoot + "post/"
        data := http.Values { 
            "remark": {*remark,},
        }
        r, err := client.PostForm(postUrl, data)
        if err != nil {
            fmt.Printf("Error: %s",err)
            return
        }
        printResponse(r)
    case 2: // DELETE
        deleteUrl := remarkUrlRoot + "delete/" + strconv.Itoa64(*id)
        fmt.Println(deleteUrl)
        request, err := http.NewRequest("DELETE", deleteUrl, nil)
        if err != nil {
            fmt.Printf("Error: %s",err)
            return
        }
        r, err := client.Do(request)
        if err != nil {
            fmt.Printf("Error: %s",err)
            return
        }
        printResponse(r)
    case 3: // PUT
        putUrl := remarkUrlRoot + "put/" + strconv.Itoa64(*id)
        putBuf := bytes.NewBuffer([]byte("remark="+*remark))
        request, err := http.NewRequest("PUT", putUrl, putBuf)
        if err != nil {
            fmt.Printf("Error: %s",err)
            return
        }
        request.Header.Add("Content-Type", "application/x-www-form-urlencoded")
        request.Header.Add("Content-Length", string(putBuf.Len()))
        r, err := client.Do(request)
        if err != nil {
            fmt.Printf("Error: %s",err)
            return
        }
        printResponse(r)
    default:
        fmt.Println("Unknown method")
        return 
    }
}

PUTのときはPOSTのマネをしてContent-Typeにapplication/x-www-form-urlencodedを設定して送っている。この辺はもっと別のやり方でもいいと思う。

クライアント実行例

$  ./client -h
flag provided but not defined: -h
Usage of ./client:
  -id=0: Set the id of the remark
  -method=0: Set the method to use. GET: 0, POST:1, DELETE:2, PUT:3
  -remark="": Set the remark string

$ ./client
200 OK
id=1312687737000000&remark=asfasdfasdfas
id=1312687714000000&remark=aardvark
id=1312687246000000&remark=aardvark

Finished reading
$ ./client -method=2 -id=1312687737000000
200 OK
Finished reading

$ ./client
200 OK
id=1312687714000000&remark=aardvark
id=1312687246000000&remark=aardvark

$ ./client -method=3 -id=1312687246000000 -remark=thunderbolt
200 OK
Finished reading

$ ./client
200 OK
id=1312687714000000&remark=aardvark
id=1312687246000000&remark=thunderbolt

Finished reading

サーバーサイド出力(PUTしたとき)

2011/08/07 06:28:45 INFO: Putting Remark
2011/08/07 06:28:45 INFO: Requested URL: /1/remark/put/1312687246000000
2011/08/07 06:28:45 INFO: Requested URL path: /1/remark/put/1312687246000000
2011/08/07 06:28:45 INFO: body: remark=thunderbolt
2011/08/07 06:28:45 INFO: Updating id:1312687246 remark:aardvark
INFO     2011-08-07 06:28:45,116 dev_appserver.py:4248] "PUT /1/remark/put/1312687246000000 HTTP/1.1" 200 -

まとめ

ということで数回に渡ってGo + Google App EngineでRESTなAPIの基礎的な部分を試してきた。一番厄介だったのはhttpパッケージまわりで、サンプルコードもあまり無いため探りつつ実行する、という感じだった。逆にGoogle App Engine側はそれほど探ることなく、素直に書いて素直に実行できた気がする。

連載中にGoogle App Engine側もアップデートをして、Go版も色々と進化してきたのでまたおいおい別の記事も書いていきたい。

2011年8月6日土曜日

Tuning Ubuntu's Unity Desktop

普段Google App Engine関連の開発にはUbuntu 11.04を使ってて、評判はともかくとしてUnityはわりと気にいっている。まぁGnome 3もいつかは試したいけど。

Alt+Tabの切り替えが遅い

ただ前から気になってる点があって、なにかというとAlt+Tabでウィンドウを切り替えたときの微妙なもたつき。正直切り替えはもっとサクサク動いて欲しい。しばらく我慢して使ってたけど耐えられなくなってきたので、設定で改善できないか探したらわりとあっさり見つかった。

Compiz Config Settings Manager

まず必要なのはCompiz Config Settings Manager。これたしか標準では入ってないんだけど、Unityまわりのチューニングやらキーバインドを変えようと思ったら必要。自分はこれでウィンドウのタイル配置キーバインド(Ctrl+Alt+5とか)も変えている。テンキーとか無いからー。

Compiz Config Settings Managerをインストールするには以下。

sudo apt-get install compizconfig-settings-manager

表示速度の改善

Compiz Config Settings Managerがインストールされたら、ccsmコマンドで起動して、以下の手順で速度を調整する。

  1. ウィンドウ・マネジメントの項目にあるStatic Application Swicherをクリックする。
  2. Behaviourタブに移動する。
  3. Popup Window Delayを0にする。

以上。これでAlt+Tabを押したらウィンドウ候補が即表示されるようになる。ちなみに、これは値を変えた時点で即反映されるはず。

ワークスペース切り替えもついでに

自分はついでにワークスペース間の切り替えも高速化してて、その方法は以下。

  1. デスクトップの項目にあるDesktop Wallをクリックする。
  2. Viewport Switchingのタブに移動する。
  3. Wall Sliding Durationを0にする。

これでワークスペースをCtrl+Alt+右矢印とかで切りかえたときのスライドアニメーションが無くなって、高速化される。

2011年8月2日火曜日

Creating an original REST API using Google App Engine and Go (6)

前回から引きつづいて、今度はDELETE用のAPIを実装していく。

コメント削除用のAPI

REST的には削除対象はURLで表現する形になるので、今回もそれにならったAPIとする。

http://localhost:8080/1/remark/delete/[コメントのID]

サーバーサイド

API的にはgetに似ているけど、コード自体もかなりgetと似た形になる。

...
func init() {
    http.HandleFunc("/", errorHandler(root))
    http.HandleFunc("/1/remark/post/", errorHandler(postRemark))
    http.HandleFunc("/1/remark/get/", errorHandler(getRemark))
    http.HandleFunc("/1/remark/delete/", errorHandler(deleteRemark))
}
...
func getRemarkID(path string) (int64, bool) {
    split := strings.Split(path, "/", -1)
    length := len(split)
    for i := 0; i<len(split); i++ {
        if (split[i] == "get" || split[i] == "delete") &&
            (i+1 < length) { // next word may not exist
            id, err := strconv.Atoi64(split[i+1])
            return id, (err == nil)
        }
    }
    return 0,false
}
...
// Handle a delete request of remarks
func deleteRemark(w http.ResponseWriter, r *http.Request) {
    c := appengine.NewContext(r)
    switch r.Method {
    case DELETE:
        c.Infof("Deleteing Remark\n")
        c.Infof("Requested URL: %s\n", r.RawURL)
        c.Infof("Requested URL path: %s\n", r.URL.Path)
        // Check if we were requested for a specific remark
        id, foundID := getRemarkID(r.URL.Path)
        if !foundID {
            msg := "Specified remark does not exist\n"
            c.Infof(msg)
            http.Error(w, msg, http.StatusNotFound)
            return
        } else { // foun ID within URL
            query := datastore.NewQuery(REMARK_KIND).Filter("Time=", id)

            // check if any results exist
            count, err := query.Count(c)
            if err != nil {
                http.Error(w, err.String(), http.StatusInternalServerError)
                return
            }
            
            if count == 0 {
                msg := "Specified remark does not exist\n"
                c.Infof(msg)
                http.Error(w, msg, http.StatusNotFound)
                return
            }

            // remove remark with given ID
            itr := query.Run(c)
            for {
                var remark Remark
                key, err := itr.Next(&remark)
                if err == datastore.Done {
                    break
                } else if err != nil {
                    http.Error(w, err.String(), http.StatusInternalServerError)
                    return
                }
                c.Infof("Removing id:%d remark:%s", key.IntID(), remark.Content)
                datastore.Delete(c, key)
            }
        }
    default:
        msg := "Invalid request\n"
        c.Infof(msg)
        http.Error(w, msg, http.StatusBadRequest)
    }
}
...

getと違うのはIDが指定されなかったらエラーを返す点と、クエリーの結果見つかったkeyに対してdatastore.Deleteを呼んで、keyを消してる点くらい。

クライアントサイド

クライアントサイドもやっぱりgetと限りなく似ていて、DELETEは若干マイナーなのか、GETみたいな便利なAPIが無いからhttp.Requestを自分で作ってからhttp.Client.Doを呼ぶ一手間が増えたくらい。

package main

import (
    "http"
    "fmt"
    "os"
)

func main() {
    client := new(http.Client)
    remarkUrlRoot := "http://localhost:8080/1/remark/"

    deleteUrl := remarkUrlRoot + "delete/1312292876000000"
    request, err := http.NewRequest("DELETE", deleteUrl, nil)
    if err != nil {
        fmt.Printf("Error: %s",err)
        return
    }
    
    r, err := client.Do(request)
    if err != nil {
        fmt.Printf("Error: %s",err)
        return
    }
    fmt.Println(r.Status)
    
    bufSize := 256
    buf := make([]byte, bufSize)
    for {
        read, err := r.Body.Read(buf)
        if read == 0 && err == os.EOF {
            fmt.Println("Finished reading")
            break
        } else if err != nil {
            fmt.Printf("Error during read: %s",err)
            break
        }
        // convert the buffer to a string and print it
        fmt.Println(string(buf[0:read]))
    }
    r.Body.Close()
}

サーバーサイドの出力

INFO     2011-08-02 13:55:43,693 __init__.py:347] building _go_app
INFO     2011-08-02 13:55:44,656 __init__.py:333] running _go_app
2011/08/02 13:55:44 INFO: Deleteing Remark
2011/08/02 13:55:44 INFO: Requested URL: /1/remark/delete/1312292876000000
2011/08/02 13:55:44 INFO: Requested URL path: /1/remark/delete/1312292876000000
2011/08/02 13:55:44 INFO: Removing id:1312292876 remark:aardvark
INFO     2011-08-02 13:55:44,771 dev_appserver.py:4248] "DELETE /1/remark/delete/1312292876000000 HTTP/1.1" 200 -

クライアントサイドの出力

$ ./client 
200 OK
Finished reading

次回

次回はPUT。それでこの連載は一旦終わりかなー。

(続く)

2011年7月23日土曜日

Creating an original REST API using Google App Engine and Go (5.1)

2011/8/2追記: コード修正

REST APIを作るための記事を名乗っていながら、前回書いた記事では全然RESTっぽいことやってなかったことに気付いた。ということでAPI部分から変えてしきりなおし!

コメント取得用のAPI

RESTは(GETに関して言えば)極力URLで表現しようよ、というスタンスなので、前回のようにパラメータで欲しいコメントを取得するのは実はあまりREST的でない。なので、APIもよりREST的な形に変更する。

http://localhost:8080/1/remark/get/[コメントのID]

前回と違って、get以降に取得したいコメントのIDをそのままURLで指定するようにした。

サーバーサイド

APIの変更にあわせてサーバーサイドのコードも少し変える。前回からの変更点は以下の通り。

  • ".../get/"以降に直接コメントのURLが指定されてたら指定されたコメントだけを返す。
  • ".../get/"までしか指定されてなかったらコメントを全て返す。

当然、存在しないコメントのIDを指定された場合の適切なエラー処理も必要。ということで以下がそのコード。

func init() {
    http.HandleFunc("/", errorHandler(root))
    http.HandleFunc("/1/remark/post/", errorHandler(postRemark))
    http.HandleFunc("/1/remark/get/", errorHandler(getRemark))
}
...
func getRemarkID(path string) (int64, bool) {
    split := strings.Split(path, "/", -1)
    length := len(split)
    for i := 0; i<len(split); i++ {
        if split[i] == "get" &&
            i+1 < length { // next word may not exist
            id, err := strconv.Atoi64(split[i+1])
            return id, (err == nil)
        }
    }
    return 0,false
}

// Handle a get request of remarks
func getRemark(w http.ResponseWriter, r *http.Request) {
    c := appengine.NewContext(r)
    switch r.Method {
    case GET:
        c.Infof("Requested URL: %s\n", r.RawURL)
        c.Infof("Requested URL path: %s\n", r.URL.Path)
        // Check if we were requested for a specific remark
        id, foundID := getRemarkID(r.URL.Path)
        
        var query *datastore.Query
        if  foundID {
            // If we were, create a query to get that remark
            c.Infof("Retrieving specified remark id: %d\n", id)
            query = datastore.NewQuery(REMARK_KIND).Filter("Time=", datastore.Time(id))
        } else {
            // If we weren't, create a query to get all the remarks (new remarks come first)
            c.Infof("Retrieving all remarks\n")
            query = datastore.NewQuery(REMARK_KIND).Order("-Time")
        }
        
        // check if any results exist
        count, err := query.Count(c)
        if err != nil {
            http.Error(w, err.String(), http.StatusInternalServerError)
            return
        }
        
        if count == 0 {
            msg := "Specified remark does not exist\n"
            c.Infof(msg)
            http.Error(w, msg, http.StatusNotFound)
            return
        }
        
        // We have a result.
        // query for it.
        itr := query.Run(c)
        buf := bytes.NewBuffer(nil) // a variable sized buffer
        for {
            var remark Remark
            key, err := itr.Next(&remark)
            if err == datastore.Done {
                break
            } else if err != nil {
                http.Error(w, err.String(), http.StatusInternalServerError)
                return
            }
            c.Infof("Retrieved id:%d remark:%s", key.IntID(), remark.Content)
            fmt.Fprintf(buf, "id=%d&remark=%s\n", remark.Time, remark.Content)
        }

        w.Header().Set("Content-Type", "text/plain; charset=utf-8")
        io.Copy(w, buf)
    default:
        msg := "Invalid request\n"
        c.Infof(msg)
        http.Error(w, msg, http.StatusBadRequest)
    }
}
...

前回よりコードが長くなっている。注意するのは、http.HandleFunc("/1/remark/get/", errorHandler(getRemark))のようにハンドラを指定するとき、URLの末尾にはしっかりスラッシュをつけるということ。こうすることで、getから先にまだURLが続いていても同じハンドラ関数が呼ばれるようになる。このスラッシュをちゃんと指定しないと、"/1/remark/get/123151..."のようなURLへリクエストを出したときはgetRemarkハンドラが呼ばれなくなってしまう。

クライアントサイド

クライアント側はコメントのIDをURLに追加するくらいで、それほど変更点は無い。

package main

import (
    "http"
    "fmt"
    "os"
)

func main() {
    client := new(http.Client)
    remarkUrlRoot := "http://localhost:8080/1/remark/"

    getUrl := remarkUrlRoot + "get/1311387270000000"
    r, err := client.Get(getUrl)
    if err != nil {
        fmt.Printf("Error: %s",err)
        return
    }
    fmt.Println(r.Status)
    
    bufSize := 256
    buf := make([]byte, bufSize)
    for {
        read, err := r.Body.Read(buf)
        if read == 0 && err == os.EOF {
            fmt.Println("Finished reading")
            break
        } else if err != nil {
            fmt.Printf("Error during read: %s",err)
            break
        }
        // convert the buffer to a string and print it
        fmt.Println(string(buf[0:read]))
    }
    r.Body.Close()
}

サーバーサイドの出力

2011/07/23 12:17:09 INFO: Requested URL: /1/remark/get/1311387270000000
2011/07/23 12:17:09 INFO: Requested URL path: /1/remark/get/1311387270000000
2011/07/23 12:17:09 INFO: Retrieving specified remark id: 1311387270000000
2011/07/23 12:17:09 INFO: Retrieved id:1311387270 remark:aardvark
INFO     2011-07-23 12:17:09,169 dev_appserver.py:4248] "GET /1/remark/get/1311387270000000 HTTP/1.1" 200 -

クライアントサイドの出力

200 OK
id=1311387270000000&remark=aardvark

Finished reading

次回

ということで改めてGET用のAPIを作ったところで、次回はDELETEあたりをやることにする。

(続く)