タグ別アーカイブ: Play

Playframework2.6でファイルアップロードのユニットテスト

Multipartでファイルアップロードするapiのテストを行いたかったんだけど、めちゃくちゃハマったというか、わからなかったのでここにメモする!

ポイント1 「アップロードするファイルをリソースから取得する」

まず、テストケースで使用するリソース(今回はアップロードするファイル)はどこに保存すればいいのか分からんね!

そんな時は次のコマンドを叩けばわかるぞ!

sbt "show test:resourceDirectory" 
> [info] Test / resourceDirectory
> [info] /Users/honyarara/project/test/resources

そしてこのファイルを取得するにはこうだ!

/Users/honyarara/project/test/resources/testfile.jpg を取得する時は。

val fileUrl = getClass.getResource("/testfile.jpg")

先頭のスラッシュが無いとnullになっちゃうので注意!

 

ポイント2 「multipart/form-dataのFakeRequestを作成する」

WEBに転がってる情報が古くて正解になかなかたどり着けなかったよ。

最初に リソースから取得したファイルをTemporaryFileに変換する。
既に存在するファイルのTemporaryFileって何言ってるんだ?と思うかもしれないが、そういうもんだ。

val tempFile = SingletonTemporaryFileCreator.create(Paths.get(fileUrl.toURI))

で、このtempFileを使ってFilePartを作成する

val filepart = FilePart[TemporaryFile]("imageFile","testfile.jpg",Some("image/jpeg"),tempFile)

次にファイルと同時に送信するフォーム情報を作成する

val data:Map[String,Seq[String]] = Map(
  "title" -> Seq("パイスラッシュ女子")
)

後は、ここまでの情報を組み立てればOK

val formData = MultipartFormData[TemporaryFile](
	dataParts = data,files = Seq(filepart),badParts = Seq())

val request = FakeRequest(POST,"/upload")
	.withMultipartFormDataBody(formData);	

これで “multipart/form-data” なFakeRequestができる。

 

ポイント3 「実行する!」

実行時にはroute経由でやらないと上手く動かなかった。

val result = route(app, request).get
status(result) mustBe OK

 

まとめると

import java.nio.file.Paths
import play.api.libs.Files.SingletonTemporaryFileCreator
import play.api.libs.Files.TemporaryFile
import play.api.mvc.MultipartFormData
import play.api.mvc.MultipartFormData.FilePart

class UploadControllerSpec extends PlaySpec with GuiceOneAppPerTest with Injecting
{
  "画像をuploadするとOkを返すよ" in {

  	// 送信するフォームデータ
	val data:Map[String,Seq[String]] = Map(
	  "title" -> Seq("パイスラッシュ女子大生")
	)

	// リソースからファイルを取得してTemporaryFileを作成する
  	val fileUrl = getClass.getResource("/testfile.jpg")
  	val tempFile = SingletonTemporaryFileCreator.create(Paths.get(fileUrl.toURI))

  	// ファイルパートを作成する
  	val filepart = FilePart[TemporaryFile](
  			"imageFile","testfile.jpg",Some("image/jpeg"),tempFile)

  	// 組み合わせてMultipartFormDataを作成
  	val formData = MultipartFormData[TemporaryFile](
  		dataParts = data,files = Seq(filepart),badParts = Seq())

  	// FakeRequestを作成
  	val request = FakeRequest(POST,uploadEndpointUrl)
  		.withMultipartFormDataBody(formData)

  	// 実行
  	val result = route(app, request).get
  	status(result) mustBe OK
  }
}

[Play] dist時に独自のリソースフォルダを加える

Play frameworkで作成したアプリケーションを公開する際に、”dist”コマンドを実行してデプロイ用のzipを作成すると思いますが、そのまま処理すると独自に追加したリソース用のディレクトリが含まれてくれません。

そういった場合にはbuild.sbtに追記します。

例えば プロジェクトフォルダ直下に”templates”というディレクトリを作成していたとしたら、
build.sbtに次のコードを追記します。

