ラベル UCW の投稿を表示しています。 すべての投稿を表示
ラベル UCW の投稿を表示しています。 すべての投稿を表示

2007/12/02

[Common Lisp][UCW][Elephant] UCW で作った TODO リストアプリケーションに永続化機能を追加する

以前 CUW で作成した TODO リストアプリケーション は todo オブジェクトをメモリ上に持っているだけでした。
今回は Elephant を使って todo オブジェクトを永続化してみます。
Elephant は Common Lisp のオブジェクトデータベースです。
バックエンドとして Berkeley DB、CL-SQL 経由の PostgreSQL or SQLite3 が使用可能です。Postmodern も使えるようになっているかもしれません。

elephant:open-store でデータストアをオープンします。
バックエンドで Berkeley DB を使う場合は、あらかじめディレクトリを作成しておく必要があります(ensure-directories-exist を使えばいいと思います)。

永続化するクラスは elephant:defpclass で作成します。
クラスオプションの :index に t を指定するとインスタンスは自動的に永続化されます。

elephant:defpclass で作成したクラスはユニークな識別子となる oid スロットを持ちます。

永続化したクラスの全インスタンスを取得するには elephant:get-instances-by-class を使います。

データストアからインスタンスを削除するには elephant:drop-instances を使います。

以下、ソースです。メモリ上にリストで保持していたときよりも少しシンプルになりました。

;; ucw がロードされていなければロードする。
(eval-when (:load-toplevel :compile-toplevel :execute)
(require :elephant) ; Elephant
(unless (find-package :ucw)
;; UCW の start.lisp をロードする。パスは環境にあわせて修正してください。
(load (merge-pathnames "letter/lisp/ucw/ucw-boxset/start.lisp"
(user-homedir-pathname)))))

(in-package :it.bese.ucw-user)

