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