Lift + Maven で始める Webサイト開発

先日のScalaハッカソンでは、あまりコーディングはできませんでしたが、LiftWebフレームワークを使っての開発を @NKJason さんにいろいろ教えて頂いたので、忘れないうちに書いておきます。LiftとATNDのAPIを使ってATNDのイベント検索機能を実装しました。自分でも実装してみながら、流れを書きます。ビルドツールは何でもいいのだと思いますが、今回はMavenです。

環境は、

  • MacOSX10.6
  • Maven2.2.1
  • Scala2.8.0

プロジェクトの準備
MavenリポジトリからデフォルトのLiftプロジェクトをダウンロードして配置します。

#!/bin/bash

mvn archetype:generate -U \
-DarchetypeGroupId=net.liftweb \
-DarchetypeArtifactId=lift-archetype-basic_2.8.0.RC7 \
-DarchetypeVersion=2.1-SNAPSHOT \
-DarchetypeRepository=http://scala-tools.org/repo-snapshots \
-DremoteRepositories=http://scala-tools.org/repo-snapshots \
-DgroupId=$2 -DartifactId=$1

何度も使いそうなので、シェルにしました。
この辺の設定内容は、もう少し詳しく確認してから追記します。
ひとまず、-DgroupIdはパッケージ名を、-DartifactIdはプロジェクト名を
指定するところなので、オプションで指定するようにしました。

lift_setup Hello net.knserve.lift.hello

上記のコマンドを実行するとカレントディレクトリにデフォルトのプロジェクトが作られます。
ソースを変更する前にとりあえず動かしてみましょう。
作成されたプロジェクトのルートディレクトリ(pom.xmlがある場所)に移動して、

mvn package
mvn jetty:run

と順番に打つとビルドされ、jettyが起動した状態になります。
localhost:8080にアクセスすれば、何も変更されていないLiftによる画面が見えるはずです。

開発作業
Liftでは主にテンプレートとスニペットというクラスを使った開発を行うようです。

  1. スニペットのクラスの中でビューとなるhtmlタグをbindするように書く。
  2. bindしたhtmlタグをテンプレートから呼び出す。

というような開発手順になります。その辺りの情報は公式wikiで言うとこの辺りでしょうか。

webapp配下です

─ webapp                  
├── WEB-INF             
│      └── web.xml        
├── images              
│      └── ajax-loader.gif
├── index.html          
├── static              
│      └── index.html     
└── templates-hidden    
    ├── default.html    
    └── wizard-all.html 

Scalaのソースコードが置かれているパッケージ配下です。

─ net                                      
└── knserve                              
    └── lift                             
        └── hello                        
            ├── comet                    
            ├── lib                      
            │      └── DepencyFactory.scala
            ├── model                    
            │      └── User.scala          
            ├── snippet                  
            │      └── HelloWorld.scala    
            └── view                     

HelloWrold.scalaをSample.scalaという名前にコピーして編集していきます。
編集前のソースです。

package net.knserve.lift.hello {
package snippet {

import _root_.scala.xml.{NodeSeq, Text}
import _root_.net.liftweb.util._
import _root_.net.liftweb.common._
import _root_.java.util.Date
import net.knserve.lift.hello.lib._
import Helpers._

class HelloWorld {
  lazy val date: Box[Date] = DependencyFactory.inject[Date] // inject the date

  def howdy(in: NodeSeq): NodeSeq =
  Helpers.bind("b", in, "time" -> date.map(d => Text(d.toString)))

  /*
   lazy val date: Date = DependencyFactory.time.vend // create the date via factory

   def howdy(in: NodeSeq): NodeSeq = Helpers.bind("b", in, "time" -> date.toString)
   */
}

}
}


どういうページにするか考え中...。
結局あんま思いつかなかったのでソースを解説します。
基本的に@NKJason さんのコードのままですが、
メソッド名などは変えて、コメントを加えました。