;; Elephant のストアをオープンする。バックエンドは Berkeley DB を使います。
(elephant:open-store `(:BDB ,(ensure-directories-exist #p"/tmp/todo/")))

(defvar *todo-list-application*
(make-instance 'cookie-session-application
:url-prefix "/todo/" ; / で終ること
:charset :utf-8 ; 文字コードを UTF-8 に設定
:debug-on-error t) ; エラー時にはデバッガを起動
"アプリケーションの作成。")

;; アプリケーションをサーバに登録する。
(register-application *default-server* *todo-list-application*)

;; エントリポイントの作成。http://localhost:8080/todo/index.ucw
(defentry-point "index.ucw" (:application *todo-list-application*)
()
(call 'top-window))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;; モデル
(elephant:defpclass todo ()
((content :initarg :content :accessor content)
(done :initform nil :accessor done))
(:index t) ; 自動的に永続化されます。
(:documentation "TODO クラス"))

(defmethod print-object ((todo todo) stream)
"debug のために"
(print-unreadable-object (todo stream :type t :identity t)
(format stream "~a: ~a" (elephant::oid todo) (content todo))))

(defun delete-todo (todo)
"TODO を削除します。"
(elephant:drop-instances (list todo)))

(defun get-all-todo ()
(elephant:get-instances-by-class 'todo))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;; ビュー
(defcomponent top-window (simple-window-component)
((body :initarg :body
:accessor body
:component todo-list-view))
(:default-initargs :title "TODO リスト")
(:documentation "トップウィンドウ。
body に一覧や編集のコンポーネントをセットして画面表示を行う。"
))

(defmethod render ((top top-window))
(<:h1 "TODO リスト")
;; body の表示
(render (body top)))

(defcomponent todo-list-view ()
()
(:documentation "TODO の一覧コンポーネント"))

(defmethod render ((self todo-list-view))
"TODO の一覧を表示する。"
(<ucw:a :action (call 'todo-create-view) "新規作成")
(<:table
:border 1
(<:tr (<:th "完了") (<:th "TODO") (<:th "削除"))
(loop for each in (get-all-todo)
do (let* ((todo each))
(<:tr
(<:td (<:as-html
(if (done todo)
"済"
(<ucw:a
:action (done-todo-action self todo)
"完了する"))))
(<:td (<:as-html (content todo)))
(<:td (<ucw:a :action (delete-todo-action self todo)
"削除する")))))))

(defaction done-todo-action ((self todo-list-view) todo)
"TODO を完了する。"
(setf (done todo) t))

(defaction delete-todo-action ((self todo-list-view) id)
"TODO を1件削除する。"
(delete-todo id))

(defcomponent todo-create-view ()
((content
:accessor content
:initform (make-instance 'string-field)))
(:documentation "TODO を新規作成するためのコンポーネント"))

(defmethod render ((self todo-create-view))
"TODO 新規作成画面"
(<ucw:form
:action (create-todo self)
"TODO" (render (content self))
(<:submit :value "新規作成"))
(<ucw:a :action (ok self) "キャンセル"))

(defaction create-todo ((self todo-create-view))
"画面からの入力により TODO を新規作成する。"
(make-instance 'todo :content (value (content self)))
(ok self))

2007/08/15

[Common Lisp][UCW] 簡単な TODO リストアプリケーションを作ってみる

UCW で簡単なアプリケーションを作成してみます。
TODO の一覧表示、新規作成と完了ができるアプリケーションです。
あまり継続を活用できいない気がしますが、そのあたりは最初のアプリということで… UCW のサンプルに Wiki アプリがあったので、今度それを見て勉強してみましょう。

まずはいつもどおりの UCW の start.lisp のロードからエントリポイントの作成です。

続いて todo クラスを定義します。
作成した todo を *todo* に保管するために、initialize-instance :after で id の設定と *todo* への push を行っています。
こうしておけば普通に make-instance するだけで、todo のインスタンスは *todo* に保管されるようになります。もっとも、今回は永続化は行っていませんので Lisp を再起動すれば消えてしまいますが。

ビューでは body スロットも持った top-window を定義し、各機能毎に body スロットにリスト表示コンポーネント、新規作成コンポーネントが設定されるようにします。top-window がテンプレート的な役割を果します。初期状態は :component todo-list-view により一覧が表示されます。

ということで、ソースは次のようになりました。


;; ucw がロードされていなければロードする。
(eval-when (:load-toplevel :compile-toplevel :execute)
(unless (find-package :ucw)
;; UCW の start.lisp をロードする。パスは環境にあわせて修正してください。
(load "/Users/ancient/letter/lisp/ucw/ucw-boxset/start.lisp")))

(in-package :it.bese.ucw-user)

(defvar *todo-list-application*
(make-instance 'cookie-session-application
:url-prefix "/todo/" ; / で終ること
:charset :utf-8 ; 文字コードを UTF-8 に設定
:debug-on-error t) ; エラー時にはデバッガを起動
"アプリケーションの作成。")

;; アプリケーションをサーバに登録する。
(register-application *default-server* *todo-list-application*)

;; エントリポイントの作成。http://localhost:8080/todo/index.ucw
(defentry-point "index.ucw" (:application *todo-list-application*)
()
(call 'top-window))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;; モデル
(defvar *todo* nil "TODO を保管するリスト")

(defvar *todo-id-counter* 0 "TODO の id カウンタ")

(defclass todo ()
((id :accessor id)
(content :initarg :content :accessor content)
(done :initform nil :accessor done))
(:documentation "TODO クラス"))

(defmethod initialize-instance :after ((todo todo) &rest initargs)
"id を設定して *todo* に保存する。"
(declare (ignore initargs))
(setf (id todo) (incf *todo-id-counter*))
(push todo *todo*))

(defmethod print-object ((todo todo) stream)
"debug のために"
(print-unreadable-object (todo stream :type t :identity t)
(format stream "~a: ~a" (id todo) (content todo))))

(defun find-todo (id)
"id をもとに TODO を取得します。"
(find id *todo* :key #'id))

(defun delete-todo (id)
"id をもとに TODO を削除します。"
(setf *todo* (delete id *todo* :key #'id)))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;; ビュー
(defcomponent top-window (simple-window-component)
((body :initarg :body
:accessor body
:component todo-list-view))
(:default-initargs :title "TODO リスト")
(:documentation "トップウィンドウ。
body に一覧や編集のコンポーネントをセットして画面表示を行う。"))

(defmethod render ((top top-window))
(<:h1 "TODO リスト")
;; body の表示
(render (body top)))

(defcomponent todo-list-view ()
()
(:documentation "TODO の一覧コンポーネント"))

(defmethod render ((self todo-list-view))
"TODO の一覧を表示する。"
(<ucw:a :action (call 'todo-create-view) "新規作成")
(<:table
:border 1
(<:tr (<:th "完了") (<:th "TODO") (<:th "削除"))
(loop for each in *todo*
do (let* ((todo each)
(id (id todo)))
(<:tr
(<:td (<:as-html
(if (done todo)
"済"
(<ucw:a
:action (done-todo-action self todo)
"完了する"))))
(<:td (<:as-html (content todo)))
(<:td (<ucw:a :action (delete-todo-action self id)
"削除する")))))))

(defaction done-todo-action ((self todo-list-view) todo)
"TODO を完了する。"
(setf (done todo) t))

(defaction delete-todo-action ((self todo-list-view) id)
"TODO を1件削除する。"
(delete-todo id))

(defcomponent todo-create-view ()
((content
:accessor content
:initform (make-instance 'string-field)))
(:documentation "TODO を新規作成するためのコンポーネント"))

(defmethod render ((self todo-create-view))
"TODO 新規作成画面"
(<ucw:form
:action (create-todo self)
"TODO" (render (content self))
(<:submit :value "新規作成"))
(<ucw:a :action (ok self) "キャンセル"))

(defaction create-todo ((self todo-create-view))
"画面からの入力により TODO を新規作成する。"
(make-instance 'todo :content (value (content self)))
(ok self))

2007/08/08

[Common Lisp][UCW] フォームの値取得と入力チェック

UCW でフォームからの値取得と入力チェックを行ってみます。

top-window コンポーネントに name スロットを定義します。
name スロットは string-field のインスタンスに初期化します。
その string-field インスタンス生成時に :validators によって必須チェックをしこみます。

入力チェックは top-window-submit アクションの中で行います。
validp メソッドによりコンポーネントのスロットに :validators で指定してあったチェック処理が実行されます。
validp は (values エラーの有無 ((スロット1 . validator1) (スロット2 . validator2))) のような多値を返します。ここからエラーの有無を判定し、エラーメッセージを取得します。
エラーがなければ、top-window の name から value メソッドで入力された値を取得し、next-page を call するときの引数にそれを渡して画面遷移します。
エラーがあれば top-window の message にエラーメッセージを設定します。

バリデーションのあたりはまだ洗練されていな感じです。
他に何かよい方法があるのでしょうか?


;; ucw がロードされていなければロードする。
(eval-when (:load-toplevel :compile-toplevel :execute)
(unless (find-package :ucw)
;; UCW の start.lisp をロードする。パスは環境にあわせて修正してください。
(load "/home/ancient/letter/lisp/ucw/ucw-boxset/start.lisp")))

(in-package :it.bese.ucw-user)

(defvar *hello-form-application*
(make-instance 'cookie-session-application
:url-prefix "/hello-form/" ; / で終ること
:debug-on-error t) ; エラー時にはデバッガを起動
"アプリケーションの作成。")

;; アプリケーションをサーバに登録する。
(register-application *default-server* *hello-form-application*)

;; エントリポイントの作成。http://localhost:8080/hello-form/index.ucw
(defentry-point "index.ucw" (:application *hello-form-application*)
()
;; トップページの無限ループ
(loop (call 'top-window)))

(defcomponent top-window (simple-window-component)
((name
:accessor name
:initform (make-instance
'string-field
:validators `(,(make-instance 'not-empty-validator
:message "名前を入力してください。")))
:documentation "必須入力チェックを行なう。")
(messages :initform nil :accessor messages
:documentation "入力エラー時のメッセージを保持するために"))
(:default-initargs :title "トップページ")
(:documentation "最初のページです。入力フォームがあります。"))

