漏水工事した
9月、入院エンジョイしてたら漏水が発覚して引っ張りだされました。
自宅が漏水してるらしく一時外出…
— Toshiyuki Takahashi (@tototoshi) September 9, 2014
うち(3階)からの漏水で2階の部屋の天井が落ち、1階の駐車場まで雨漏りしていました。
自分だけでなく家まで内視鏡検査することになってしまった…
— Toshiyuki Takahashi (@tototoshi) September 9, 2014
内視鏡検査の結果、引っ越し前のリフォームのときに床下の配管の締めが甘く、それからずっと漏水していたらしいことがわかりました。どうしてこんなになるまで...早く言おうよ2階の人... (リフォーム業者の不手際なので私は無実です)
うちも少しやられてしまったので、修理しました。
以下、修理の手順です。
穴を開けて床下のパイプを直します。 どこがおかしいのかわからないので、とりあえず穴を開けます。 当たりが出れば直せます。
良くみたら壁が床から水を吸い上げてカビてきてたのではがしました。
手頃な穴を開けて、一週間くらい空気を送りこんで床下を乾かします。 乾かしたあとで消毒もします。
工事の途中でインターネットの線を破壊されちゃって辛かった。
穴をふさぎ、壁を貼りました。
壁紙をはりました。
以上です。
だいたい3ヶ月くらいかかります。みなさんも是非漏水してみてくださいね!
Play 2.4 の Module の作り方と Plugin からの移行について
Play 2.4 では今までの Plugin の仕組みが deprecated となり、 新たに Module という仕組みが導入されています。
Module はこれまた新たに導入された Runtime Dependency Injection の上に乗っかっています。 Play では Guice をデフォルトの DI 実装として利用しますが、 Guice 以外の実装を利用するユーザーもいるだろうということで、 ライブラリとして提供するモジュールについてはそこだけ Guice ではなく、Play 独自の Module という仕組みに沿った形で実装します。
アプリケーションの起動時と終了時にメッセージを表示する Play Module を実装してみましょう。
まずは処理の本体です。
package example import play.api._ import javax.inject._ import play.api.inject._ import scala.concurrent._ import scala.concurrent.ExecutionContext.Implicits.global @Singleton class Hello @Inject() (lifecycle: ApplicationLifecycle) { lifecycle.addStopHook(() => Future.successful { println("Goodbye") }) println("Hello") }
今までの Plugin インタフェースのメソッドを実装する形で実装していた、 Play アプリケーションの起動時の処理は、シングルトンクラスの初期化処理の中で行います。 終了時の処理は injection された lifecycle オブジェクトの addStopHook メソッドを利用して登録します。
これを Play Module として読み込むには
play.api.inject.Module
を継承して、binding メソッドを実装します。
package example import play.api._ import javax.inject._ import play.api.inject._ import scala.concurrent._ import scala.concurrent.ExecutionContext.Implicits.global class HelloModule extends Module { def bindings(environment: Environment, configuration: Configuration) = { Seq( bind[Hello].toSelf.eagerly ) } }
bind の仕方を DSL で記述します。
Play アプリケーション起動時の処理を行うためは、起動時に bind する必要があるので、
.eagerly
を使用します。
作った Module を利用するときは application.conf でその Module を有効にします。
play.modules.enabled += "example.HelloModule"
シングルトンオブジェクトなので toSelf
で自分自身に bind してしまいましたが、
より一般的にはインタフェースを作ってそれに bind します。
package example import play.api._ import javax.inject._ import play.api.inject._ import scala.concurrent._ import scala.concurrent.ExecutionContext.Implicits.global class HelloModule extends Module { def bindings(environment: Environment, configuration: Configuration) = { Seq( bind[HelloInterface].to[Hello].eagerly ) } } @Singleton class Hello @Inject() (lifecycle: ApplicationLifecycle) extends HelloInterface { lifecycle.addStopHook(() => Future.successful { println("Goodbye") }) println("Hello") } trait HelloInterface
さらに、インスタンスの生成が複雑な場合は Provider を利用することもありそうです。
package example import play.api._ import javax.inject._ import play.api.inject._ import scala.concurrent._ import scala.concurrent.ExecutionContext.Implicits.global class HelloModule extends Module { def bindings(environment: Environment, configuration: Configuration) = { Seq( bind[HelloInterface].toProvider[HelloProvider].eagerly ) } } @Singleton class HelloProvider @Inject() (lifecycle: ApplicationLifecycle) extends Provider[HelloInterface] { lazy val get = new Hello(lifecycle) } class Hello (lifecycle: ApplicationLifecycle) extends HelloInterface { lifecycle.addStopHook(() => Future.successful { println("Goodbye") }) println("Hello") } trait HelloInterface
Java っぽさが増したので、特に Java の DI になじみのない LL からのユーザーは戸惑うかもしれません。 Runtime Dependency Injection の導入経緯やテストへの影響などは playframework-dev のメーリングリストを読むと面白いかと思います。
sbt でファイル変更をフックしてコンソールをクリアしつつコンパイルする
Scala Advent Calendar 2014 の 12 日目です。
sbt で ~compile
でファイル変更をフックしてコンパイルは皆さんよくやってると思いますが、
~ ;eval "\u001B[2J\u001B[0\u003B0H" ;compile
とにするとコンソールをクリアしつつコンパイル続行するのでちょっといいかんじになります。
~/.sbtrc に
alias cc = ~ ;eval "\u001B[2J\u001B[0\u003B0H" ;compile
って書けば cc
で使えるようになります。
Heroku で JDK のバージョンを指定する
Heroku でサポートされている JDK は 1.6, 1.7, 1.8 です。 今ではデフォルトは 1.8 ですが、古いアプリではどうやらそのまま 1.6 が使われているようです。
JDK のバージョンを指定したいときには system.properties というファイルを使います。
java.runtime.version=1.8
このファイルをコミットし、PATH を設定します。
APP_PATH=`heroku config:get PATH` heroku config:set PATH=/app/.jdk/bin:$APP_PATH
で、あとはこれを git push heroku master するだけで、JDK のバージョンアップができます。
% git push heroku master Fetching repository, done. Counting objects: 4, done. Delta compression using up to 4 threads. Compressing objects: 100% (2/2), done. Writing objects: 100% (3/3), 324 bytes | 0 bytes/s, done. Total 3 (delta 1), reused 0 (delta 0) -----> Play 2.x - Scala app detected -----> Installing OpenJDK 1.8...done
参考: Updating Existing Java Apps to Use Java 7 | Heroku Dev Center
scala-csv 1.1.0 をリリースしました
https://github.com/tototoshi/scala-csv
scala-csv 1.0.0 ではパーサーをパーサーコンビネータで実装していましたが、巨大なファイルをパースしたときにパフォーマンスで問題があったので、パーサーコンビネータをやめて var をふんだんに使った実装に書き直しました。
Play で Scalate を使う
play-scalate っていう名前のプラグインはいろんな人が書き散らかしてて github 検索するとボロボロ出てくるんですが、ついうっかり仲間に加わってしまいました。
https://github.com/tototoshi/play-scalate
build.sbt に設定すれば
libraryDependencies ++= Seq( "com.github.tototoshi" %% "play-scalate" % "0.1.0-SNAPSHOT", "org.scalatra.scalate" %% "scalate-core" % "1.7.0", "org.scala-lang" % "scala-compiler" % scalaVersion.value ) unmanagedResourceDirectories in Compile += baseDirectory.value / "app" / "views"
こんな感じで使えます。
package controllers import play.api._ import play.api.mvc._ import com.github.tototoshi.play2.scalate._ object Application extends Controller { def index = Action { implicit request => Ok(Scalate.render("index.jade", Map("message" -> "hello"))) } }
ライブラリという形にして、SNAPSHOT を publish はしたけれど、そんな大したものではないのでいじりたければプロジェクト内にコピペして好きにいじってもらえばいいかなと思います。
Play で Scalate を使うときに気をつけることは、Dev モードで普通に動いてるのに Prod モードだと動かん!ってことにならないように、テンプレートファイルをファイルとしてではなくリソースとして読み込めってこどでしょうか。つまり、Play.api.current.getFile
じゃなくて Play.api.current.resourceAsStream
とか使おうってことです。Scalate はファイルを読み込むのを想定しているので、リソースを読み込むようにするのに一手間必要です。
Play ではリソースファイルは conf/
に置くことになってる(ちょっと気持ちわるい)ために
app/views
のファイルはリソースとして扱われないので、デプロイしたときにリソースが見つからん!というエラーも起きます。これを防ぐためにはテンプレートファイルは、app/views
ではなく conf/views
に置くか、conf
以下から app/views
にシンボリックリンクを...っていやまさか、
build.sbt で
unmanagedResourceDirectories in Compile += baseDirectory.value / "app" / "views"
とすれば良いです。
RFC 的には text/* のデフォルトエンコーディングは ISO-8859-1
以下のような、日本語のデータを text/csv という Content-Type で返す Play アプリケーションで、
GET /test controllers.Application.test
package controllers import play.api._ import play.api.mvc._ import play.api.libs.iteratee.Enumerator import scala.concurrent.ExecutionContext.Implicits.global object Application extends Controller { def test = Action { implicit request => { Result( header = ResponseHeader(200, Map(CONTENT_TYPE -> "text/csv", CONTENT_DISPOSITION -> ("attachment; filename=test.csv"))), body = Enumerator.outputStream { out => try { // CSVデータを出力 out.write("テスト".getBytes("UTF-8")) } finally { out.close() } } ) } } }
次のようなテストを書くとエラーになるんだけどなんで?と聞かれました。
import org.scalatest._ import play.api.test._ import play.api.test.Helpers._ import play.api.libs.ws.WS import play.api.Play.current class ApplicationSpec extends FlatSpec with Matchers { it should "work" in { running(TestServer(3333, FakeApplication())) { val testUrl = "http://localhost:3333/test" val body = await(WS.url(testUrl).get).body body should be("テスト") } } }
[info] Compiling 1 Scala source to /Users/toshi/work/scala/play-hello/target/scala-2.11/test-classes... [info] ApplicationSpec: [info] - should works *** FAILED *** [info] "[ãã¹ã]" was not equal to "[テスト]" (ApplicationSpec.scala:13) [info] ScalaTest [info] Run completed in 3 seconds, 921 milliseconds. [info] Total number of tests run: 1 [info] Suites: completed 1, aborted 0 [info] Tests: succeeded 0, failed 1, canceled 0, ignored 0, pending 0 [info] *** 1 TEST FAILED *** [error] Failed: Total 1, Failed 1, Errors 0, Passed 0 [error] Failed tests: [error] ApplicationSpec [error] (test:test) sbt.TestsFailedException: Tests unsuccessful [error] Total time: 7 s, completed 2014/10/18 23:44:56
見ての通り、エンコードがおかしくなってアサーションが通りません。
これ不思議だなあと思って調べたんですが、Play の WS(クライアント側) のソースコードの該当部分を見てみるとコメントがついていました。
lazy val body: String = { // RFC-2616#3.7.1 states that any text/* mime type should default to ISO-8859-1 charset if not // explicitly set, while Plays default encoding is UTF-8. So, use UTF-8 if charset is not explicitly // set and content type is not text/*, otherwise default to ISO-8859-1 val contentType = Option(ahcResponse.getContentType).getOrElse("application/octet-stream") val charset = Option(AsyncHttpProviderUtils.parseCharset(contentType)).getOrElse { if (contentType.startsWith("text/")) AsyncHttpProviderUtils.DEFAULT_CHARSET else "utf-8" } ahcResponse.getResponseBody(charset) }
Play のデフォルトのエンコーディングは UTF-8 なのですが、 RFC-2616 的には Content-Type が text/* のデフォルトエンコーディングは ISO-8859-1 なので Content-Type が text/* なデータはあえて ISO-8859-1 にしてあります。
つまりサーバーの実装のほうが、
CONTENT_TYPE -> "text/csv; charset=utf-8;"
とちゃんと UTF-8 と書けって話でした。
なるほどなとは思いましたが、サーバー側のほうは Play のデフォルトである UTF-8 で返すのにクライアントだけ ISO-8859-1 で受け取るってのはハマるよなあと思いました。ただサーバーのほうでもエンコーディングが指定されていなければ ISO-8859-1 に変えるとするのは今の Play の実装では無理があるし、そんなに望まれている動作でもないかもなとも思います。微妙ですね。
なので、とりあえず RFC-2616 的には Content-Type が text/* のデフォルトエンコーディングは ISO-8859-1 という事実だけ覚えておくことにしました。
The "charset" parameter is used with some media types to define the character set (section 3.4) of the data. When no explicit charset parameter is provided by the sender, media subtypes of the "text" type are defined to have a default charset value of "ISO-8859-1" when received via HTTP. Data in character sets other than "ISO-8859-1" or its subsets MUST be labeled with an appropriate charset value. See section 3.4.1 for compatibility problems.