package com.snssuite.atnd {
package snippet {

import _root_.scala.xml.{NodeSeq, Text}
import _root_.net.liftweb.util._
import _root_.net.liftweb.common._
import _root_.java.util.Date
import com.snssuite.atnd.lib._
import Helpers._

//追加でインポート
import scala.xml._
import net.liftweb.util.HttpHelpers._ 
import net.liftweb.http._

class AtndSearch {

  def searchBox( in: NodeSeq ): NodeSeq = {
    //searchパラメータを取得して格納
    //検索窓のデフォルト値として利用
    var keyword = S.param("search") openOr ""
    
    //検索パラメータを付けたURLへリダイレクト
    def search() = {
      S.redirectTo("/static/index?search=" + urlEncode( keyword ))
    }
    
    //コンポーネントをバインド
    Helpers.bind("searchbox", in
             , "input" -> SHtml.text( keyword, keyword = _ )
             , "submit"   -> SHtml.submit("検索", search))
  }

  def resultList( in: NodeSeq ): NodeSeq = {
    //searchパラメータを取得
    val keyword = S.param("search") openOr ""

    //searchパラメータからURLを作成して、xmlで結果を得る
    val xml = XML.load("http://api.atnd.org/events/?keyword_or=" + urlEncode( keyword ) + "&format=xml") 
    //events配下のeventの一覧をリストで取得
    val xmlList = ( xml \\ "events" \\ "event" ) 

    xmlList.flatMap( xm => Helpers.bind("resultlist", in
                , "title" -> (xm \\ "title").map(_.text).mkString
                , "event_id" -> SHtml.link("http://atnd.org/events/" + (xm \\ "event_id").map(_.text).mkString 
                                 , () =>"" , scala.xml.Text((xm \\ "event_id").map(_.text).mkString))
                ))

  }

}

}
}
<lift:surround with="default" at="content">

  <!-- searchBoxメソッドのバインド -->
  <lift:AtndSearch.searchBox form="POST">
    <searchbox:input />&nbsp;
    <searchbox:submit />
  </lift:AtndSearch.searchBox>

  <table border="1">
    <tr>
      <td>title</td>
      <td>eventLink</td>
    </tr>
    <!-- resultListメソッドのバインド -->
    <lift:AtndSearch.resultList>
    <tr>
      <td><resultlist:title /></td>
      <td><resultlist:event_id /></td>
    </tr>
    </lift:AtndSearch.resultList>
  </table> 

</lift:surround>

Lift + Maven で始める Webサイト開発

先日のScalaハッカソンでは、あまりコーディングはできませんでしたが、LiftWebフレームワークを使っての開発を @NKJason さんにいろいろ教えて頂いたので、忘れないうちに書いておきます。LiftとATNDのAPIを使ってATNDのイベント検索機能を実装しました。自分でも実装してみながら、流れを書きます。ビルドツールは何でもいいのだと思いますが、今回はMavenです。

環境は、

  • MacOSX10.6
  • Maven2.2.1
  • Scala2.8.0

プロジェクトの準備
MavenリポジトリからデフォルトのLiftプロジェクトをダウンロードして配置します。

#!/bin/bash

mvn archetype:generate -U \
-DarchetypeGroupId=net.liftweb \
-DarchetypeArtifactId=lift-archetype-basic_2.8.0.RC7 \
-DarchetypeVersion=2.1-SNAPSHOT \
-DarchetypeRepository=http://scala-tools.org/repo-snapshots \
-DremoteRepositories=http://scala-tools.org/repo-snapshots \
-DgroupId=$2 -DartifactId=$1

何度も使いそうなので、シェルにしました。
この辺の設定内容は、もう少し詳しく確認してから追記します。
ひとまず、-DgroupIdはパッケージ名を、-DartifactIdはプロジェクト名を
指定するところなので、オプションで指定するようにしました。

lift_setup Hello net.knserve.lift.hello

上記のコマンドを実行するとカレントディレクトリにデフォルトのプロジェクトが作られます。
ソースを変更する前にとりあえず動かしてみましょう。
作成されたプロジェクトのルートディレクトリ(pom.xmlがある場所)に移動して、

mvn package
mvn jetty:run

