lift入門

どうも最新のVersionでliftの入門的な情報が、日本語ではなかなか無いので、本家Todo Sample Application | Lift Space | Assemblaを和訳してみることにする。一部、筆者注記を入れています。

尚、ここで扱うバージョンは以下の通り。

java : java version "1.6.0_22"
scala : Scala code runner version 2.8.1.final
lift : 2.2 final


まずは、このサイトのチュートリアルを開始する前に、ブランクプロジェクトを作成する。

mvn archetype:generate \
-DarchetypeGroupId=net.liftweb \
-DarchetypeArtifactId=lift-archetype-basic_2.8.1 \
-DarchetypeVersion=2.2 \
-DarchetypeRepository=http://scala-tools.org/repo-releases \
-DremoteRepositories=http://scala-tools.org/repo-releases \
-DgroupId=com.liftworkshop \
-DartifactId= todo \
-Dversion=1.0

一行にしたのがこれ。

mvn archetype:generate -DarchetypeGroupId=net.liftweb -DarchetypeArtifactId=lift-archetype-basic_2.8.1 -DarchetypeVersion=2.2 -DarchetypeRepository=http://scala-tools.org/repo-releases -DremoteRepositories=http://scala-tools.org/repo-releases -DgroupId=com.liftworkshop -DartifactId=todo -Dversion=1.0

これで、todoというプロジェクトができあがります。

todoディレクトリに移動して、todoアプリを起動してみましょう。

mvn jetty:run

f:id:teppei-studio:20110219190817p:image

現時点で最新版のlift1.0.1をインストールすると、maven2.2.0が組み込まれてしまいますが、ここでjetty:run起動すると、maven2.2.1を求められます。lift1.0.1をアンインストールして、maven2.2.1をインストールしておきましょう。環境変数のお忘れずに。筆者の ~/.bash_profile は以下のようにしています。

export M2_HOME=/usr/local/maven-2.2.1
export M2=$M2_HOME/bin
export MAVEN_OPTS="-noverify"
export PATH=$M2:$PATH

export SCALA_HOME=/Users/ITPUser/apps/scala-2.8.1.final
export PATH=$PATH:$SCALA_HOME/bin

条件付き表示

Todoリストは、個人に紐付くものなので、Todoリスト表示前にログインしているかどうかを確認する必要があります。ユーザがログインしているときにアプリケーションを表示するという、挙動でこれを表現します。最初の反復では、ログイン済みユーザに対してWelcomeバナーを表示し、その後でTodoアプリケーションを表示します。
これを、その中にユーティリティ関数を含むコードスニペットを作ることで実現します。これらの関数(loggedIn と loggedOut)は、条件が合致していれば子ノードへのパスを返し、さもなければ空のSequenceを返します。テンプレートは、このスニペットを適切なコンテンツに囲ませることができます。

この考え方について詳しく知りたければ、Templates and Binding | Lift Space | Assemblaを参照してください。

odo/src/main/scala/com/liftworkshop/snippet に、以下のように Util.scala を作成。

package com.liftworkshop.snippet

import scala.xml.{NodeSeq}
import com.liftworkshop._
import model._

class Util {
 def loggedIn(html: NodeSeq) =
   if (User.loggedIn_?) html else NodeSeq.Empty

 def loggedOut(html: NodeSeq) =
   if (!User.loggedIn_?) html else NodeSeq.Empty
}
<lift:surround with="default" at="content"> 
 <lift:Util.loggedOut>Please 
   <lift:menu.item name="Login">Log In</lift:menu.item> 
   </lift:Util.loggedOut> 
 <lift:Util.loggedIn>
     Welcome. You may
   <lift:menu.item name="Logout">Log Out</lift:menu.item> if you wish
   </lift:Util.loggedIn> 
</lift:surround>

と、スニペットは、Userのログイン状態から、タグで囲まれたHtmlをそのまま返すか、空のノードを返すの処理を振り分けます。

f:id:teppei-studio:20110219203750p:image

loginボタンを押下と、ログイン画面が出ます。

f:id:teppei-studio:20110219203752p:image

「sign up」でアカウント作ると、ログインした状態になり、この画面になります。

f:id:teppei-studio:20110219204944p:image

Modelを作る

次は、Todoオブジェクトを格納するためのModelを作成するステップです。

src/main/scala/com/liftworkshop/model/に、ToDo.scala を作る。

package com.liftworkshop.model 

import net.liftweb._ 
import mapper._ 
import http._ 
import SHtml._ 
import util._ 

class ToDo extends LongKeyedMapper[ToDo] with IdPK { 
    def getSingleton = ToDo 
    object done extends MappedBoolean(this) 
    object owner extends MappedLongForeignKey(this, User) 
    object priority extends MappedInt(this) { 
        override def defaultValue = 5 
    } 
    object desc extends MappedPoliteString(this, 128) 
} 

object ToDo extends ToDo with LongKeyedMetaMapper[ToDo]

