Liftことはじめ その7 自作validation

Liftで、validationルールの自作 備忘録
modelクラスの属性に、validationをメソッドをoverride
BandSeqPlayer.scala

package code.model

import net.liftweb.mapper._
import net.liftweb.util._
import net.liftweb.common._

object BandSeqPlayers extends BandSeqPlayers with LongKeyedMetaMapper[BandSeqPlayers] 

class BandSeqPlayers extends Relation with LongKeyedMapper[BandSeqPlayers] with IdPK with OneToMany[Long, BandSeqPlayers] {
  def getSingleton = BandSeqPlayers
…
  object bandseq extends LongMappedMapper(this, BandSeq)
  object player extends LongMappedMapper(this, Player)
  object seq extends MappedLong(this) {
    override def validations =
      minVal _ :: super.validations
    def minVal(in: Long): List[FieldError] =
      if (in > 0 ) Nil
      else List(FieldError(this, <li>Seq must be over 1</li>))
  }

上記のコードでは、seq属性のvalidationsにminValメソッドを追加。
minValメソッドは、0以下に数値をエラーとする。

利用側では、
validateの結果が、errorsとなる。

            bandSeqPlayers.validate match {
              case Nil => {
                bandSeqPlayers.save
                S.notice(msg)
                S.redirectTo(path)
              }
              case errors => {
                S.error(errors)
                S.redirectTo(path)
              }
            }

ソースコードこちら

LiftをOPENSHIFTにデプロイ mysql設定の覚書

前回の記事mysqlの設定に関する覚書。
OPENSHIFTのcartridgeにmysql5.5を追加すると、以下の用なメッセージが表示される。

MySQL 5.5 database added.  Please make note of these credentials:

