ログ日記

作業ログと日記とメモ

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に定義されている<*>や^/よりかは見やすいかと。


ブラウザでアクセスした時の処理の流れは以下のようになっている。

  1. 初回アクセス
  2. dispatch が呼び出される("render" 以外は渡ってこない?以下renderとする)
  3. renderで設定したget系関数が呼び出される
  4. ブラウザ描写
  1. submitボタンをクリックする
  2. renderで設定したset系関数が呼び出される
  3. onSubmit(SHtml.onSubmitUnitで登録した関数)が呼び出される
  4. renderが呼び出される
  5. get系関数が呼び出される
  6. ブラウザ描写


これをふまえて、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でも長時間起動していたら同じ現象になるのかもしれない。