// templatesフォルダとその内容をdist対象に追加
mappings in Universal ++= {
  val templateDirectory = baseDirectory(_ / "templates").value
  val templateDirectoryLen = templateDirectory.getCanonicalPath.length
  (templateDirectory ** "*").get.map { f: File =>
    f -> ("templates/" + f.getCanonicalPath.substring(templateDirectoryLen))
  }
}

[Play] APIドキュメントの生成を抑制する

play stage とか play dist とかしたときに何故がAPIドキュメントとかが生成されちゃって、
ただでさえ遅いscalaのコンパイルがさらに遅くなってイライラしているそんな貴方、こちらをお試しあれ。

build.sbt(プロジェクトのルートにあると思う)の playScalaSettings にちょっと手を加えれば解決できます。

こんな感じ

name := "playapplication"

version := "1.0-SNAPSHOT"

libraryDependencies ++= Seq(
  jdbc,
  anorm,
  cache
)     

//play.Project.playScalaSettings
// ↓ こんな感じに書き換える (ver2.2以下)
play.Project.playScalaSettings ++ Seq(doc in Compile <<= target.map(_ / "none"))

// ↓ (追記) 2.3以降だと下記の行を加えるとOK
doc in Compile <<= target.map(_ / "none")

こうするとドキュメントの生成を止めてくれますよ。
※playframework 2.2.2で動作確認

[Play] Playframeworkのセッションでハマる!

表題の時点で分かる人は大体「あー、あれだな!」って気づかれてしまうネタですけど、
拡散の意味を含めて投稿しておきます。

セッションに値を設定する時にwithSessionを使いますよね。
んで、新規セッションとする場合にはwithNewSessionを使う。

コードだとこんな感じ

Ok(・・・).withSession("authKey" -> "eyewhale")
Ok(・・・).withNewSession

そもそもこのメソッド名が混乱の元なんですよね。
一方がNewSessionならもう一方は既存のセッションを引き継ぐって思ってしまうんですが、これが間違い!
withSessionは指定された内容で既存セッションを丸ごと上書きします!!(重要)
(with・・・なのでそうあるべきなのは分かりますが)

なのでセッションに追加したい場合には

Ok(・・・).withSession(session + ("authKey" -> "eyewhale"))

のように記述する必要があります。
複数の値をセットする場合にはこんな感じ。

Ok(・・・).withSession(
      session + ("authKey" -> "eyewhale") + ("authorName" -> "tateo"))

逆にセッションから値を消す場合にはマイナスすれば消えてくれます。

Ok(・・・).withSession(session - "authKey")

という事で、
気をつけなはれや!

[Play] ブラウザキャッシュを無効化

Play Framework2でブラウザキャッシュを無効化するサンプルです。
たぶんもっとscalaっぽいやり方があるはず。

下記みたいなコードを
helpers / NoCache.scala として保存

package helpers

import play.api.mvc._
import play.api.http.HeaderNames._

object NoCache {

  def setHeaders(result:SimpleResult): SimpleResult = {
    result.withHeaders(
        PRAGMA -> "no-cache",
        CACHE_CONTROL -> "no-cache",
        EXPIRES -> "Thu, 01 Jan 1970 00:00:00 GMT")    
  }

  implicit class NoCacheSimpleResult(val self: SimpleResult) extends AnyVal {
    def NoCache: SimpleResult = helpers.NoCache.setHeaders(self)
  }
  
}

使い方は各コントローラーで下のように書くとヘッダーへno-cacheを設定してくれます。


import helpers.NoCache._

def hogehoge(code: String) = Action.async { implicit request =>
    scala.concurrent.Future {
         
          // 何かしら処理 ・・・
    
          Ok(・・・).NoCache    
    }
  } 

[Play] アップロードファイルの制限を掛ける方法

PlayFramework2.xでのファイルアップロード時にサイズ制限を掛ける方法です。

どうやら maxLength という指定を行う事で対応できるようです。
以下サンプル