(defmethod render ((self top-window))
"入力フォームを表示します。"
;; エラーメッセージの表示
(when (messages self)
(<:ul (dolist (message (messages self))
(<:li (<:b (<:as-html message))))))
;; フォームの表示
(<ucw:form :action (top-window-submit self)
(<:div "名前" (render (name self))
(<:submit))))

(defaction top-window-submit ((self top-window))
"入力チェックを行い、次のページに遷移します。"
(setf (messages self) nil)
(multiple-value-bind (validp faileds) (validp self)
(if validp
(call 'next-page :name (value (name self)))
(setf (messages self)
(mapcar #'(lambda (arg)
(ucw::message (cdr arg)))
faileds)))))

(defcomponent next-page (simple-window-component)
((name :initarg :name :accessor name))
(:default-initargs :title "次のページです")
(:documentation "前のページの入力を表示するためのページです。"))

(defmethod render ((self next-page))
"前のページで入力した name を表示します。"
(<:p (<:as-html (name self)))
(<ucw:a :action (ok self) "最初のページに戻る"))

2007/08/03

[Common Lisp][UCW] 継続によるページ遷移

前回のハローワールドアプリケーションにもう1ページ追加し、ページ遷移をしてみます。
defentry-point で loop を使い2つのページを call します。
各ページでは <ucw:a でリンクを作成し、その action で ok を呼びます。
ok は call の戻りになります。
hello-world-window からの ok は次の next-page を呼び出し、next-page からの ok は loop により再度 hello-world-window を呼び出し、それがぐるぐるまわります。
継続を使ったページ制御です。


