ClojureでGoogle App Engine Webアプリケーションの作成

Google App EngineのWebアプリをClojureで簡単に作成できるか試してみた。

appengine-magic

appengine-magicを使うと簡単そうだな。
https://github.com/gcv/appengine-magic/tree/
Getting Startedに従って、作ってみよう。

Google AppEngine SDKのインストール

Python版とJava版があるが、Java版をダウンロードする。
~/optにappengne-java-sdk-1.4.0を解凍する。
~/opt/appengine-java-sdk-1.4.0/binにPATHを通す。

Hello,world!を作る

まずはHello,world!を作ってみるよ。

プロジェクトの作成

helloプロジェクトを作成する。

$ lein new hello

カレントディレクトリを変更する。

$ cd hello

不要ファイルの削除

src/hello/core.cljは後でappengine-magicで生成するので、削除する。

$ rm src/hello/core.clj

依存ライブラリの取得

project.cljを編集し、:dev-dependenciesに次のライブラリを追加する。

:dev-dependencies [ [appengine-magic "0.3.2"] ]

依存ライブラリを取得し、appengine-magicプラグインを有効にする。

$ lein deps

デフォルトファイルの作成

appengine-newタスクを実行し、次のデフォルトファイルを作成する。

  1. src/hello/core.clj
  2. app_servlet.clj
  3. resources/WEB-INF/web.xml
  4. resources/WEB-INF/appengine-web.xml
$ lein appengine-new

動作確認

REPLを起動する。