と順番に打つとビルドされ、jettyが起動した状態になります。
localhost:8080にアクセスすれば、何も変更されていないLiftによる画面が見えるはずです。

開発作業
Liftでは主にテンプレートとスニペットというクラスを使った開発を行うようです。

  1. スニペットのクラスの中でビューとなるhtmlタグをbindするように書く。
  2. bindしたhtmlタグをテンプレートから呼び出す。

というような開発手順になります。その辺りの情報は公式wikiで言うとこの辺りでしょうか。

webapp配下です

─ webapp                  
├── WEB-INF             
│      └── web.xml        
├── images              
│      └── ajax-loader.gif
├── index.html          
├── static              
│      └── index.html     
└── templates-hidden    
    ├── default.html    
    └── wizard-all.html 

Scalaのソースコードが置かれているパッケージ配下です。

─ net                                      
└── knserve                              
    └── lift                             
        └── hello                        
            ├── comet                    
            ├── lib                      
            │      └── DepencyFactory.scala
            ├── model                    
            │      └── User.scala          
            ├── snippet                  
            │      └── HelloWorld.scala    
            └── view                     

HelloWrold.scalaをSample.scalaという名前にコピーして編集していきます。
編集前のソースです。

package net.knserve.lift.hello {
package snippet {

import _root_.scala.xml.{NodeSeq, Text}
import _root_.net.liftweb.util._
import _root_.net.liftweb.common._
import _root_.java.util.Date
import net.knserve.lift.hello.lib._
import Helpers._

class HelloWorld {
  lazy val date: Box[Date] = DependencyFactory.inject[Date] // inject the date

  def howdy(in: NodeSeq): NodeSeq =
  Helpers.bind("b", in, "time" -> date.map(d => Text(d.toString)))

  /*
   lazy val date: Date = DependencyFactory.time.vend // create the date via factory

   def howdy(in: NodeSeq): NodeSeq = Helpers.bind("b", in, "time" -> date.toString)
   */
}

}
}


どういうページにするか考え中...。

GoogleAppEngine+Slim3のファイルアップロードをScalaで書き直す

【変更前】元ネタは、 http://slim3demo.appspot.com/upload/;jsessionid=dBGn20s5q7i28ZwKBGOrFw

package slim3.demo.service;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import org.slim3.controller.upload.FileItem;
import org.slim3.datastore.Datastore;
import org.slim3.util.ByteUtil;

import slim3.demo.meta.UploadedDataFragmentMeta;
import slim3.demo.meta.UploadedDataMeta;
import slim3.demo.model.UploadedData;
import slim3.demo.model.UploadedDataFragment;

import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.Transaction;

public class UploadService {
    
    //1つのデータの最大値は1MなのでFragmentのサイズを900000に設定
    private static final int FRAGMENT_SIZE = 900000;
    
    //DataとDataFragmentそれぞれのエンティティへのマッピングクラスを取得
    private UploadedDataMeta d = UploadedDataMeta.get();
    private UploadedDataFragmentMeta f = UploadedDataFragmentMeta.get();
    
    //アップロードしたDataの一覧を取得
    public List<UploadedData> getDataList() {
        return Datastore.query(d).asList();
    }
    