;; ucw がロードされていなければロードする。
(eval-when (:load-toplevel :compile-toplevel :execute)
(unless (find-package :ucw)
;; UCW の start.lisp をロードする。
(load "/home/ancient/letter/lisp/ucw/ucw-boxset/start.lisp")))

;; ucw のユーザ用パッケージ。
;; 簡単なアプリケーションならこのパッケージを使うのが簡便。
(in-package :it.bese.ucw-user)

;; アプリケーションの作成。
(defvar *hello-world-application*
(make-instance 'cookie-session-application
;; http://localhost:8080/hello/ でこのアプリケーションの
;; アクセスできるようにする。
:url-prefix "/hello/" ; / で終ること
))

;; アプリケーションをサーバに登録する。
(register-application *default-server* *hello-world-application*)

;; エントリポイントの作成。
;; http://localhost:8080/hello/index.ucw で
;; hello-world-window の render メソッドが呼ばれる。
(defentry-point "index.ucw" (:application *hello-world-application*)
()
;; 1ページ目と2ページ目をループする。
(loop
;; ハローワールドのページ
(call 'hello-world-window)
;; 次のページ
(call 'next-page)))

;; hello-world-window の定義。
;; simple-window-component を継承する。
(defcomponent hello-world-window (simple-window-component)
()
(:default-initargs :title "ハローワールド")) ; title の設定。

;; 表示用のメソッドの定義
(defmethod render ((hello hello-world-window))
(<:p "ハローワールド")
(<ucw:a :action (ok hello) "次のページへ"))

;; 次のページを定義する。
(defcomponent next-page (simple-window-component)
()
(:default-initargs :title "次のページです"))

;; 次のページの表示
(defmethod render ((self next-page))
(<:p "次のページに遷移しました。")
(<ucw:a :action (ok self) "最初のページに戻る"))

2007/07/29

[Common Lisp][UCW] UnCommon Web で Hello World

先日はサンプルを動かすところまでだった UCW(UnCommon Web)ですが、今回は Hello Wold アプリを作成してみます。
ソースは次のとおりです。5行目の load の引数は UCW のインストール先にあわせて変更してください。
おおまかには次のような感じになっています。


  1. cookie-session-application のインスタンス生成でアプリケーションを作成。

  2. register-application でアプリケーションのデフォルトサーバに登録。

  3. defentry-point でエントリポイントの作成。

  4. defcomponent でページを定義。

  5. defmethod render でページの表示の仕方を定義。


UCW は継続ベースです。URL が各ページに1対1に対応しているわけではありません。エントリポイント(入口)を定義する必要があります。今回は1ページだけなので、全然イメージができないかもしれませんが…

;; ucw がロードされていなければロードする。
(eval-when (:load-toplevel :compile-toplevel :execute)
(unless (find-package :ucw)
;; UCW の start.lisp をロードする。
(load "/home/ancient/letter/lisp/ucw/ucw-boxset/start.lisp")))

;; ucw のユーザ用パッケージ。
;; 簡単なアプリケーションならこのパッケージを使うのが簡便。
(in-package :it.bese.ucw-user)

;; アプリケーションの作成。
(defvar *hello-world-application*
(make-instance 'cookie-session-application
;; http://localhost:8080/hello/ でこのアプリケーションの
;; アクセスできるようにする。
:url-prefix "/hello/" ; / で終ること
))

;; アプリケーションをサーバに登録する。
(register-application *default-server* *hello-world-application*)

;; エントリポイントの作成。
;; http://localhost:8080/hello/index.ucw で
;; hello-world-window の render メソッドが呼ばれる。
(defentry-point "index.ucw" (:application *hello-world-application*)
()
(call 'hello-world-window))

;; hello-world-window の定義。
;; simple-window-component を継承する。
(defcomponent hello-world-window (simple-window-component)
()
(:default-initargs :title "ハローワールド")) ; title の設定。

;; 表示用のメソッドの定義
(defmethod render ((hello hello-world-window))
(<:p "ハローワールド"))

次回はページ遷移をしてみましょう。