次のステップは、SchemifierにこのModelを加えることです。

src/main/scala/boostrap/liftweb/Boot.scala を更新して、ToDoモデルへの参照を含むようにします。

サイトでは、

Schemifier.schemify(true, Schemifier.infoF _, User)

Schemifier.schemify(true, Log.infoF _, User, ToDo)

に直すようありますが、これだとLog4jが無いとエラーになるので、

Schemifier.schemify(true, Schemifier.infoF _, User,	ToDo)

と直します。

これで、jettyサーバを再起動すると、コンソール上に、

CREATE TABLE todo (priority INTEGER , id BIGINT NOT NULL AUTO_INCREMENT , done BOOLEAN , desc_c VARCHAR(128) , owner BIGINT)
ALTER TABLE todo ADD CONSTRAINT todo_PK PRIMARY KEY(id)
CREATE INDEX todo_owner ON todo ( owner )

と出て、テーブルが生成されたことが分かります。

基本的な永続性

ここまでTodoデータを格納するModelを作成してきました。次は、todoデータを入力するForm画面と、データベースにInsertする処理を作ります。
src/main/scala/com/liftworkshop/snippet/TD.scala を作り、以下のようにコーディングしましょう。

package com.liftworkshop.snippet

import com.liftworkshop._
import model._

import net.liftweb._
import http._
import SHtml._
import S._

import js._
import JsCmds._

import mapper._

import util._
import Helpers._

import scala.xml.{NodeSeq, Text}

class TD {
 def add(form: NodeSeq) = {
   val todo = ToDo.create.owner(User.currentUser)

   def checkAndSave(): Unit =
   todo.validate match {
    case Nil => todo.save ; S.notice("Added "+todo.desc)
    case xs => S.error(xs) ; S.mapSnippet("TD.add", doBind)
   }

   def doBind(form: NodeSeq) =
   bind("todo", form,
      "priority" -> todo.priority.toForm,
      "desc" -> todo.desc.toForm,
      "submit" -> submit("New", checkAndSave))

   doBind(form)
 }
}

以下のコードを含むように、src/main/webapp/index.html を更新しましょう。

<lift:surround with="default" at="content">
   <lift:Util.loggedOut>
       Please <lift:menu.item name="Login">Log In</lift:menu.item>
   </lift:Util.loggedOut>
   <lift:Util.loggedIn>
       <lift:TD.add form="post">
        <table>
          <tr>
           <td>Description:</td>
           <td><todo:desc>To Do</todo:desc></td>
          </tr>

          <tr>
           <td>
             Priority 
           </td>
           <td>
             <todo:priority>
              <select><option>1</option></select>
             </todo:priority>
          </td>
          </tr>
          <tr>
           <td>&nbsp;</td>
           <td>
             <todo:submit>
              <button>New</button>
             </todo:submit>
           </td>
          </tr>
        </table>
       </lift:TD.add>
   </lift:Util.loggedIn>
</lift:surround>


WEBアプリケーションを、リロードして、ログインしなおすと、DescriptionとPriorityの入力欄の画面を確認することができます。この状態で、Todo情報を登録することはできますが、まだそれをリストアップする機能は作っていません。

f:id:teppei-studio:20110219230644p:image


※ 現状、チュートリアルではまだリストを表示する機能の実装については書かれていません。
※ が、作ってみたら簡単だったので、本家に先行して追記します。

src/main/webapp/index.html に

<table width="90%"><lift:TD.list /></table>

を追記します。
追記したものがこれ↓

<lift:surround with="default" at="content">
  <lift:Util.loggedOut>
    Please <lift:menu.item name="Login">Log In</lift:menu.item>
  </lift:Util.loggedOut>
  <lift:Util.loggedIn>
    <lift:TD.add form="post">
      <table>
        <tr>
          <td>Description:</td>
          <td><todo:desc>To Do</todo:desc></td>
        </tr>
        <tr>
          <td>
            Priority
          </td>
          <td>
            <todo:priority>
              <select><option>1</option></select>
            </todo:priority>
          </td>
        </tr>
        <tr>
          <td>&nbsp;</td>
          <td>
          <td>
            <todo:submit>
              <button>New</button>
            </todo:submit>
          </td>
        </tr>
      </table>
    </lift:TD.add>
    <table width="90%"><lift:TD.list /></table>
  </lift:Util.loggedIn>
</lift:surround>

src/main/scala/com/liftworkshop/snippet/TD.scala に、スニペットメソッド list を追加します。

  def list(html:NodeSeq):NodeSeq = {
    <tr>{ToDo.htmlHeaders}</tr> ::
    ToDo.findAll().flatMap(t => <tr>{t.htmlLine}</tr>)
  }

これだけ。簡単ですね。findAllメソッドやら、htmlLineメソッド、htmlHeadersメソッドは、ToDoクラスが継承しているどっかのクラスで実装されているみたいです。

f:id:teppei-studio:20110220202554p:image