    //DataをDataFragmentに分割しながらアップロード
    public UploadedData upload(FileItem formFile) {
        //ファイルが空だったら、nullを返す
        if (formFile == null) {
            return null;
        }
        List<Object> models = new ArrayList<Object>();
        UploadedData data = new UploadedData();
        //datastoreに保存するデータの
        models.add(data);
        //各種Dataのパラメータ指定
        data.setKey(Datastore.allocateId(d));
        data.setFileName(formFile.getShortFileName());
        data.setLength(formFile.getData().length);
        //formFileをバイト化
        byte[] bytes = formFile.getData();
        //bytesをFragmentのサイズに分割
        byte[][] bytesArray = ByteUtil.split(bytes, FRAGMENT_SIZE);
        //byteArrayの長さ分のKeyを生成
        Iterator<Key> keys =
            Datastore
                .allocateIds(data.getKey(), f, bytesArray.length)
                .iterator();
        for (int i = 0; i < bytesArray.length; i++) {
            //FragmentData用のbyte[]
            byte[] fragmentData = bytesArray[i];
            //インスタンス生成しmodelsに格納
            UploadedDataFragment fragment = new UploadedDataFragment();
            models.add(fragment);
            //Fragmentに各種プロパティを設定
            fragment.setKey(keys.next());
            fragment.setBytes(fragmentData);
            fragment.setIndex(i);
            fragment.getUploadDataRef().setModel(data);
        }
        //トランザクション開始
        Transaction tx = Datastore.beginTransaction();
        //Data,Fragmentを含むモデルオブジェクトの一覧をdatastoreに追加していく
        for (Object model : models) {
            Datastore.put(tx, model);
        }
        //トランザクション終了
        tx.commit();
        //dataのオブジェクトを返す
        return data;
    }
    
    //Dataの情報を取得
    public UploadedData getData(Key key, Long version) {
        return Datastore.get(d, key, version);
    }
    
    //取得したDataの情報からFragmentをかき集めて修復
    public byte[] getBytes(UploadedData uploadedData) {
        //Dataが空だった場合にNullPointExceptionを投げる
        if (uploadedData == null) {
            throw new NullPointerException(
                "The uploadedData parameter must not be null.");
        }
        //DataからFragmentの一覧を取得
        List<UploadedDataFragment> fragmentList =
            uploadedData.getFragmentListRef().getModelList();
        //各々のFragmentからbyteを取得してbyte[][]に格納
        byte[][] bytesArray = new byte[fragmentList.size()][0];
        for (int i = 0; i < fragmentList.size(); i++) {
            bytesArray[i] = fragmentList.get(i).getBytes();
        }
        //bite[][]をjoinしてリターン
        return ByteUtil.join(bytesArray);
    }
    
    //削除するDataのKeyを指定してDataとFragmentの両方を削除
    public void delete(Key key) {
        Transaction tx = Datastore.beginTransaction();
        //削除するエンティティのKey一覧
        List<Key> keys = new ArrayList<Key>();
        //削除するエンティティ一覧にDataとFragmentのKeyを追加
        keys.add(key);
        keys.addAll(Datastore.query(f, key).asKeyList());
        //トランザクション内で削除とトランザクション完了
        Datastore.delete(tx, keys);
        Datastore.commit(tx);
    }
}

【変更後】

package slim3.demo.service;

import java.util.ArrayList
import java.util.Iterator
import java.util.List

import org.slim3.controller.upload.FileItem
import org.slim3.datastore.Datastore
import org.slim3.util.ByteUtil

import slim3.demo.meta.UploadedDataFragmentMeta
import slim3.demo.meta.UploadedDataMeta
import slim3.demo.model.UploadedData
import slim3.demo.model.UploadedDataFragment

import com.google.appengine.api.datastore.Key
import com.google.appengine.api.datastore.Transaction

import scala.collection.JavaConversions._


