LiftでWebアプリ(4): フォーム用共通処理を作る 1
フォームの書き方は複数ある。
http://stable.simply.liftweb.net/#toc-Chapter-4
LiftScreenを使ったフォームの自動生成は、HTMLを編集しにくいので却下。
手軽そうなStatefulSnippetを使ってみる。
サンプルに上がっているようなprocess関数でのエラーチェックは使いにくいのでListScreenのように変数自体にバリデーターを割り当てる方式でいく。
まずはWicketのようにrawInputと指定した型の変数を保持するtraitを作る。
trait FilterValidator extends StatefulSnippet { trait ValueWithFilterValidator[T] { /* * 型に応じて変換メソッドをサブクラスで実装する */ protected def convertToString: T => String protected def convertFromString: String => T /** * Internal Value */ protected var _value: Box[T] = Empty /** * SHtmlなどに渡すゲッター */ def get: String = rawInput /** * SHtmlなどに渡すセッター */ protected def set(a: String) { rawInput = a ... // ここでフィルタやバリデータやコンバータなどを使って… ... _value = a2 } /** * フィルタを設定する */ def |(fv: String => String): ValueWithFilterValidator[T] = { this } def filter(fv: String => String): ValueWithFilterValidator[T] = |(fv) /** * バリデータを設定する */ def <<(f: T => Box[String]): ValueWithFilterValidator[T] = { this } def validator(f: T => Box[String]): ValueWithFilterValidator[T] = <<(f) } implicit def fromValue(a: String) = new ValueWithFilterValidator[String] { override protected def convertToString: (String) => String = a => a override protected def convertFromString: (String) => String = a => a } implicit def fromValue(a: Int) = new ValueWithFilterValidator[Int] { override protected def convertToString: (Int) => String = (in: Int) => in.toString override protected def convertFromString: String => Int = (in:String) => in.toInt } // template method override def dispatch = { case "render" => { hasErrors = false render } case _ => (a: NodeSeq) => a } def render: NodeSeq => NodeSeq def onSubmit() { if (hasErrors) onError() else onSuccess() } def onSuccess() def onError() = () }
StatefulSnippetを継承して、エラーがなければonSuccessを呼び出す。
フォームの値は型に応じてimplicitで変換する。
利用するクラスでは次のように書きたい。
class MyForm extends FilterValidator { //def dispatch: DispatchIt = { case "render" => render } はtraitで定義されている private val name1 = "" | trim private val name2 = "" << { case "bad" => Full("not accept [bad]") case _ => Empty } | (_.replace("a", "b")) private val num1 = 1 << min(2, "num1 min error.") << max(5, "num1 max error.") | (trim) required override def render = { ... } override def onSuccess() = { ... }
演算子の記号は考える余地があるが、LiftScreen.scalaに定義されている<*>や^/よりかは見やすいかと。
ブラウザでアクセスした時の処理の流れは以下のようになっている。
- 初回アクセス
- dispatch が呼び出される("render" 以外は渡ってこない?以下renderとする)
- renderで設定したget系関数が呼び出される
- ブラウザ描写
- submitボタンをクリックする
- renderで設定したset系関数が呼び出される
- onSubmit(SHtml.onSubmitUnitで登録した関数)が呼び出される
- renderが呼び出される
- get系関数が呼び出される
- ブラウザ描写
これをふまえて、StringとIntのフィールドが動くまで出来た。
ここから一気にtraitやimplicitなどが出てくる。まだ理解が及ばないので雑なコードだけど取りあえず貼る。
trait FormUtil { trait FilterValidator extends StatefulSnippet { protected def invalidFormat(label: String, input: String):String = label + " is invalid format [" + input + "]." protected def emptyError(label: String):String = label + " is empty." var hasErrors: Boolean = false trait ValueWithFilterValidator[T] { protected var rawInput: String = "" var hasError = false protected val isRequired = false protected var fieldLabel: String = "Field" protected var errorId: Box[String] = Empty protected val stringFilters: List[String => String] = Nil protected val validators: List[T => Box[String]] = Nil //protected val invalidFunc: ((String, String) => String) = invalidFormat // TODO: 型別に対応する /* * 型に応じて変換メソッドをサブクラスで実装する */ protected def convertToString: T => String protected def convertFromString: String => T /** * Internal Value */ protected var _value: Box[T] = Empty protected def htmlError(){ hasError = true hasErrors = true } protected def htmlError(msg: String){ htmlError() errorId match { case Full(a) => S.error(a, msg) case _ => S.error(msg) } } /** * 入力された値があればFull(入力値)を返却する */ def valueBox = _value protected def value_=(a: String) { rawInput = a val a1 = (a /: stringFilters) { (v, f) => f(v) } if (!isRequired && a1.length() == 0){ }else{ if (isRequired && a1.length() == 0){ htmlError(emptyError(fieldLabel)) }else{ convertAndSet() } } def convertAndSet(){ val a2 = tryo{convertFromString(a1)} def checkError(v: T, list: List[T => Box[String]]):Boolean = list match { case Nil => false case f :: fs => f(v) match { case Full(a) => htmlError(a); true case _ => checkError(v, fs) } } a2 match { case Full(a3) => hasError = checkError(a3, validators) case _ => htmlError(invalidFormat(fieldLabel, rawInput)) } _value = a2 } } /** * SHtmlなどに渡すゲッター */ def get: String = rawInput /** * SHtmlなどに渡すセッター */ def set = (in:String) => value_=(in) class CopySelf[T](old: ValueWithFilterValidator[T]) extends ValueWithFilterValidator[T] { errorId = old.errorId fieldLabel = old.fieldLabel hasError = old.hasError rawInput = old.rawInput _value = old._value override protected def convertToString = old.convertToString override protected def convertFromString = old.convertFromString override protected val stringFilters = old.stringFilters override protected val validators = old.validators } /** * idとnameの文字列を指定してSHtml.textを生成する */ def #>(idName: String): CssBindFunc = ("name=" + idName) #> SHtml.text(get, set) & <--(idName) /** * id、nameの文字列とElem生成関数を指定してElem生成する */ def #>(idName: String, f: (String, String => Any, ElemAttr*)=> Elem, attr: ElemAttr*): CssBindFunc = ("name=" + idName) #> f(get, set, attr:_*) & <--(idName) /** * エラーメッセージのIDを設定し、ラベルをHTMLから取得して設定する */ def <--(id: String): CssBind = { errorId = Full(id) ("for="+id) #> {in: NodeSeq => :@(in.text); in} } /** * エラーメッセージで利用するラベルを設定する */ def :@(name: String): ValueWithFilterValidator[T] = { fieldLabel = name this } def labelWith(name: String): ValueWithFilterValidator[T] = :@(name) /** * エラーメッセージのIDを設定する */ def :#(errId: String): ValueWithFilterValidator[T] = { errorId = Full(errId) this } def errorIdWith(errId: String): ValueWithFilterValidator[T] = :#(errId) /** * フィルタを設定する */ def |(fv: String => String): ValueWithFilterValidator[T] = { val old = this fv match { case f: (String => String) => new CopySelf[T](old) { override protected val stringFilters: List[String => String] = f :: old.stringFilters } case _ => htmlError("internal error"); old } } def filter(fv: String => String): ValueWithFilterValidator[T] = |(fv) /** * バリデータを設定する */ def <<(f: T => Box[String]): ValueWithFilterValidator[T] = { val old = this new CopySelf[T](old) { override protected val validators = f :: old.validators } } def validator(f: T => Box[String]): ValueWithFilterValidator[T] = <<(f) def as(w: With) = w match { case Required => mkRequired() } trait WithRequired { self: ValueWithFilterValidator[T] => /** * 入力された値を返却する。 * * onErrorでは私用不可。 * onSuccessでNullPointerExceptionが発生したらバグ */ def value:T = _value.open_! } def required = mkRequired() private def mkRequired() = new CopySelf[T](this) with WithRequired { protected override val isRequired = true } /** * フィールドに変換する */ def asField: ValueWithFilterValidator[T] = this } private sealed trait With private[FilterValidator] case class RequiredCase() extends With val Required = RequiredCase() /** * type=submitのボタンにsubmit関数を登録する */ def #!(): CssBind = "type=submit" #> SHtml.onSubmitUnit(onSubmit) implicit def fromValue(a: String) = new ValueWithFilterValidator[String] { override protected def convertToString: (String) => String = a => a override protected def convertFromString: (String) => String = a => a } implicit def fromValue(a: Int) = new ValueWithFilterValidator[Int] { override protected def convertToString: (Int) => String = (in: Int) => in.toString override protected def convertFromString: String => Int = (in:String) => in.toInt } // Filters def trim: String => String = { case null => null case s => s.trim() } // Validators def min(min: Int, msg: String): Int => Box[String] = { case a if a < min => Full(msg) case _ => Empty } def max(max: Int, msg: String): Int => Box[String] = { case a if a > max => Full(msg) case _ => Empty } // template method override def dispatch = { case "render" => { println("*** render ***: " + hasErrors) hasErrors = false render } case _ => (a: NodeSeq) => a } def render: NodeSeq => NodeSeq def onSubmit() { println("*** onSubmit ***: " + hasErrors) if (hasErrors) onError() else onSuccess() } def onSuccess() def onError() = () } }
やたらHelper的な関数が増えた。varとvalが混ざっていたりCopySelfのようなクラスがあったりRequiredのcase classがイマイチだったりするが、その辺は後で考える。
Requiredがなければ入力値はBoxでしか受け取れないが、Requiredを付けると型変換後の値を直接受け取れるようになる。
受け取りがStringで良いなら失敗は無いので、これも後で考える。
本当は独自演算子は全部 | にしてパイプのように繋げたかったんだけど、そうすると型とmatchの省略ができなくなって利用側で型を書く必要が出てくるので諦めた。
演算子の優先順位があるので、バリデータ、フィルタ、as Requiredの順に書かないといけない。右から入力値が来るイメージで。
利用するページでは FormUtil._をimportして
trait CustomFilterValidator { override def invalidFormat(label: String, input: String) = label + "に入力された値 " + input + " が数値ではありません。" override def emptyError(label: String) = label + " は入力必須です。" } class MyForm extends FilterValidator with CustomFilterValidator { private val name = "" :# "errorFieldId" | trim private val name1 = "".errorIdWith("errorFieldId1").filter(trim).validator{ case "b" => Full("err") // all error } private val name2 = "" :# "name2" << { case "b" => Full("not accept b") case _ => Empty } | (_.replace("a", "b")) private val num2 = 1 :# "num2" << min(2, "num2 min error ") << max(5, "num2 max error ") | trim private val num3 = 1 :# "num3" << min(2, "num3 min error ") << max(5, "num3 max error ") | trim as Required override def render = num2 <-- "num2" & num3 <-- "num3" & "name=name" #> SHtml.text(name.get, name.set) & "name=name2" #> SHtml.text(name2.get, name2.set) & "name=num2" #> SHtml.text(num2.get, num2.set) & "name=num3" #> SHtml.text(num3.get, num3.set) & "type=submit" #> SHtml.onSubmitUnit(onSubmit) override def onSuccess() = { S.notice("ON SUCCESS!!") println("hasErrors: " + hasErrors) println(num3.value) } override def onError() = { S.error("ON ERROR!!") println("ON ERROR!! num2 error:" + num2.hasError) } }
このように書ける。
途中まで書いていて、エラーメッセージで利用するラベルは誰が決める?という疑問がふと出てきた。
そこで <-- メソッドを使ってHTMLからラベルを取得するようにした。ここまできたら、もう #> メソッドも独自に実装したらいいんじゃないか?ということで #> メソッドも追加した。これを使って最終的には
class LoginForm extends FilterValidator { private val mail = "" << { case a if (a.matches("[a-zA-Z0-9._+-]+@([a-zA-Z0-9]+\\.)+[a-zA-Z]{2,6}")) => Empty case _ => Full("メールアドレスが正しくありません。") } | (trim) as Required private val password = "" as Required override def render = mail #> "mail" & password #> ("password", SHtml.password) & #! override def onSuccess(){ println(mail.value) println(password.value) } }
<html> <body> <form class="lift:LoginForm?form=post"> <label for="mail">メールアドレス</label>: <input type="text" name="mail" id="mail" value=""> <span class="lift:msg?id=mail"></span> <br> <label for="password">パスワード</label>: <input name="password" type="password" id="password" value=""> <span class="lift:msg?id=password"></span> <br> <input type="submit"> </form> </body> </html>
こんな感じに。エラーメッセージ用のidやラベルの取得は、名前を揃えると自動化される。
ここまで作ると、ステートフルの意味はあまり無い気がしてきた。逆に余計なデータをセッションに溜め込むので非効率になる気がする。
リファクタリングも兼ねて、onSubmit方式のフォームをベースにもう一度作ろうか…。
その他メモ。
require を後置演算子っぽくしようとしたらエディタ警告が出た。
http://twitter.com/#!/odersky/status/49882758968905728
それで一時的にcase classに。
JRebelを使わずに SBTで ~jetty-run を使って何度もリロードすると
java.lang.OutOfMemoryError: PermGen space
のエラーが出るが、解決方法はよくわからない。
オプションで XX:+CMSClassUnloadingEnabled -XX:+CMSPermGenSweepingEnabled を指定してもダメみたい。
もしかしたらJRebelでも長時間起動していたら同じ現象になるのかもしれない。
LiftでWebアプリ(3): データベースの設定
本番用・開発用のデータベースを設定する。
Debian squeeze 標準の PostgreSQL 8.4 を使う。
aptititude install postgresql
pom.xml を編集。
<dependency> <groupId>postgresql</groupId> <artifactId>postgresql</artifactId> <version>8.4-702.jdbc4</version> </dependency>
データベースを作成。
createuser scala-cart -P createdb -O scala-cart -E UTF-8 scala-cart
DB接続情報をプロパティファイル src/main/resources/props/production.default.props に書く。
db.driver=org.postgresql.Driver db.url=jdbc:postgresql:scala-cart db.user=scala-cart db.password=topsecret
http://www.assembla.com/spaces/liftweb/wiki/Set_Up_Jetty_and_PostgreSQL
http://www.assembla.com/spaces/liftweb/wiki/Run_Modes
ローカルでもテストするために、RunWebApp の設定で VM parameters に -Drun.mode=production を追加する。
確認のためにHelloWorld.scala を修正。
def howdy = "#time *" #> date.map(_.toString + " : " + Props.mode)
環境切り替えのために Boot.scala を編集する。
if (!DB.jndiJdbcConnAvailable_?) { val vendor = Props.mode match { case Props.RunModes.Production => new StandardDBVendor(Props.get("db.driver") openOr "org.postgresql.Driver", Props.get("db.url") openOr "jdbc:postgresql:mydatabase", Props.get("db.user"), Props.get("db.password")) case _ => new StandardDBVendor(Props.get("db.driver") openOr "org.h2.Driver", Props.get("db.url") openOr "jdbc:h2:lift_proto.db;AUTO_SERVER=TRUE", Props.get("db.user"), Props.get("db.password")) }
前回の続きだとフォームが見れないので UrlRewriteFilter を設定して確認できるようにする。
<rule match-type="regex"> <from>^(/public/|/user_mgt/|/classpath/|/ajax_request/)(.*)$</from> <to last="true">$1$2</to> </rule>
user_mgtを追加。
起動したらDBのテーブルが自動生成されるので、PostgreSQLを使ってサインアップできるようになった。
psqlでデータを確認できる。
サインアップ時のフォームは User.scala で
override def signupFields: List[FieldPointerType] = List(firstName, lastName, email, password) override def editFields: List[FieldPointerType] = List(firstName, lastName, email)
などを書くと変更できるが、ちょっと自由度に欠ける。
ここまででPostgreSQLの設定は終了したので、あとで独自のアカウントテーブルを作る。
LiftでWebアプリ(0): Scala の Liftフレームワーク で Webアプリケーションを作る
Scalaの断片的な記事は見かけるが、一つのアプリを作ろうとすると迷うことが多い。
Liftになると尚更情報がない。
そこで簡単なメール送信ショッピングカートをサンプルアプリとして作る過程をメモする。
「ScalaやLiftの機能を色々使ってみよう」ではなく、Tipsは分かったから早く実用的なアプリが作りたいという方向で。
PHP的なアバウトさでちゃっちゃか出来たらいいなと。
http://d.hatena.ne.jp/yuroyoro/20080808/1218168451
この辺とか見ながら。
Liftの特徴としては、やはりAjaxやcometのサーバー実装用frameworkとして使うのがいんじゃね?
http://d.hatena.ne.jp/yuroyoro/20090523/1243079118
というのが最近の俺のLiftに対して感じてることです。
正直、snipet + xhtmlでは、wicketのオブジェクト指向コンポーネントモデルに及ばないし。
その分、actor+cometの組み合わせは凶悪です。
Liftの特徴はcometにありっ!!
なんて書いてあるけれども気にせずに。
XHTMLじゃなくても大丈夫になったしね。
初Scalaだし完成するか分からんけれども、随時更新していくつもりで。
LiftでWebアプリ(1): プロジェクト環境の初期設定 - より良い環境を求めて
LiftでWebアプリ(2): Lift を更にデザイナーフレンドリーにする - より良い環境を求めて
LiftでWebアプリ(3): データベースの設定 - より良い環境を求めて
LiftでWebアプリ(1): プロジェクト環境の初期設定
エディタでコードを書き始められるようにするまで。
maven + sbt + JRebel と IntelliJ を使う。
IDEの初期設定は http://d.hatena.ne.jp/n314/20110810/1312942732 この辺。やってることはだいたい同じ。sbt、JRebelはダウンロード済みとしてまとめ直し。
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.example \ -DartifactId=cart \ -Dpackage=com.example.cart \ -Dversion=1.0 cd cart sbt update
IntelliJ のプロジェクト新規作成で Import project from external model から Maven を選んで作成。Automatically download の sources と Documentation にチェックを入れる。
右下のステータスバー(?)の[ ]をクリックしてtype aware highlightingを有効にする。
プロジェクト設定のFacetsでJRebelを追加し、Automatically generate のチェックを外す。test/scala/RunWebAppを右クリックして Run with JRebelを実行。
ツールバーの実行ボタンの横にRunWebAppが出てくるので Edit Configurations をクリックし、 VM parametersに -Drebel.lift_plugin=true を設定する。
プロジェクトを右クリックしてGenerate rebel.xmlを実行。target/classes を target/scala_2.8.1/classes に変更する。
SBT Consoleを実行し、~compileを入力。
再度 Run with JRebel を実行し、ログの最初の行に -Drebel.lift_plugin=true が、JRebel: Directory に 先ほど設定した scala_2.8.1/classes が出力されていることを確認する。
ブラウザで localhost:8080 を開く。初期のメニューとWelcomeメッセージが出ている。
src/main/scala/com/example/cart/snippet/HelloWorld を開き
def howdy = "#time *" #> date.map(_.toString)
となっているところを
def howdy = "#time *" #> date.map(_.toString + "FOO BAR")
などに変更し、ファイルを保存すると SBT Consoleのコンパイルが走ることを確認する。ちなみにIntelliJはファイルを自動保存してくれるらしいが、コンパイルエラーをチェックしたいので明示的に保存するのが吉。エディタでエラーが出てなくても SBT Console でエラーになることがある。逆に SBT Console でコンパイルが通ってもエディタにエラーが出ることはよくある。こっちは問題ないので気にしない。型を細かく書けば消えることもある。
ブラウザをリロードして、今修正した部分が反映していれば基本的な設定は完了。
LiftでWebアプリ(2): Lift を更にデザイナーフレンドリーにする
HTMLを分離するところまで。
LiftはHTMLにロジックが入らないからデザイナーでも分かりやすいかと言うと、そんなわけない。
ヘッダをXHTMLで宣言しておいて中身はHTMLだというソースも見たことがある。
プログラマーが更新するHTMLとデザイナーが更新するHTMLを分けたい。
サーバーの設定が大いに絡んでくるが、これも一つの案として。
以前書いたもののまとめ直し。
サーバーはGlassFish、OSはDebian squeeze。
更新用ユーザーの作成
Scalaとまったく関係ないが一応書いとく。
adduser app-cart aptitude install vsftpd # chrootを有効にする # /etc/vsftpd.conf を編集 local_enable=YES chroot_local_user=YES chroot_list_enable=YES chroot_list_file=/etc/ftpusers /etc/init.d/vsftpd reload
サーバーや設定はお好みで。
アプリケーションの設定
Boot.scala に
LiftRules.templateCache = Full(NoCache)
を追加してHTMLキャッシュを使わないようにする。
Run Modeがdevelopmentの場合はHTMLはキャッシュされないので、動作確認は後で。
DoctypeがXHTMLまたはHTML5のどちらかっていうのが厳しい場合はHTML4のDoctype出力を書く。
val docType4 = """<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">""" val props4 = (r: Req) => new Html5Properties(r.userAgent).setDocType(() => Full(docType4)) .setHtmlOutputHeader(() => Full(docType4 + "\n")) : HtmlProperties LiftRules.htmlProperties.default.set(props4)
ここではハードコーディングしているが、汎用的にするならdefault.htmlの一行目から取ってくるなどする。
次にHTMLのディレクトリを分ける。
cd src/main/webapp/ mkdir public mv images index.html static public/
全てpublicディレクトリへ。
このままだとURLにもpublicが必要になるのでUrlRewriteFilterを使ってURLを書き換え。
pom.xml
<dependency> <groupId>org.tuckey</groupId> <artifactId>urlrewritefilter</artifactId> <version>3.2.0</version> </dependency>
import changesと右上に出るのでクリックする。sbt updateも実行する。
WEB-INF/urlrewrite.xml
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE urlrewrite PUBLIC "-//tuckey.org//DTD UrlRewrite 3.0//EN" "http://tuckey.org/res/dtds/urlrewrite3.0.dtd"> <urlrewrite> <rule match-type="regex"> <from>^(/public/|/classpath/|/ajax_request/)(.*)$</from> <to last="true">$1$2</to> </rule> <rule match-type="regex"> <from>^(.*)$</from> <to last="true">/public$1</to> </rule> </urlrewrite>
全てをpublicディレクトリへ転送。publicディレクトリ、その他Liftが自動で生成するパスは省く。
web.xml を変更。
<web-app> <filter> <filter-name>UrlRewriteFilter</filter-name> <filter-class>org.tuckey.web.filters.urlrewrite.UrlRewriteFilter</filter-class> </filter> <filter> <filter-name>LiftFilter</filter-name> <display-name>Lift Filter</display-name> <description>The Filter that intercepts lift calls</description> <filter-class>net.liftweb.http.LiftFilter</filter-class> </filter> <filter-mapping> <filter-name>UrlRewriteFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <filter-mapping> <filter-name>LiftFilter</filter-name> <url-pattern>/*</url-pattern> <dispatcher>REQUEST</dispatcher> <dispatcher>FORWARD</dispatcher> </filter-mapping>
public以下のテンプレートを読み込むためにSiteMapも変更する必要が出てきたので Boot.scala を編集する。
def sitemap() = SiteMap( Menu(Loc("TopIndex", Link(List("public", "index"), true, "/index.html"), "Home")) , Menu(Loc("Static", Link(List("public", "static"), true, "/static/index.html"), "Static Content")))
List("public", "index")は実際のパス、"/index.html"はリンク。.html は無くても動くのでお好みで。
この状態だと /user_mgt/login などが動かないが、これは後で考える。
たぶん元々用意されているユーザー操作系ページは使わないと思う。
デザイナーがロールオーバー込みのアイコン画像などを作るだろうからSiteMapからメニュー生成も基本使わない。よってstaticフラグをtrueにして、htmlがあればメニューに関係なくそれを表示するようにしている。
GlassFishの設定と動作確認
管理画面で「設定」→「server-config(利用している設定)」→「JVM 設定」の「JVM オプション」タブの項目に -Drun.mode=production を追加して再起動する。
シンボリックリンクが動くように、WEB-INF/sun-web.xml を作成する。
<?xml version="1.0" encoding="UTF-8"?> <sun-web-app> <property name="allowLinking" value="true"/> </sun-web-app>
sbt package したものを管理画面からデプロイする。
type "dispatcher" must be declared
のエラーが出た場合はweb.xmlのdoctypeを削除する。
「設定」→「server-config(利用している設定)」→「仮想サーバー」→「(利用しているサーバー)」のデフォルトWebモジュールを選択する。
cd glassfish/domains/domain1/applications/cart_2.8.1-1.0/ mv public ~app-cart/ chown -R app-cart:app-cart ~app-cart/public/ sudo -u appserv ln -s ~app-cart/public . # appserv は GlassFish実行ユーザー
ここまで出来たらブラウザで確認する。
su app-cart を実行し、~/public以下のHTMLを変更して更新が反映されるか確認する。
これでFTPからapp-cartでログインしてファイルを更新できるようになった。
LiftでビューのHTMLを動的に更新するためのメモ
http://www.slideshare.net/fungoing/webscalaliftui-first
ここの20ページ目に
※Viewの更新は再コンパイル不要
LiftはViewからロジックが完全に切り離されて いるので、Viewの更新はサーバを起動したま ま行うことができます。 UIは文言やレイアウトなど細かい調整が入るこ とが多いのでこれは大きなメリットです。Viewの配置作業もBoot.scalaが変わる時以外 は起動したまま作業ができます。
http://www.slideshare.net/fungoing/webscalaliftui-first
こう書いてあるが、これって開発時だけ?完成したあとにちょっとした文章編集だとやっぱりwarからアップし直し?と思ったので試した。
まずはLiftのRun Mode。
Development
- Snippet errors are shown in the browser
- Comments are retained in XHTML (comments are stripped out in other run modes)
- Non-minified version of JavaScript libraries (e.g., JQuery) are served
Production deployments
Production, Staging and Pilot are all considered to be production-level deployments, and as such have similar differences:http://www.assembla.com/wiki/show/liftweb/Run_Modes
- Templates are cached in memory
productionモードだとHTMLをキャッシュするらしい。
まずはその動作を試す。
GlassFishの場合は「設定」→「server-config(利用している設定)」→「JVM 設定」の「JVM オプション」タブの項目に -Drun.mode=production を追加して再起動する。
モードの確認は Props.mode.toString を表示すれば簡単。
Productionを確認してからglassfish/domains/domain1/applications/myapp 以下のHTMLファイルを編集してブラウザをリロード。キャッシュされているので予想通り更新されない。
キャッシュ設定の流れ、というかBootの流れは
http://d.hatena.ne.jp/katzchang/20110607/p1
ここが詳しい。
private def postBoot { try { ResourceBundle getBundle (LiftRules.liftCoreResourceName) if (Props.productionMode && LiftRules.templateCache.isEmpty) { // Since we're in productin mode and user did not explicitely set any template caching, we're setting it LiftRules.templateCache = Full(InMemoryCache(500)) } } catch { case _ => logger.error("LiftWeb core resource bundle for locale " + Locale.getDefault() + ", was not found ! ") } finally { LiftRules.bootFinished() } }http://d.hatena.ne.jp/katzchang/20110607/p1
こういうことらしい。
さっそくソースを探索。
InMemoryCacheはTemplateCache.scalaにあった。同じソースにNoCacheというオブジェクトを発見。
ということは…
LiftRules.templateCache = Full(NoCache)
Boot.scala にこれを書くとキャッシュを無効にできる。
試しにHTMLファイルを編集してブラウザをリロードすると…更新された!
Boxとか知らないレベルだったのでソース探索に時間がかかったがscalaに慣れてる人なら一瞬で出来たかもしれないくらいの分かりやすさ。
あとはアップロードの仕組みだね。
未だに分からないんだけど、Javaでの開発ってFTPしか使えないデザイナーは直接的な更新は何も出来ない?
少しの文面変更でHTMLを渡してもらってこっちで更新っていうのはやってられないのでまずはシンボリックリンクを試す。
GlassFishの設定で
<sun-web-app> <property name="allowLinking" value="true"/> </sun-web-app>http://wikis.sun.com/display/GlassFish/FaqActivateSymbolicLinksJa
これを書くとシンボリックリンクが有効になるらしい。
これで動作中のアプリケーションのディレクトリから適当なユーザーのホームディレクトリの中にシンボリックリンクを張る。
ln -s /home/ftpuser/lifthtml /opt/glassfish3/glassfish/domains/domain1/applications/liftdemo/static
例えばこんな感じ。
これで、サーバー再読込の必要なしに任意ユーザーのディレクトリのファイルを更新することでHTMLの変更が可能になった。
アプリを再デプロイしたらリンクが消えると思ってたんだけど消えなかった。
それどころか、なんか色々維持したまま再デプロイできる? http://weblogs.java.net/blog/swchan2/archive/2011/03/09/keepstate-keepsessions-keep-state-save-sessions-enabled-glassfish-31
これならPHPのように本番環境を度々更新する方針でも大丈夫なんじゃないか?という気がしてきた。
※ 8/16 追記
再デプロイじゃなくて配備解除してもリンクが残ってる! 他のファイルは綺麗に消えているのにシンボリックリンクだけ残ってる。これって正式な動作なんだろうか?個人的にはこの動きはありがたいが、今のバージョンで偶然そうなってるだけだと困るな…。
※ 8/17 追記
アプリのディレクトリに .glassfishStaleFiles というファイルができていて、ここにシンボリックリンクのファイルが書かれている。検索してもヒット数ゼロという状態なのでまたあとで。
Emacs で Scala + Lift の環境を作る
ファイルを保存したらsbtが自動でコンパイルしてJRebelがclassファイルの自動リロードするなら、もうIDEである必要は無いんじゃね?ってことで。
mvn でプロジェクトを作って sbt updateするところまでは前回 *1と同じ。
scala-modeのためにソースをダウンロードする。
wget http://www.scala-lang.org/downloads/distrib/files/scala-2.9.0.1.tgz
scalaはDebian sidのパッケージを入れたので、それに対応するやつ。
Debianのscalaパッケージは依存関係がほぼ無いのでsidでも問題ない。ってことはパッケージ版を使わなくてもいいんだけど。
tar xvzf scala-2.9.0.1.tgz cp -a scala-2.9.0.1/misc/scala-tool-support/emacs ~/.emacs.d/plugins/scala-mode cp scala-2.9.0.1/misc/scala-tool-support/emacs/contrib/dot-ctags ~/.ctags aptitude install exuberant-ctags
http://d.hatena.ne.jp/tototoshi/20100925/1285420294
http://d.hatena.ne.jp/tototoshi/20100927/1285595939
ここを参考に。
wget https://github.com/downloads/aemoncannon/ensime/ensime_2.9.0-1-0.6.1.tar.gz tar xvzf ensime_2.9.0-1-0.6.1.tar.gz cp -a ensime_2.9.0-1-0.6.1 ~/.emacs.d/plugins/
.emacsを編集する。
;; scala (add-to-list 'load-path "~/.emacs.d/plugins/scala-mode") (require 'scala-mode-auto) (add-to-list 'auto-mode-alist '("\\.scala$" . scala-mode)) ;; (require 'scala-mode-feature-electric) ;; (add-hook 'scala-mode-hook ;; (lambda () ;; (scala-electric-mode))) (add-to-list 'load-path "~/.emacs.d/plugins/ensime_2.9.0-1-0.6.1/elisp/") (require 'ensime) (add-hook 'scala-mode-hook 'ensime-scala-mode-hook) ;;(define-key ensime-mode-map (kbd "C-.") 'ensime-edit-definition) (defadvice scala-block-indentation (around improve-indentation-after-brace activate) (if (eq (char-before) ?\{) (setq ad-return-value (+ (current-indentation) scala-mode-indent:step)) ad-do-it)) (defun scala-newline-and-indent () (interactive) (delete-horizontal-space) (let ((last-command nil)) (newline-and-indent)) (when (scala-in-multi-line-comment-p) (insert "* "))) (add-hook 'scala-mode-hook (lambda () (define-key scala-mode-map (kbd "RET") 'scala-newline-and-indent)))
インデントは http://www.callcc.net/diary/20101106.html ここに書いてあるものを使った。
scala-mode-feature-electric は何となく指に合わないのでコメントアウトしている。
cat ~/bin/sbt #!/bin/bash java -Xmx512M -jar `dirname $0`/sbt-launch.jar "$@" cat ~/bin/sbtj #!/bin/bash java -noverify -javaagent:/home/user/lib/jrebel/jrebel.jar \ -Drebel.lift_plugin=true -XX:+CMSClassUnloadingEnabled \ -XX:MaxPermSize=512m -Xmx512M -Xss2M -jar `dirname $0`/sbt-launch.jar \ "$@" cd project_dir sbt "~compile"
http://batsov.com/Java/Scala/Programming/2011/04/26/jrebel-with-scala.html
ここの通りにjrebel用のコマンドを作った。
別窓で
sbtj test-run
で RunWebApp を選択すると動的にリロードするサーバーが立ち上がる。
emacsを起動して M-x ensime-config-gen 後に M-x ensime する。起動が正直重い。etagsのみでいいかもしれない。
逆にensimeを使うなら、etagsは利用しないので exuberant-ctags や .ctags は不要。
ensimeをやめてもflymake が使えそう。 http://d.hatena.ne.jp/kiris60/20091004/1254586627
でもsbtのコンパイルが動きっぱなしならそれも不要かもしれない。
http://www.scala-lang.org/node/5940
この辺に emacs からsbtコンパイルの連携があるが…もう一歩という感じ。
SBTのコマンド
http://code.google.com/p/simple-build-tool/wiki/RunningSbt
githubよりこっちの古い方が分かりやすい。