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にて返却。
ソースコードはこちら