class UploadService(
        //DataとDataFragmentそれぞれのエンティティへのマッピングクラスを取得
        var d: UploadedDataMeta = UploadedDataMeta.get(),
        var f: UploadedDataFragmentMeta = UploadedDataFragmentMeta.get()
    ){
    
    object UploadService {
        //1つのデータの最大値は1MなのでFragmentのサイズを900000に設定
        val FRAGMENT_SIZE: Int = 900000;
    }
    
    //アップロードしたDataの一覧を取得
    def getDataList(): java.util.List[UploadedData] = {
        Datastore.query(d).asList();
    }
    
    //DataをDataFragmentに分割しながらアップロード
    def upload( formFile: FileItem ): UploadedData = formFile match {
        //★caseにしてみた
        case null => null
        case _ => {
            val models: List[Object] = Nil
            val data: UploadedData = new UploadedData()
            //datastoreに保存するデータの
            models.add(data);
            //☆この辺のオブジェクトのパラメータを指定してくとこをシンプルにする方法ない?
            data.setKey(Datastore.allocateId(d))
            data.setFileName(formFile.getShortFileName())
            data.setLength(formFile.getData().length)
            //Fragmentのサイズに分割
            val bytesArray: Array[Array[Byte]] = ByteUtil.split(formFile.getData, UploadService.FRAGMENT_SIZE);
            //Key一覧を生成
            val keys: Iterator[Key] = Datastore.allocateIds(data.getKey(), f, bytesArray.length).iterator();
            for { i <- 0 to bytesArray.length - 1 } {
                //FragmentData用のbyte[]
                var fragmentData = bytesArray(i)
                //インスタンス生成しmodelsに格納
                var fragment: UploadedDataFragment = new UploadedDataFragment()
                models.add(fragment)
                //★この辺のオブジェクトのパラメータを指定してくとこをシンプルにする方法ない?
                fragment.setKey(keys.next())
                fragment.setBytes(fragmentData)
                fragment.setIndex(i)
                fragment.getUploadDataRef().setModel(data)
            }
            //トランザクション開始
            val tx: Transaction = Datastore.beginTransaction()
            //★modelsのmodelを全部datastoreに追加
            models.map( model => Datastore.put(tx, model) )
            //トランザクション終了
            tx.commit()
            //dataのオブジェクトを返す
            data
        }
    }
    
    //Dataの情報を取得
    def getData(key: Key, version: Long): UploadedData = {
        Datastore.get(d, key, version)
    }
    
    //取得したDataの情報からFragmentをかき集めて修復
    def getBytes(uploadedData: UploadedData): Array[Byte] = uploadedData match {
        //☆ケースにしてみた
        case null => throw new NullPointerException( "The uploadedData parameter must not be null." )
        //★引数のフラグメントをmapで各々バイトに変換してArrayに格納,Arrayを最後にjoin
        case _ => ByteUtil.join(uploadedData.getFragmentListRef().getModelList().map( item => item.getBytes() ).toArray )
    }
    
    //削除するDataのKeyを指定してDataとFragmentの両方を削除
    def delete(key: Key): Unit =  {
        val tx: Transaction = Datastore.beginTransaction();
        //削除するエンティティのKey一覧
        val keys: List[Key] = Nil
        //削除するエンティティ一覧にDataとFragmentのKeyを追加
        keys.add(key);
        //★全部追加するメソッドとか元々ある
        keys.addAll(Datastore.query(f, key).asKeyList());
        //トランザクション内で削除とトランザクション完了
        Datastore.delete(tx, keys);
        Datastore.commit(tx);
    }
}

CentOSにvsftpd-2.2.2をソースからインストール

tar -xzvf vsftpd-2.2.2.tar.gz
cd vsftpd-2.2.2
make


すると、vsf_findlibs.sh の実行のところで
/lib/libcap.so.1、/lib/libpam.so.0 が読めないとか言うエラーが出る。


これはCentOSの64bit版を使っていることが原因のようで、
ひとまず対処策として、vsf_findlibs.sh 内の /lib/libcap.so.1 を /lib64/libcap.so.1 に
/lib/libpam.so.0 を /lib64/libpam.so.0 にそれぞれ変更することでひとまず make 完了。
vsf_findlibs.sh ではライブラリの場所の設定を行っているようで。
利用できるライブラリのパスが追加できればOK。


/usr/share/empty/ と ユーザ nobody がいない場合はそれぞれ作成。
/var/ftp と ユーザ ftp がいない場合はそれぞれ作成。

make install


インストール完了
ちなみに今回みつからなかったライブラリの
PAMとCAPについてだが、


PAM:

CAP:

CentOSにWordPressをインストール

転けたとこ3つ

  • SELinuxの影響でwp-admin/install.phpが実行できない
    • →とりあえずSELinuxを停止
  • PHPのモジュールが足りなくて実行できない
    • →足りないと言われてるモジュールをyumでサクッとインストール
  • MySQLのユーザ権限が正しく設定されておらず、DBにアクセスできない
    • とりあえずローカルからのアクセスだけなのでmysql@localhostに全権限を与える

無事インストール完了