Skinny FrameworkのTIPSまとめ (1) #skinnyjp
Skinny Framework(以降skinnyと表記)を使っていく中で、こういう時どうする?みたいなのがいくつか蓄積されたので、まとめてみました。
TIPSと言って良いものかどうかはさておき。
※この記事は、Skinny Framework 1.1.0
時点のものです。また、Skinny Framework 1.0.18
でも同様に使えます。
目次
- 画像ファイルをアプリケーションに追加する
- カスタムcontrollerのMIMEマッピングをデフォルトに合わせる
- Javaファイルをアプリケーションに追加する
- ページごとのタイトルをlayoutテンプレートの中で切り替える
- ファイルアップロード機能のサンプル
- エラーなどのメッセージを表示する
画像ファイルをアプリケーションに追加する
runモードかwarで動かしている場合は、例えば./src/main/webapp/img.png
にファイルを置けば、localhost:8080/img.png
で参照できます。
standaloneの場合は、ちょっとした仕掛けが必要になります。
まず、standaloneでは、standalone jar
ファイルを作るときに、./src/main/webapp/
の下がそのままjar
に格納されるようになっていますので、./src/main/webapp/images/
の下に画像ファイルを配備することにしましょう。
それから、standalone jar
ファイルの中の./src/main/webapp/images/*
にアクセスするためのアクセス用controllerを作ってマウントします。これをマウントすると、runモードかwarで使う場合でもこれが呼び出されてしまいますが、./src/main/webapp/
がクラスパスディレクトリーになっていますので、同様に処理されるようになります。
ちなみに、このcontrollerは、skinny-assets
のAssetsController
を参考にしています。特に、このあたり(GitHub)のコンパイラー以外の部分を見てみてください。
images
の画像ファイルにアクセスするcontroller(Controllers.scala
の一部)- 追記(2014-06-17): ちょっと無駄が多かったので、リファクタリングしました。
// ... // // mountメソッドに追加 images.mount(ctx) // ... // // imagesにアクセスするためのcontrollerオブジェクトを追加 object images extends SkinnyController with Routes { // MIMEマッピング addMimeMapping("image/png", "png") addMimeMapping("image/jpeg", "jpg") addMimeMapping("image/gif", "gif") // ルーティング get("/images/*")(images).as('images) // アクション def images(): Any = { val resOpt: Option[(String, ClassPathResource)] = for { name <- multiParams("splat").headOption resource <- ClassPathResourceLoader.getClassPathResource(s"/images/${name}") } yield (name, resource) resOpt match { case Some((name, resource)) if (isModified(resource.lastModified)) => contentType = name.split('.').lastOption.flatMap(formats.get(_)).getOrElse("application/octet-stream") setLastModified(resource.lastModified) resource.stream case Some(_) => halt(304) case None => pass() } } // HTTPレスポンスヘッダーに最終更新日時を設定 private[this] def setLastModified(lastModified: Long): Unit = { /* dummy */ } // HTTPリクエストヘッダーをチェックしてリソースが更新されたかどうかを判定 private[this] def isModified(resourceLastModified: Long): Boolean = true // dummy } // ... //
setLastModified
とisModified
は、処理が少し煩雑になるので、ここには書いていません。先に挙げたAssetsController
を参考にして実装すると良いと思います。
カスタムcontrollerのMIMEマッピングをデフォルトに合わせる
さまざまな種類のファイル(のダウンロード)を扱うアプリを作るとしたら、ファイルの種類ごとにMIMEの設定が必要になります。ですが、あらゆるファイル種別に対応するMIMEマッピングを設定するのはちょっと手間がかかります。
それならば、controllerを介さないでアクセスするのと同じようにすれば良いのでは? さらに、アプリが直接jetty
に依存してもかまわないのであれば、jetty
の設定を拝借してしまえば良いのでは?
ということで、jetty-http
にあるmime.properties
を読み込んで設定する処理を書いてみました。
※注意: warで使う場合は、jetty-http
の依存関係を明示的に指定する必要があります。
jetty-http
のdependency設定(project/Build.scala
の一部)
// libraryDependencies "org.eclipse.jetty" % "jetty-http" % jettyVersion % "compile",
jetty
のMIME設定をカスタムcontrollerに反映する(Controllers.scala
の一部)- ※注意 - 追記(2014-08-16): 本項の最後に注意を追記しました。
case class MimeType(mime: String, extension: String) val jettyMimeTypes: Iterator[MimeType] = { import java.util.ResourceBundle import collection.JavaConverters._ val rb = ResourceBundle.getBundle("org/eclipse/jetty/http/mime") rb.getKeys.asScala.map(ext => MimeType(rb.getString(ext), ext)) } object customController extends SkinnyController with Routes { jettyMimeTypes.foreach { mime => addMimeMapping(mime.mime, mime.extension) } // ... action ... // ... routing ... }
java.util.ResourceBundle
を使って、org/eclipse/jetty/http/mime.properties
を読み込み、それをMimeType
の配列に変換しておきます。これを、カスタムcontrollerの初期化時に、すべての要素の対してaddMimeMapping
を実行します。
これを実施しておけば、前項の画像ファイルのためのマッピングは不要になります。
※注意 - 追記(2014-08-16): 上記コード例のjettyMimeTypes
はIterator[MimeType]
になっていますので、もしこれを複数のController
で使ってしまうと、上手くいきません。念のため説明すると、Iterator
は一度しかTraverse
(要素を全走査する処理全般)できません(実際に自分でワナに嵌ってしまいました...)。
もし複数で使うならば、jettyMimeTypes
をメソッドdef
にする(毎回実行)か、Array
にする(キャッシュ)か、対策してください。
Javaファイルをアプリケーションに追加する
これは、どちらかというと、SBTのTIPS(というか基本?)になるのかも知れません。
Play2のように、フレームワークのクラス自体はJavaでは書けませんが、Javaで書いたユーティリティークラスなどは、skinnyで使うことができます。
./src/main/java
ディレクトリーを作って、この下にJavaファイルを置けば、SBTが一緒にビルドしてくれます。
./src/main/java/lib/MyUtil.java
package lib; public class MyUtil { public static String foo(String s) { return s.toUpperCase(); } }
MyUtil
をScalaから呼び出す
import lib.MyUtil MyUtil.foo("skinny") // => SKINNY
わざわざ説明する必要はないような気もしますが、すべてをScalaで書かなくても良いなら敷居が低くなるんじゃないかな、と思ったので。
ページごとのタイトルをlayoutテンプレートの中で切り替える
skinny-blank-app
の内容がそうなっているように、HTMLのBODYの外側をlayoutに持つようにした場合、何かしらの方法でタイトル文字列をlayoutに渡す必要があります。
公式では、Scalateの方法になっています。
これだとページを作るたびにattributes("title")
を書かないといけないので、ちょっと面倒かな、と思いました。
そこで、layoutテンプレートとmessages.conf
を組み合わせて、ページに対応するタイトル文字列を切り替えられるような方法を考えてみました。
companies
とmembers
というcontrollerがあるとしましょう。
まず、companies
とmembers
のタイトルをmessages.conf
に書いておきます。
messages.conf
の一部
companies.title="*** Companies ***" members.title="*** Members ***"
layoutテンプレートでは、skinny.Skinny
クラスのインスタンスが渡されるようになっていますので、これを使ってページのパスを特定してみます。Skinny.requestPath
メソッドから、/companies/foo
のような文字列が得られますので、この文字列からcompanies
を取り出して、メッセージのキーとして利用できるようにしてみます。
layouts/default.ssp
の一部
<%@val appId: String = s.requestPath.drop(uri("/").size).takeWhile(_ != '/') %> <%@val title: String = s.i18n.get(appId + ".title").getOrElse(appId) %> <title>${title}</title>
appId
には、Skinny.requestPath
で取得した文字列から、basePathの後ろから次の/
までを取り出したものを設定しています。これによって、companies
やmembers
が得られます。uri("/").size
になっているのは、prefixに対応するためです。
title
は、s.i18n.get
経由でmessages.conf
から取得した文字列を設定します。取得できなかったときは、appId
をそのまま設定しています。
もっと細やかに切り替えたければ、ヘルパークラス(object)を作って、テンプレートからはそれを呼び出すようにすれば良いでしょう。
ファイルアップロード(で実際にファイルを保存する)
公式では、下記リンクにあります。
- skinny-framework/example/src/main/scala/controller/FileUploadController.scala at 1.1.x · skinny-framework/skinny-framework · GitHub
- skinny-framework/example/src/main/webapp/WEB-INF/views/fileUpload/form.html.ssp at 1.1.x · skinny-framework/skinny-framework · GitHub
- Controller & Routes - Skinny Framework
ファイルアップロードについては、上2つのページが参考になると思います。
ただし、実際にファイルに保存する具体的な処理については書かれていませんので、ここではその辺を書いてみます。
example
のコードに追加する想定で書いていますので、Controllers
の変更とfileUpload/index.html.ssp
については、最初に挙げたリンクのコードをそのまま使ってください。
まず、アップロードしたファイルの、保存先のディレクトリーの処理について。
開発するPCと、稼働させるサーバーのディレクトリ構成が異なることを想定して、そのマシンのhostname
をキーとしてディレクトリーのパスを取得できるようにします。
設定ファイルには、以下のように設定します。skinny.env
だけで切り替えるのであれば、hostname
を使わずにdevelopment.dir.fileUpload
のようにしても良いと思います。
application.conf
の一部
dir { fileUpload { Winpc1 = "D:/fileupload" server1 = "/var/www/fileupload" } }
application.conf
からディレクトリーパスを取り出す処理の例(FileUploadController.scala
の一部)
import java.io.File import java.net.InetAddress import com.typesafe.config._ private def getDir: Either[String, File] = { val hostName = InetAddress.getLocalHost.getHostName val propKey = s"dir.fileUpload.${hostName}" ConfigFactory.load.getString(propKey) match { case s if !s.trim.isEmpty => { new File(s) match { case f if !f.exists => Left(s"dir [${f}] does not exist") case f if !f.isDirectory => Left(s"[${f}] is not a directory") case f => Right(f) } } case _ => Left(s"""prop "${propKey}" is empty""") } }
この例では、Either
を使って、ディレクトリーを取得できた場合はjava.io.File
を、取得できなかった場合はその理由を書いたString
を返すようにしました。
呼び出す側では、case Left(x)
のときはディレクトリーの取得が失敗したものと判断します。
アップロードされてきたファイルは、org.scalatra.servlet.FileUploadSupport
のfileParams.get("file")
でorg.scalatra.servlet.FileItem(Option)
を取得できるようになっています。
ですので、何も考えずに...といいつつ上書きのみガードして...保存するだけなら、次のようにします。
FileItem
をファイルに書き出す
// import java.io.File val dir: File = ... // getDir.right.get fileParams.get("file") match { case Some(fileItem) => new File(dir, fileItem.name) match { case f if f.exists => // error: file ${f} already exists case f => fileItem.write(f) } case None => // file was not set }
エラーなどのメッセージを表示する
Scaffoldされたコードや、examplesなどを見てみると、いくつか見つかります。
flash
Seq[String]
skinny.KeyAndErrorMessages
(formのvalidation error用?)
flash
は明示的にview
にパラメーターを渡す必要が無いので、手軽に使えます。
Seq[String]
は、単に、パラメーターとして渡すだけです。
skinny.KeyAndErrorMessages
の使い方は、Scaffoldして生成されたviews/***/_form.html.ssp
にそのまま書いてあります。
flash
を使った例です。
skinny.Skinny
controller
でメッセージを設定
flash += ("error" -> "foo")
ssp
でメッセージを表示
#for (e <- s.flash.error) <p class="alert alert-danger">${e}</p> #end
s.flash
はOption[Any]
が設定できますが、Seq
などを使いたいときは、他の方法を使った方が良いと思います。s.flash
はあくまでお手軽方式ということで。
と言いつつ、タイトルと同じようにlayoutテンプレートだけで処理できると楽ができるので、ちょっとした仕掛けを作ってしまいましょう。
ViewMessage
とflash
への設定
package controller abstract class ViewMessage { def content: String def style: String } case class ErrorMessage(content: String, style: String = "alert-danger") extends ViewMessage case class WarningMessage(content: String, style: String = "alert-warning") extends ViewMessage case class SuccessMessage(content: String, style: String = "alert-success") extends ViewMessage case class NoticeMessage(content: String, style: String = "alert-info") extends ViewMessage // flashへの設定 flash += ("messages" -> Seq(ErrorMessage("error"), WarningMessage("warning"), SuccessMessage("success"), NoticeMessage("notice")))
- テンプレート(ssp)の記述
#for (messages <- s.flash.messages; message <- messages.asInstanceOf[Seq[ViewMessage]]) <p class="alert ${message.style}">${message.content}</p> #end
これで、それぞれのページの先頭にメッセージを出すことができるようになりました。
以上、skinnyでアプリ作るときの参考になれば幸いです。
またネタがストックできたら、公開させていただきます。