def upload = Action.async(parse.maxLength(1024 * 200, parse.multipartFormData)){ 
  implicit request =&gt;   
  scala.concurrent.Future {      
    request.body match {
      case Left(err) =&gt; EntityTooLarge(&quot;upload image too big&quot;)
      case Right(body) =&gt; {

         val opFile = body.file(&quot;image&quot;)
         opFile match {
            case None =&gt; Forbidden
            case Some(file) =&gt;{
                val tempFile: TemporaryFile = file.ref
                     
                // ごにょごにょ 
                // val f:java.io.File = tempFile.file
                     
                tempFile.clean
            }
          }
    
      }
    }
  }
}

[Play] AnormでダイナミックSQL

Anorm面倒ですね。
静的なパラメータクエリは簡単にできますが、動的にwhere句・パラメータを生成しなければならない場合もあるわけで。。

SQLのwhereについては単なる文字列なのでどうとでもなりますが、問題はon{・・}に記述するパラメータです。
これにはすっごく悩みましたが下記の方法にて動的にwhereの生成とパラメータの設定を行う事が可能です。

val datamap = { //(省略)マッピング定義 }

def selectBooks(offset: Int, pageSize: Int, orderBy: String
    , filterTitle: Option[String] = None,filterAuthor:Option[Int] = None):List[Book] = {
  DB.withTransaction { implicit connection => 
  
    // 条件をtupleで定義する(項目ID,条件演算子,値)
	// 無視する条件は値にNoneを設定
    val conditions = Seq(
      ("title", "like",filterTitle),
      ("author_id", "=",filterAuthor)
    )
	
    // Someな値を抽出してwhere句を生成
    val filterString = conditions.filter(_._3.isDefined).map(flt => {
      "%s %s {%s}".format(flt._1,flt._2,flt._1)
    }).mkString(" where "," and ","")
    
	// 同じくSomeな値に対してのパラメータリストを作成
    val filterParams = conditions.filter(_._3.isDefined).map(flt => { 
      flt._1 -> toParameterValue(flt._3)
    })       
	
	// 常に設定するパラメータを作成
    val baseParams =  Seq(
      "pageSize" -> toParameterValue(Option(pageSize)),
      "offset" -> toParameterValue(Option(offset))
    )
	// パラメータのリストを結合
    val params = baseParams ++ filterParams

    // queryを実行
    SQL("""
        select * from books
        %s order by %s limit {pageSize} offset {offset}         
        """.format(filterString,orderBy))        
        .on(params: _* )
        .as(datamap *)
  }
}

条件項目をOptionで受け取り、Someな項目のみを条件として設定しています。
もうちょっと綺麗に書きたいのですが、イマイチscala的な書き方が良くわかっていないので。。

[Play]ネストしたFormを使う

playframework2.2でネストしたFormをListで使うサンプルがなかなか見つからなかったのでメモしておきます。

最初にエンティティの定義
今回は簡単にcaseクラスで下記の用に定義します。

case class User(
  userId: String,
  codename: String,  
  skills: List[Skill])
 
case class Skill(skillname:String,level:Int)
 

UserにネストしてSkillを持ちますよ。

controllerでは下記のようにFormのパース定義を記述します。

val userFormDef :Form[User] = Form(
  mapping(
    "userId" -> number,
    "codename" -> text,            
    "skills" ->  list[Skill](
      mapping(
        "skillname" -> text,
        "level" -> number
      )(Skill.apply)(Skill.unapply))                      
  )(User.apply)(User.unapply))

エンティティのネストと同様にFormの定義もネストして記述しています。

これを受けるビューはこんな感じで記述
@(form:Form[User])(implicit request: RequestHeader)
<!DOCTYPE html>
・・省略・・
<h2>UserID: @form.get.userId <h2>
@helper.inputText(form("codename"))<br />

@helper.repeat(form("skills"), min = 1) { skill =>
<div>
  @helper.inputText(skill("skillname"))
  @helper.inputText(skill("level"))
</div>
}

</html>

念のため表示させる時のコードも書いておきます

val user = User(・・・・・)
Ok(views.html.user(userFormDef.fill(user)))

パースさせる時も普段どおりこんな感じでOkす

userFormDef.bindFromRequest.fold(
      formWithErrors => BadRequest,
      form => {
	・・・
      })

以上、参考になれば。