       Root User: mysql_user
   Root Password: mysql_password
   Database Name: database_name

Connection URL: mysql://$OPENSHIFT_MYSQL_DB_HOST:$OPENSHIFT_MYSQL_DB_PORT/

mysqlサーバーのホスト名とポートが、環境変数にセットされている!
liftのデフォルトのリソースファイル(src/main/resources/props/default.props)のキー、db.url=に環境変数を記述してもアクセスできる訳もない。
やむなく、src/main/scala/bootstrap/liftweb/Boot.scala環境変数を参照する用に修正。

src/main/resources/props/default.props:

db.driver     =com.mysql.jdbc.Driver
db.user       =mysql_user
db.password   =mysql_password
db.url_prefix =jdbc:mysql://
db.host       =OPENSHIFT_MYSQL_DB_HOST
db.port       =OPENSHIFT_MYSQL_DB_PORT
db.database   =database_name

src/main/scala/bootstrap/liftweb/Boot.scala

class Boot {
  def boot {
    if (!DB.jndiJdbcConnAvailable_?) {
      sys.props.put("h2.implicitRelativePath", "true")
      val url = (Props.get("db.url"): Option[String]) match {
       // default.propsにdb.urlのキーがあれば、それを採用。
        case Some(url) => url
       // 無ければ、各々から組み立てる。その際に環境変数から値を取得。
        case _ => { 
          val url_prefix = Props.get("db.url_prefix").getOrElse("")
          val host       = System.getenv(Props.get("db.host").getOrElse(""))
          val port       = System.getenv(Props.get("db.port").getOrElse(""))
          val database   = Props.get("db.database").getOrElse("")
          // 2バイト文字を使用出来るように、urlにパラメータを追加。
          url_prefix + host + ":" + port + "/" + database + "?useUnicode=true&characterEncoding=utf8"
        }
      }
      val vendor = 
	new StandardDBVendor(Props.get("db.driver") openOr "org.h2.Driver",
                             url,
			     Props.get("db.user"), Props.get("db.password"))

      LiftRules.unloadHooks.append(vendor.closeAllConnections_! _)

      DB.defineConnectionManager(util.DefaultConnectionIdentifier, vendor)
    }

LiftをOPENSHIFTにデプロイ

前述の記事のアプリが形になってきたので、デモ公開してみる。
利用するのは、RedHat社のPaas OPENSHIFTにデプロイ。
CookBookを確認するとtomcat上で可動とのこと。

まず, OpenShiftにsignup。 Getting Started guide で、SSH keyの設定や、コマンドラインツール(RHC)のインストール等行う。

ログインしたら、Add Application。アプリケーションタイプの選択は、
Tomcat 7 (JBoss EWS 2.0) を選択。アプリケーションの作成が出来たら、Mysql5.5とphpMyAdmin4.0も追加しておく。

作成した、OPENSHIFTのアプリを、ローカル、git clone。
USER_IDは、rhc ssh [app-name]で、確認できる。

  $ git clone \
     ssh://[USER_ID]@[app-name]-[YOUR_DOMAIN].rhcloud.com/~/git/[app-name].git/
  $ cd [app-name]/

デプロイするliftアプリケーションは、以下のコマンドにてパッケージ。

  $ cd [Lift application root]
  $ sbt package

warファイルは、デフォルトでは、[Lift application root]/target/[scala ver]配下に出来る。
warを、git cloneした、プロジェクトフォルダのwebappsフォルダ配下に配置。
あとは、gitでOPENSHIFTに送り込む。

  $git add -A
  $git commit -m "nice message"
  $git push

http://[app-name]-[YOUR_DOMAIN].rhcloud.com/[war file neme]にアクセスして確認。

本記事のアプリの公開はこちら

2018年追記
2017/09をもって、open shift ver2がサービス停止してしまったので
現在は、HEROKUにて、アプリ公開中。

Liftことはじめ その6 mapper ManyToMany

前述の記事の覚書
LiftのORMでManyToManyを実装。
AlbumとTrack間で実装。(同一Trackが、複数のAlbumに収録される事実を踏まえ。regular Albumとbest AlbumのTrack共用。)
まずは、Albumクラス。
model/Album.scala

package code.model

import net.liftweb.mapper._
import net.liftweb.util._
import net.liftweb.common._

object Album extends Album with LongKeyedMetaMapper[Album] {
  override def dbTableName = "albums"
}

class Album extends LongKeyedMapper[Album] with IdPK with ManyToMany with OneToMany[Long, Album] {
  def this(albumtitle: String) = {
    this()
    this.albumtitle(albumtitle)
  }
 
  def getSingleton = Album

  object albumtitle extends MappedString(this, 100) {
    override def validations =
      valMaxLen(100, "message must be under 100 characters long ") _ ::
      valMinLen(1, "you have to input") _ ::
      super.validations
  }
  object band extends LongMappedMapper(this, Band)

  def getBand(): Band = {
    Band.findAll(By(Band.id, band.get)).head
  } 

  object tracks extends MappedManyToMany(AlbumTracks, AlbumTracks.album, AlbumTracks.track, Track, OrderBy(AlbumTracks.seq, Ascending))
  object albumTracks extends MappedOneToMany(AlbumTracks, AlbumTracks.album, OrderBy(AlbumTracks.seq, Ascending))

}

(Albumクラスが、OneToManyトレイトもmixinしているのは、AlbumTracksクラスとOneToManyのアソシエーションを構成しているため)

続いて、Trackクラス

package code.model

import net.liftweb.mapper._
import net.liftweb.util._
import net.liftweb.common._

object Track extends Track with LongKeyedMetaMapper[Track] {
  override def dbTableName = "tracks"
}

class Track extends LongKeyedMapper[Track] with IdPK with ManyToMany with OneToMany[Long, Track] {
  def this(seq: Long, tracktitle: String) = {
    this()
    this.tracktitle(tracktitle)
  }

  def getSingleton = Track

  object tracktitle extends MappedString(this, 100) {
    override def validations =
      valMaxLen(100, "name length must be under 100 characters long ") _  ::
      valMinLen(1, "you have to input!!") _ ::
      super.validations
  }

  object albums extends MappedManyToMany(AlbumTracks, AlbumTracks.track, AlbumTracks.album, Album)

  object attaches extends MappedOneToMany(Attach, Attach.track, OrderBy(Attach.id, Ascending))
  object albumTracks extends MappedOneToMany(AlbumTracks, AlbumTracks.track, OrderBy(AlbumTracks.album, Ascending))

}

(Trackクラスが、OneToManyトレイトもmixinしているのは、AtachクラスとOneToManyのアソシエーションを構成しているため)

最後に、AlbumTracksクラス
model/AlbumTracks.scala

package code.model

import net.liftweb.mapper._
import net.liftweb.util._
import net.liftweb.common._

object AlbumTracks extends AlbumTracks with LongKeyedMetaMapper[AlbumTracks]

class AlbumTracks extends LongKeyedMapper[AlbumTracks] with IdPK {
  def this(album: Long, track: Long, seq: Long) = {
    this()
    this.seq(seq)
    this.album(album)
    this.track(track)
  }
  def getSingleton = AlbumTracks
  object album extends LongMappedMapper(this, Album)
  object track extends LongMappedMapper(this, Track)
  object seq extends MappedLong(this)
  def getTrack(): Track = Track.findAll(By(Track.id, track.get)).head
  def setSeq(seq: Long): Unit = {this.seq(seq)} 
}

ちょっと悩んだのは、IdPKトレイトをmixinしたこと、ManyToManyを構成する分には、
不要であるが、Aubum、Track間のアソシエーションを削除するためにmixinした。

以下は、利用のためのコード
(登録、更新)

          album.tracks += track
          album.save

と実装すればよいのだが、seq(曲順)の属性をAlbumTracksの属性にしたかったので、個別にインスタンス化した。

              val albumTrack: AlbumTracks = AlbumTracks.create.album(albumid.toLong).track(track.id.get).seq(seq.toLong)
              albumTrack.save

(削除)

           val album = Album.findAll(By(Album.id, getAlbumId().toLong)).head
           album.tracks -= track
           album.save

ソースコードこちら

Liftことはじめ その5 mapper OneToMany

前述の記事の覚書
LiftのORMでOneToManyを実装。
各trackの添付を複数指定可能にする。(必然性はないが、音楽ファイルとTab符とか)
One側のTrackクラスは、OneToManyをMixIn。属性にattachesを定義。
model/Track

package code.model

import net.liftweb.mapper._
import net.liftweb.util._
import net.liftweb.common._

object Track extends Track with LongKeyedMetaMapper[Track] {
  override def dbTableName = "tracks"
}

class Track extends LongKeyedMapper[Track] with IdPK with OneToMany[Long, Track]{
  def this(albumid: Long, seq: Long, tracktitle: String) = {
    this()
    this.albumid(albumid)
    this.seq(seq)
    this.tracktitle(tracktitle)
  }

  def getSingleton = Track

  object albumid extends MappedLong(this)
   
  object seq extends MappedLong(this)

  object tracktitle extends MappedString(this, 100) {
    override def validations =
      valMaxLen(100, "name length must be under 100 characters long ") _  ::
      valMinLen(1, "you have to input!!") _ ::
      super.validations
  }

  object attaches extends MappedOneToMany(Attach, Attach.track, OrderBy(Attach.id, Ascending))

}

Many側のAttachクラス。属性にtrackを定義。

package code.model

import net.liftweb.mapper._

object Attach extends Attach with LongKeyedMetaMapper[Attach] {
  override def dbTableName = "attaches"
}

class Attach extends LongKeyedMapper[Attach] with IdPK {
  def getSingleton = Attach

  def this(filename: String, mimetype: String, trackattach: Array[Byte]) = {
    this()
    this.filename(filename)
    this.mimetype(mimetype)
    this.trackattach(trackattach)
  }

  object filename extends MappedString(this, 100)
  object mimetype extends MappedString(this, 40)
  object trackattach extends MappedBinary(this)
  object track extends LongMappedMapper(this, Track)
}

利用は、以下のようなコード。

package code.snippet

import java.io._
import scala.xml.{NodeSeq, Text}
import net.liftweb.util._
import net.liftweb.common._
import Helpers._

import code.model._
import net.liftweb.mapper._
import net.liftweb.http._
import S._
import SHtml._
import net.liftweb.http.js.{JsCmd, JsCmds}

class TrackView {
…
// Save
  def addProcess() {
    try {
      val track: Track = isAtachFileExist(upload) match {
        case true => {
          val attach: Attach = new Attach(getFileParamHolder(upload).fileName, getFileParamHolder(upload).mimeType, getFileParamHolder(upload).file)
          val track: Track = Track.create.albumid(albumid.toLong).seq(seq.toLong).tracktitle(tracktitle)
          track.attaches += attach
          track.save
          track
        }
        case false => new Track(albumid.toLong, seq.toLong, tracktitle)
      }
      track.validate match{
        case Nil => {
          track.save()
          S.notice("Added " + track.tracktitle)
          S.redirectTo("/track?albumid=" + albumid)
        }
        case x => {
          S.error("Validation Error!")
          S.redirectTo("/track?albumid=" + albumid)
        }
      }
    } catch {
      case e: java.lang.NumberFormatException => {
        S.error("SEQ must be the number!")
        S.redirectTo("/track?albumid=" + albumid)
      }
    }
  }
// Select
  private def doList(reDraw: () => JsCmd)(html: NodeSeq): NodeSeq = {
    val tracks:List[Track] = Track.findAll(By(Track.albumid, getAlbumId().toLong), OrderBy(Track.seq, Ascending))
    bind("track", html, "albumid" -> <input type="text" name="albumid" class="column span-10"/>)
    tracks.flatMap(trk => {
      trk.attaches.flatMap(atc => {
…
}

ソースコードこちら

Liftことはじめ その4 アップロードファイルのサイズ制限設定

前述の記事の覚書。

アップロードファイルのサイズ制限。
scala/bootstrap/liftweb/Boot.scala

class Boot {
  def boot {
…
    // Upload file size capped at 100Mb
    LiftRules.maxMimeSize = 100 * 1024 * 1024
    LiftRules.maxMimeFileSize = 100 * 1024 * 1024
  }
}

ついでに、mariadbファイルサイズ制限:com.mysql.jdbc.PacketTooBigException
max_allow_packetを調整。
/etc/mysql/my.cnf

#
# * Fine Tuning
#
max_connections         = 100
connect_timeout         = 5
wait_timeout            = 600
#max_allowed_packet     = 16M
max_allowed_packet      = 100M
thread_cache_size       = 128
sort_buffer_size        = 4M
bulk_insert_buffer_size = 16M
tmp_table_size          = 32M
max_heap_table_size     = 32M

Liftことはじめ その3 ファイルダウンロード

前述の記事の覚え書き

ファイルダウンロード
snippet/TrackView.scala

  private def doList(reDraw: () => JsCmd)(html: NodeSeq): NodeSeq = {
    val tracks:List[Track] = Track.findAll(By(Track.albumid, getAlbumId().toLong), OrderBy(Track.seq, Ascending))
    bind("track", html, "albumid" -> <input type="text" name="albumid" class="column span-10"/>)
    tracks.flatMap(trk =>
      bind("track", html, AttrBindParam("id", trk.id.toString, "id"),
                          "seq" -> <span>{link("track?albumid=" + getAlbumId() + "&seq=" +trk.seq.get, () => (), Text(trk.seq.toString))}</span>,
                          "tracktitle" -> <span>{trk.tracktitle.toString}</span>,
                          "filename" -> <span>{link("lob/" + trk.id.get.toString, () => (), Text(trk.filename.toString))}</span>,
                          "delete" -> <span>{link("track?albumid=" + getAlbumId(), () => delete(trk.id.get), Text("delete"))}</span>
      )
    )
  }

filenameに、パス /lobを定義。

scala/bootstrap/liftweb/Boot.scala

class Boot {
  def boot {
…
    // Download url
    import code.lib._
    LiftRules.statelessDispatchTable.append{
      case Req( "lob" :: id :: Nil, _, _ ) =>
        () => TrackDownload.download(id.toLong)
    }

idを引数とするdownloadメソッドを定義。

lib/TrackDownload.scala

package code.lib

import java.io._
import javax.mail.internet._
import net.liftweb.common._

import code.model.Track
import net.liftweb.mapper._
import net.liftweb.http._

object TrackDownload {
  def download(id: Long): Box[LiftResponse] = {
    val track: Track = (Track.findAll(By(Track.id, id))).head
    val bais = new ByteArrayInputStream(track.trackatach.get)
    val attachment = "attachment; filename=\'" + MimeUtility.encodeWord(track.filename.get.replace(" ", "_"), "ISO-2022-JP", "B") + "\'"
    val content = track.mimetype.get + "; charset=UTF-8"
    val headers = ("Content-Type" -> content) :: ("Content-length" -> track.trackatach.get.length.toString) ::("Content-disposition" -> attachment) :: Nil
    Full(
      StreamingResponse(bais, () => {bais.close}, track.trackatach.get.length, headers, Nil, 200)
    )
  }
}

lobをByteArrayInputStreamに取り込み、headerをセットし、StreamingResponseにて返却。
ソースコードこちら