user=> (use 'hello.core)
nil
user=> (require '[appengine-magic.core :as ae])
nil
user=> (ae/start hello-app)
2011-01-15 10:11:10.314:INFO::Logging to STDERR via org.mortbay.log.StdErrLog
2011-01-15 10:11:10.371:INFO::jetty-6.1.x
2011-01-15 10:11:14.795:INFO::Started SocketConnector@0.0.0.0:8080
#<Server Server@62fb35>
user=> 

ブラウザで http://localhost:8080/ を表示する。
「Hello, world!」が表示されればOK。

サーバ停止は

user=> (ae/stop)

ポート番号を変えるには、

user=> (ae/start foo-app :port 8095)

開発用サーバでのテスト

エントリポイントのサーブレットコンパイルし、アプリケーションのjarファイルを作成する。
このjarファイルと依存ライブラリをresorces/WEB-INF/libへコピーする。

$ lein appengine-prepare

開発用サーバを起動する。

$ dev_appserver.sh resources/
..snip..
情報: Started SelectChannelConnector@127.0.0.1:8080
2011/01/15 1:35:05 com.google.appengine.tools.development.DevAppServerImpl start
情報: The server is running at http://localhost:8080/

ブラウザで http://localhost:8080/ を表示する。
「Hello, world!」が表示されればOK。

Google App Engineへのデプロイ

resouces/WEB-INF/appengine-web.xmlのapplicationに公開するアプリケーションIDを設定する。
hogehoge-appを取得していれば、

  <application>hogehoge-app</application>

を設定する。

アプリケーションをデプロイする。

$ appcfg.sh update resources/
..snip..
Email: メールアドレスを入力
Password for 入力したメールアドレス: パスワードを入力
..snip..
Update completed successfully.
Success.
Cleaning up temporary files...

http://アプリケーションID.appspot.com/ にアクセスしてみる。
「Hello, world!」が表示された。
動いた。\(^▽^)/

マルチスレッドでWebページを取得する

HttpClientを使って、マルチスレッドでWebページを取得するサンプル。
leiningenを使う場合は、project.cljの:dependenciesに[commons-httpclient"3.1"]を記述し、lein depsを実行すればライブラリを取得してくれる。

(ns web-get
  (:import [org.apache.commons.httpclient HttpClient MultiThreadedHttpConnectionManager])
  (:import [org.apache.commons.httpclient.methods GetMethod]))

(defn fetch [client url]
  (let [get (GetMethod. url)]
    (println (str "Fetching: " url))
    (try
      (let [status (.executeMethod client get)]
	(println (str "Got " url ": " status)))
      (finally
       (.releaseConnection get)))))

(def client (HttpClient. (MultiThreadedHttpConnectionManager.)))
(def pages ["http://www.yahoo.co.jp/" "http://www.google.com/" "http://localhost/"])

(defn do-it []
  (doseq [url pages]
    (.start (Thread. (fn [] (fetch client url))))))

実行結果

user> (in-ns 'web-get)
#<Namespace web-get>
web-get> (do-it)
nil
Fetching: http://www.yahoo.co.jp/
Fetching: http://www.google.com/
Fetching: http://localhost/
Got http://localhost/: 200
Got http://www.yahoo.co.jp/: 200
Got http://www.google.com/: 200

動いた。\(^▽^)/

参考URL

Echo server

Programming Clojureを読んだ直後にも書いたが、またEcho serverを書いてみた。以前に作成したのは冗長だった。
clojure.contrib.server-socketとclojure.contrib.duck-streams/copyを使うと簡単だ。

(ns socket-test
  (:use [clojure.contrib.server-socket :only (create-server close-server)]
	[clojure.contrib.duck-streams :only (copy)]))

(def *server* (atom nil))

(def *port* 10000)

(defn echo-fn [in out]
  (println "echo start")
  (copy in out)
  (println "echo end"))

(defn start-server []
  (reset! *server*
	  (create-server *port* echo-fn)))

(defn stop-server []
  (when-not (nil? @*server*)
    (close-server @*server*)
    (reset! *server* nil)))

Echo serverを実行する。

user> (in-ns 'socket-test)
#<Namespace socket-test>
socket-test> (start-server)
{:server-socket #<ServerSocket ServerSocket[addr=0.0.0.0/0.0.0.0,port=0,localport=10000]>,
 :connections #<Ref@1c1eceb: #{}>}

telnetしてみる。

satoshi@tpx61 /cygdrive/c/home
$ telnet localhost 10000
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
hoge ←◆入力
hoge ←◆返ってきた
fuga ←◆入力
fuga ←◆返ってきた

telnet> Connection closed.

satoshi@tpx61 /cygdrive/c/home
$ 

動いた。\(^▽^)/

Echo serverを終了する。

socket-test> (stop-server)
nil

printlnが不要なら、start-server関数のcreate-server部分は

(create-server *port* copy)

だけで良さそうだ。

trace - 便利なdotraceマクロ

ここしばらくHaskellAndroidプログラミングに興味を引かれ、Clojureからは遠ざかっていた。blogの更新は5ヶ月ぶりか。月日が経つのは早いなぁ。

print関数を使ってデバッグしていたが、面倒くさくなってきたので、調べてみたら、ClojureでもCommon Lispみたいなtraceマクロがあった。初めから調べておけば良かったよ。

トレース出力を試すためのサンプルコード。特に意味は無いコード。

(defn mul [a b]
  (* a b))

(defn mul-n [n]
  (loop [n n
	 sum 0]
    (if (<= n 0)
      sum
      (recur (dec n) (+ sum (mul n n))))))
user> (mul-n 3)
14

mul関数をトレースしてみる。
トレースするにはdotraceマクロを使用する。dotraceの第一引数にはトレースしたい関数名をリストで渡す(複数指定できる)。第二引数以降は評価したい式を渡す。

user> (use 'clojure.contrib.trace) ←◆useして
nil
user> (dotrace [mul] (mul-n 3)) ←◆トレースする
TRACE t2746: (mul 3 3)
TRACE t2746: => 9
TRACE t2747: (mul 2 2)
TRACE t2747: => 4
TRACE t2748: (mul 1 1)
TRACE t2748: => 1
14

これは便利。\(^▽^)/

ここ http://groups.google.com/group/clojure/browse_thread/thread/5553d6ab004ad8a0/3cfe2a8709b43a99 を見ると、他のtraceライブラリの情報あるが、一年以上前の投稿なので、今はもっと良いものがあるかもしれない。

フィールドとメソッド探しにちょっと便利な関数を作ってみた

SLIMEで開発している際に、Javaのクラスが持つフィールドやメソッド名を調べたい事がある。
例えば、文字列を大文字に変換するメソッドを忘れてしまって、uppercaseだったかな?toUpperだったかな?なんて時に、ちょっと便利な関数。

使い方

(inspect 調べたいオブジェクトまたはクラス マッチ文字列)

すべてのフィールド・メソッドを表示したい場合は

(inspect 調べたいオブジェクトまたはクラス)

とする。

使用例

user> (use 'tools) ; tools名前空間をuseする
nil
user> (inspect String "upper") ; Stringクラスをupperで調べる
String toUpperCase(Locale)
String toUpperCase()
nil
user> (inspect Integer) ; Integerクラスの全てのフィールド・メソッドを調べる
Integer MIN_VALUE
Integer MAX_VALUE
Integer TYPE
Integer SIZE
int hashCode()
int reverseBytes(int)
boolean equals(Object)
...略...
user> (inspect 1.0 "infi") ; オブジェクトを指定しても良い
Double NEGATIVE_INFINITY
Double POSITIVE_INFINITY
boolean isInfinite(double)
boolean isInfinite()
nil
user> 

コード

(ns tools
  (:use [clojure.contrib.str-utils :only (str-join)]))

(defn to-class [obj]
  (if (class? obj) obj (class obj)))

(defn- filter-name [coll s]
  (filter #(not= (.. % getName toUpperCase (indexOf s)) -1)
	  (sort-by #(. % getName) coll)))

(defn- match-field [cls s]
  (let [fields (. cls getFields)]
    (if s
      (filter-name fields s)
      fields)))

(defn- match-method [cls s]
  (let [methods (. cls getMethods)]
    (if s
      (filter-name methods s)
      methods)))

(defn inspect
  ([obj] (inspect obj nil))
  ([obj s]
     (let [cls (to-class obj)
	   mstr (if s (. s toUpperCase))]
       (doseq [field (match-field cls mstr)]
	 (let [field-name (. field getName)
	       class-name (.. field getDeclaringClass getSimpleName)]
	   (println (str class-name " " field-name))))
       (doseq [method (match-method cls mstr)]
	 (let [method-name (. method getName)
	       return-class-name (.. method getReturnType getSimpleName)
	       param-class-names (for [param-class (. method getParameterTypes)]
				   (. param-class getSimpleName))]
	   (println (str return-class-name " " method-name
			 "(" (str-join ", " param-class-names) ")")))))))

clojure,clojure-contribのバージョンアップ(1.1.0のビルド方法)

今更なのですが、http://d.hatena.ne.jp/e-o-n/20091024 に書いた手順でバージョン1.0をビルド後に、バージョン1.1.0のjarを作成する手順のメモ。
~/opt以下にclojureclojure-contribをgit cloneしている。

まずはclojureから。

~% cd opt/clojure
~/opt/clojure% git branch
* (no branch)
  master
~/opt/clojure% git checkout master
Previous HEAD position was f85444e... tag for 1.0
Switched to branch "master"
~/opt/clojure% git pull
Already up-to-date. ※いつの間にかgit pullしていた...
~/opt/clojure% git tag           
1.0
1.1.0
~/opt/clojure% git checkout 1.1.0
Note: moving to "1.1.0" which isn't a local branch
If you want to create a new branch from this checkout, you may do so
(now or later) by using -b with the checkout command again. Example:
  git checkout -b <new_branch_name>
HEAD is now at 5293929... remove qualifier for 1.1.0 release
~/opt/clojure% ant
Buildfile: build.xml
...略...
clojure:
      [jar] Building jar: ~/opt/clojure/clojure-1.1.0.jar
     [copy] Copying 1 file to ~/opt/clojure
...略...
BUILD SUCCESSFUL
Total time: 21 seconds

次はclojure-contrib。

~/opt/clojure% cd ../clojure-contrib/
~/opt/clojure-contrib% git branch
* (no branch)
  master
~/opt/clojure-contrib% git checkout master
Previous HEAD position was b985ecf... Set version to 1.0.0 in Ant build and POM
Switched to branch "master"
~/opt/clojure-contrib% git pull
remote: Counting objects: 106, done.
remote: Compressing objects: 100% (70/70), done.
...略...
~/opt/clojure-contrib% git tag
1.0.0
1.0.0-RC1
1.0.0-RC2
1.1.0
1.1.0-RC1
1.1.0-RC2
1.1.0-RC3
~/opt/clojure-contrib% git checkout 1.1.0
Note: moving to "1.1.0" which isn't a local branch
If you want to create a new branch from this checkout, you may do so
(now or later) by using -b with the checkout command again. Example:
  git checkout -b <new_branch_name>
HEAD is now at d132c5f... Set version to 1.1.0 in Ant build and POM
~/opt/clojure-contrib% ant
Buildfile: build.xml
...略...
jar:
      [jar] Building jar: ~/opt/clojure-contrib/clojure-contrib.jar
      [jar] Building jar: ~/opt/clojure-contrib/clojure-contrib-slim.jar

BUILD SUCCESSFUL
Total time: 1 second

次の場所にclojureclojure-contribのjarファイルが作成される。
~/opt/clojure/clojure-1.1.0.jar
~/opt/clojure-contrib/clojure-contrib.jar

Twitter4jでつぶやく

ClojureTwitterbotを作ろうと思い、TWitterライブラリを探してみたら、Twitter4Jは扱いが簡単そうだったので試してみた。
Twitter4Jは最新の安定バージョンである2.1.0を使用した。
まずは、Twitter4Jのサンプルコードを参考に、つぶやくコードを書いてみた。

(ns twitter.hellobot
  (:import (twitter4j TwitterFactory Twitter)))

(def user-id "******")
(def password "******")

(def twitter (. (TwitterFactory.) getInstance user-id password))

(. twitter updateStatus "Hello, world!")

実行すると、twitterインスタンスの取得でエラーとなる。

(def twitter (. (TwitterFactory.) getInstance user-id password)) ←◆ここでエラー

getInstanceメソッドが見えないようだ。

No matching method found: getInstance for class twitter4j.TwitterFactory
  [Thrown class java.lang.IllegalArgumentException]

Restarts:
 0: [ABORT] Return to SLIME's top level.

Backtrace:
  0: clojure.lang.Reflector.invokeMatchingMethod(Reflector.java:85)
  1: clojure.lang.Reflector.invokeInstanceMethod(Reflector.java:28)
  2: clojure.lang.Compiler$InstanceMethodExpr.eval(Compiler.java:1150)
  3: clojure.lang.Compiler$DefExpr.eval(Compiler.java:298)
  4: clojure.lang.Compiler.eval(Compiler.java:4537)

何が原因が良く分からないので、とりあえずclojureのバージョンを1.1.0に上げて試してみたら、以下のエラーとなった。

Can't call public method of non-public class: public java.lang.Object twitter4j.TwitterFactoryBase.getInstance(java.lang.String,java.lang.String)
  [Thrown class java.lang.IllegalArgumentException]

Restarts:
 0: [ABORT] Return to SLIME's top level.

Backtrace:
  0: clojure.lang.Reflector.invokeMatchingMethod(Reflector.java:85)
  1: clojure.lang.Reflector.invokeInstanceMethod(Reflector.java:28)
  2: clojure.lang.Compiler$InstanceMethodExpr.eval(Compiler.java:1206)
  3: clojure.lang.Compiler$DefExpr.eval(Compiler.java:302)
  4: clojure.lang.Compiler.eval(Compiler.java:4647)
  5: clojure.core$eval__5236.invoke(core.clj:2017)

バージョン1.1.0ではエラーメッセージが親切になっている。
TwitterFactoryBaseクラスはTwitterFactoryクラスの親クラスであり、そのクラスがgetInstanceメソッドを実装している。エラーを見ると親クラスTwitterFactoryBaseも可視属性がpublicでないと駄目のようだ。
リフレクションの仕様?時間ができたら調べてみよう。

面倒になってきたので、twitter4jのソースを修正し、TwitterFactoryBaseクラスをpublicにしてみる。

twitter.hellobot> (def twitter (. (TwitterFactory.) getInstance user-id password))
#'twitter.hellobot/twitter
twitter.hellobot> 

正常にインスタンスを取得できたようだ。
一歩前進だが、いきあたりばったり方式でちょっと鬱。

twitterインスタンスが取得できたので、つぶやいてみる。

twitter.hellobot> (. twitter updateStatus "hello, world!")
#<StatusJSONImpl StatusJSONImpl{createdAt=Sat Jan 30 21:32:43 JST 2010, 
  id=8409854604, text='hello, world!', ...以下略...
twitter.hellobot> 

http://twitter.com/ で確認してみる。
書き込めた。\(^▽^)/


Twitter4Jは依存ライブラリが無いので、jarファイル一つで済むので、なかなか良いです。
オープンソースで公開して頂ける作者に感謝です。