Magnolia Tech

いつもコードのことばかり考えている人のために。

Effective DevOpsを読んだ

Effective DevOps ―4本柱による持続可能な組織文化の育て方

Effective DevOps ―4本柱による持続可能な組織文化の育て方

買ったはいいけど、なかなか読む時間が取れなかった「Effective DevOps」を読み始めた。

一言でまとめれば、以下のツイートで全部かな。

以下、章ごとの感想

1章〜4章

1章から4章までは歴史や概念の説明なので、少しでも最近の開発手法を知っていたり、それなりの経験がある人であれば後から読んでもいい気もするが、2.2.2だけは印象深いトピックが書かれていてぜひ開発組織に属する人であれば読んで欲しい内容(あとで5.2.1でも再度出てくるけど)。

要はミスが起きた時に、個人の責とするか、組織の問題とするか、なのだけど、それを「ヒューマンエラーはトラブルの原因だ」と考える文化と、「ヒューマンエラーをシステムのもっと深いところにある問題の兆候」と考える文化という言い方で説明するところが凄くしっくりくる。

とくに現代的な開発手法やツールの発展を考えると、20年同じ手順を秘伝のタレ的な知見に基づいて続けていく(しかもその根拠は既に失われている)という現場は少なく、個人の責を問うにはあまりに学ぶべきことは多いし、変化も激しい。また、人もどんどん入れ替わっていくし、伝統的な先輩から後輩への伝達とも限らない(ある日突然アウトソーシング先が入札でガラっと変わることだってあり得る)。

そう考えると個人を責めてもしょうがなくて、組織で考えていかないとトータルでは良くなって行かないよねって考え方は実に正しいなって。

5章 devopsに対する誤解とアンチパターン

ある程度の開発経験が有る人であれば、5章の「5.1 devopsに対するよくある誤解」と、「5.2 devopsのアンチパターン」から読み始めると良いかなって思った。特に、5.2は一番最初に読んだ方が良い章。この章に対して納得感が有るか無いかで、この本から得られるものが有るか無いかははっきりすると思う。

6章はⅡ部の導入部なので飛ばして…

7章 コラボレーション

ここは完全に組織で仕事をしていく上での普遍的な話にフォーカスしている。

  • 7.3 個人の違いと経歴、背景
  • 7.4 競争優位を得るためのチャンス
  • 7.5 メンターシップ
  • 7.6 マインドセット入門
  • 7.7 マインドセットと学習する組織
  • 7.8 フィードバックの役割
  • 7.9 評価とランキング
  • 7.10 コミュニケーションと対立の解決スタイル
  • 7.11 共感と信頼
  • 7.12 人材配置と人事管理

これ、完全に組織論、人材論になっているってことがよく分かると思う。個人的には「7.6 マインドセット入門」あたりが興味深かったけど、本当にこの章はdevopsに特化した内容ではなく、どうやってより良い組織を作っていくか?という話が続くので、ひょっとしたら人によってはガッカリしたり、意外な発見が有ったりするのでは、と思う箇所。

だからこそ冒頭のツイートに繋がるんだけど、ひょっとして開発プロセスや、ツールの使い方の本だと思ってこの本を読まないとしたら、本当にそれはもったいないって。開発チームのマネジメントを行う人、特にこれからリーダーになるような人はまず読んだ方が良い。5章と7章だけでも良いので読んで欲しい。

以降はチームでの仕事をどうやってより良くしていくか?という話や、(概念的な意味での)ツール導入、devopsをスケールさせる話とかなので、一気に読むより少しずつ自分の状況に合わせて読み進める方が良いと感じた。

おわりに

全般を通して「これがdevopsだ!」というより、一貫して「良い開発組織であり続けるためには?」という話が続く所が良い本。

ただ、上司にdevopsの導入を進言する時には使いづらいかもね:)

300ページ超は、この手の本としてはそこまで大ボリュームではないけど、それでも全部を一気に読み切ろうとすると消化不良を起こしかねないような「考えさせられる」本であることは間違いないので、あまり慌てずゆっくり(できれば周りの人とディスカッションしたりしながら)読んだ方が良さそう。

GraphQLナイトへ参加してきた

たまたま「GraphQL」について調べていたタイミングで開催が決定したのと、最近すっかりpmイベント以外に参加できていないことも有って参加してきた。

connpass.com

GraphQL自体の入門、ScalaRubyでのライブラリなどの紹介など、丁度知りたいレベル感と合っていたので、ぴったりのイベントでした。

あと、今回開催のきっかけが主催の@htomineさんが所属する会社の中でみんながGraphQLについて楽しそうに語っているんだけど、意外と外に出て発表していないからって所も良かった。背中を押す人、環境を作る人ってマジで大事ですね。

確認できた分のスライドへのリンクを張っておきます。そのうち、イベントサイトにもまとめられると思います。

speakerdeck.com

speakerdeck.com

speakerdeck.com

会場提供はfreeeさんでした。

あと、GraphQLのイベントとしてGraphQL Tokyoというミートアップが有るそうです。次回はすぐの7月4日だそうです。

www.meetup.com

GraphQL、複雑化した要求や制約の中で効率よくデータを取得する必要性から生まれたものだそうですが、こうやってベストプラクティスがライブラリや規約の形で世の中に広まっていく感じがいいですね。

イベント最高でした。@htomine++!

Learning GraphQL: Declarative Data Fetching for Modern Web Apps

Learning GraphQL: Declarative Data Fetching for Modern Web Apps

WEB+DB PRESS Vol.104

WEB+DB PRESS Vol.104

  • 作者: 末田卓巳,林田千瑛,陶山嶺,八谷賢,辰己佳祐,竹澤俊季,服部智,藤岡裕吾,牧大輔,西郡卓矢,松木雅幸,穴井宏幸,新日出海,桑原仁雄,小田知央,ひげぽん,池田拓司,はまちや2,竹原,大場光一郎,大場寧子,松館大輝,日高尚美,Vu Xuan Dung,WEB+DB PRESS編集部
  • 出版社/メーカー: 技術評論社
  • 発売日: 2018/04/24
  • メディア: 単行本
  • この商品を含むブログを見る

GraphQL API Design (API-University Series Book 5) (English Edition)

GraphQL API Design (API-University Series Book 5) (English Edition)

java.net.URLConnectionのguessContentTypeFromNameが使うMIME Type設定は実行時に変更することはできない

java.net.URLConnectionのguessContentTypeFromNameは、content.types.user.tableというシステムプロパティで定義された内容で任意のMIME typeを推測できるようになります。

公式ドキュメントにも書かれています。

URLConnection (Java Platform SE 8)

では実際にそれを確かめてみようと、テストコードの中でSystem.setProperty("content.types.user.table", "/path/to/content-types.properties")と指定してもさっぱり有効になりませんでした。

例えば、拡張子.csstext/cssと判定させるためには、以下のような設定ファイルを用意します。

text/css: \
        description=Cascading Style Sheets;\
        file_extensions=.css

以下のようなコードを用意して、設定ファイルを読み込んでみましが、mimeTypeにはnullが入りました。

System.setProperty("content.types.user.table", "/path/to/content-types.properties")
val mimeType = URLConnection.guessContentTypeFromName("test.css")

実行時ではなく、予めsbt -Dcontent.types.user.table=/path/to/content-types.propertiesというふうに実行前に設定されるようにしておくと意図した通りにmimeTypeにはtext/cssが入ります。

java.net.URLConnectionのguessContentTypeFromNameのコードを追いかける

これは何が起きているのでしょうか?

実は最初、Stack OverflowにSystem.setPropertyでセットすれば反映されると書かれていたので、それを鵜呑みにして「正しく動かない!自分の書いたコードがおかしいのか?」と思いましたが、実際にはその回答が誤りでした。

順番に、java.net.URLConnectionguessContentTypeFromNameメソッドの挙動を、openJDK10のコードを例に追いかけていきましょう。

openJDKの該当するソースコードは以下の場所に有ります。

jdk10/master: be620a591379 src/java.base/share/classes/java/net/URLConnection.java

public static String guessContentTypeFromName(String fname) {
        return getFileNameMap().getContentTypeFor(fname);
}

public static FileNameMap getFileNameMap() {
    FileNameMap map = fileNameMap;

    if (map == null) {
        fileNameMap = map = new FileNameMap() {
            private FileNameMap internalMap =
                sun.net.www.MimeTable.loadTable();

            public String getContentTypeFor(String fileName) {
                return internalMap.getContentTypeFor(fileName);
            }
        };
    }

    return map;
}

MIME Typeに関連するデータは、FileNameMapというクラスに格納されていること、値が無ければ(nullならば)されていなければsun.net.www.MimeTable.loadTableを呼び出して、初期化していることが分かります。

なお、getContentTypeForFileNameMapのメソッドで、ほぼ単純なmap構造のデータから該当するMIME typeを取得する機能を提供します。

sun.net.www.MimeTable.loadTableの中身を追いかける

では、実際にMIME typeのテーブルを保持するsun.net.www.MimeTable.loadTableの中身を追いかけてみましょう。

sun.net.www.MimeTable.loadTableは以下のファイルに収録されています。

jdk10/master: be620a591379 src/java.base/share/classes/sun/net/www/MimeTable.java

public static FileNameMap loadTable() {
    MimeTable mt = getDefaultTable();
    return (FileNameMap)mt;
}

public static MimeTable getDefaultTable() {
    return DefaultInstanceHolder.defaultInstance;
}

private static class DefaultInstanceHolder {
    static final MimeTable defaultInstance = getDefaultInstance();

    static MimeTable getDefaultInstance() {
        return java.security.AccessController.doPrivileged(
            new java.security.PrivilegedAction<MimeTable>() {
            public MimeTable run() {
                MimeTable instance = new MimeTable();
                URLConnection.setFileNameMap(instance);
                return instance;
            }
        });
    }
}

loadTableから始まり、DefaultInstanceHolderクラスにdefaultInstanceというstaticなメンバが有ることが分かります。staticなクラスのstaticなメンバなので、クラス自体がロードされた時点で、getDefaultInstanceメソッドが実行され、defaultInstanceにはMIME typeのデータがロードされている、ということなのです。

更に、この時点でなぜかURLConnection.setFileNameMapを呼び出し、ロードしたMIME Typeをセットしています。getFileNameMapメソッドの中でやっているFileNameMap mapがnullか否かを判定するロジックっていらなくない?って思いますね。

実際にロードする部分のコードをもう少し追いかけてみましょう。

MimeTable() {
    load();
}

public synchronized void load() {
    Properties entries = new Properties();
    File file = null;
    InputStream in;

    // First try to load the user-specific table, if it exists
    String userTablePath = System.getProperty("content.types.user.table");
    if (userTablePath != null && (file = new File(userTablePath)).exists()) {
        try {
            in = new FileInputStream(file);
        } catch (FileNotFoundException e) {
            System.err.println("Warning: " + file.getPath()
                                + " mime table not found.");
            return;
        }
    } else {
        in = MimeTable.class.getResourceAsStream("content-types.properties");
        if (in == null)
            throw new InternalError("default mime table not found");
    }

    try (BufferedInputStream bin = new BufferedInputStream(in)) {
        entries.load(bin);
    } catch (IOException e) {
        System.err.println("Warning: " + e.getMessage());
    }
    parse(entries);
}

確かにcontent.types.user.tableというシステムプロパティが設定されていれば、そこからパスを取得するようになっていますね。存在しなければデフォルトのcontent-types.propertiesを取得しています。

ちなみに、このcontent.types.user.tableというシステムプロパティを元に任意のMIME Typeを設定する機能について、テストコードが存在しません。確かにこれではグローバルにシステムプロパティを汚染しないとテストが書けないですね…

おわりに

以上、java.net.URLConnectionguessContentTypeFromNameにおけるcontent.types.user.tableの取扱について、実際のJavaのライブラリのコードを追いかけてみました。

依存関係(sun.net.www.MimeTable.loadTablejava.net.URLConnectionに依存している)がおかしいとか、初回利用時ではなく、クラスロード時に強制的に初期化が行われ、以降は再設定もできないとか、そもそもテストが無いとか、よく考えるとguessContentTypeFromNameというメソッド自体java.net.URLConnectionではなく、独立したMIMEに関するクラスに所属しているべきでは?と、標準のJavaのライブラリでも色々と設計が気になるところが有るんだな、というのが今回の感想でした。

java.net.URLConnection.guessContentTypeFromNameが使うデフォルトのcontent-types.propertiesがJava9から変わった

java.net.URLConnection.guessContentTypeFromNameはファイルの拡張子からMIME Typeを特定するメソッドです。このメソッドはJava8以前ではデフォルトで、$JAVA_HOME/lib/content-types.propertiesを参照していました。

しかし、Java9以降では、$JAVA_HOME/lib/content-types.propertiesは有りません。

この辺りの事情は、Java9で削除された機能に関する解説に書かれています。

Removed APIs, Features, and Options

詳細はこちら。

bugs.java.com

ちなみに本当に削除された訳では無く、jdk/src/java.base/windows/classes/sun/net/www/配下に、ソースと一緒におかれています。

jdk10/master: 4554a9ae19ef jdk/src/java.base/windows/classes/sun/net/www/content-types.properties

なお、システムプロパティのcontent.types.user.tableに設定したパスのファイルに定義を書いておくと、任意のMIME Typeを追加することができる点は変わっていません。

以上、意外なところでJava9の変更点に引っかかって、「ファイルが無い!」と1時間溶かした記録です。モジュール化、難しい。

Java 9 Modularity: Patterns and Practices for Developing Maintainable Applications

Java 9 Modularity: Patterns and Practices for Developing Maintainable Applications

WEB+DB PRESS Vol.101

WEB+DB PRESS Vol.101

Javaでファイルの内容からMIME Typeを推測する方法

前回のエントリの続きです。

blog.magnolia.tech

前回はファイル名から…つまり拡張子をもとにMIME Typeを特定する方法を紹介しましたが、今回はファイルの内容からMIME Typeを推測する方法です。

Files.probeContentType

前回も紹介した下記のブログエントリに、カスタマイズ方法が記載されていますが、Files.probeContentTypeMIME Typeを決定するアルゴリズムはカスタマイズ可能で、java.nio.file.spi.FileTypeDetectorを継承したクラスを作成し、サービスとして登録します。

waman.hatenablog.com

デフォルトのアルゴリズムでは、ファイル名を元にMIME Typeを決定しているので、独自にロジックを書けばファイルの内容からMIME Typeを推測することが可能になります…が、ちょっと使うにはかなりハードルが高いですし、汎用的に作ろうとすると異常な努力が必要になってくるので、アプリケーションの仕様から特定のフォーマットを識別したい、という時以外は採用は厳しそうです。

URLConnection.guessContentTypeFromStream

URLConnection.guessContentTypeFromNameはファイル名(拡張子)からMIME Typeを特定していましたが、java.io.InputStreamからの入力(つまりデータの中身そのもの)からMIME Typeを推測するメソッドがURLConnection.guessContentTypeFromStreamです。

いきなりopenJDKのソースへのリンクを張っておきますが、実装を見ると主にtext/htmlapplication/xmlimage/gifimage/pngimage/jpegあたりの判定には使えそうです(それ以外は…なぜそれを判定対象にした?という感じの並びですね…)。

github.com

判定ロジックがべた書きされているので、カスタマイズ方法は無さそうです。サブクラスを作れば別ですが、そうすると完全にロジックを書かないといけないので、これまた異常な努力が必要になりますね。

おわりに

ファイルの内容から推測する方法は、あまり汎用的な方法は(当然ですが)無いので、URLConnection.guessContentTypeFromStreamを使う以上の汎用性は求めない方が良さそうです。

スッキリわかるJava入門 第2版 (スッキリシリーズ)

スッキリわかるJava入門 第2版 (スッキリシリーズ)

ファイル名からMIME Typeを教えてくれるJavaのライブラリ

ScalatraのMIME Type関係の機能が盛大に壊れている訳ですが、じゃあそれを直すためにMIME Typeを教えてくれるライブラリについて調べてみました。標準ライブラリだけでもいくつか方法が有りますね。

Files.probeContentType

Java7から導入されたFiles.probeContentTypeを使うと、簡単にMIME Typeを取得できます。

scala> import java.nio.file.Files
import java.nio.file.Files

scala> import java.nio.file.Paths
import java.nio.file.Paths

scala> val htmlfile = Paths.get("test.html")
htmlfile: java.nio.file.Path = test.html

scala> Files.probeContentType(htmlfile)
res2: String = text/html

scala> val cssfile = Paths.get("test.css")
cssfile: java.nio.file.Path = test.css

scala> Files.probeContentType(cssfile)
res3: String = text/css

scala> val jsfile = Paths.get("test.js")
jsfile: java.nio.file.Path = test.js

scala> Files.probeContentType(jsfile)
res4: String = text/javascript

最後、javascripttext/javascriptになっていますが、application/javascriptで返して欲しいですね。カスタマイズは可能です。下記のブログエントリが参考になります。

waman.hatenablog.com

返って来る値が実装依存なので、自分が使いたいMIME Typeはきちっとテストを書いておきましょう。

URLConnection.guessContentTypeFromName

URLConnection.guessContentTypeFromNameもファイル名からMIME Typeを取得できます。

scala> import java.net.URLConnection
import java.net.URLConnection

scala> URLConnection.guessContentTypeFromName("test.html")
res5: String = text/html

scala> URLConnection.guessContentTypeFromName("test.css")
res6: String = null

scala> URLConnection.guessContentTypeFromName("test.js")
res7: String = null

デフォルトでは、かなり残念な結果になりました。こちらもMIME Typeはカスタマイズ可能です。こちらの記事によると、content-types.propertiesというファイルを用意すれば良さそうです。

Getting and Extending mime-types in Javathilosdevblog.wordpress.com

content-types.propertiesの正式なフォーマットの解説が見つけられませんでしたが、こちらのブログにサンプルファイルが紹介されていました。

alvinalexander.com

javax.activation.MimetypesFileTypeMap

最後はjavax.activation.MimetypesFileTypeMapです。

scala> import javax.activation.MimetypesFileTypeMap
import javax.activation.MimetypesFileTypeMap

scala> val mimeTypesMap = new MimetypesFileTypeMap
mimeTypesMap: javax.activation.MimetypesFileTypeMap = javax.activation.MimetypesFileTypeMap@7e258a05

scala> mimeTypesMap.getContentType("test.html")
res8: String = text/html

scala> mimeTypesMap.getContentType("test.css")
res9: String = application/octet-stream

scala> mimeTypesMap.getContentType("test.js")
res10: String = application/octet-stream

こちらもcssjavascriptが正しく扱えていません。カスタマイズ方法は公式ドキュメントにきちんと書かれています。

MimetypesFileTypeMap (Java Platform SE 8)

しかし、残念ながらこのパッケージは将来廃止が予定されています。

JEP 320: Remove the Java EE and CORBA Modules

これから新しく使うには相応しくないですね。

おわりに

ファイル名からMIME Typeを取得する方法を紹介しました。標準ライブラリだけでも3種類有って、更にそれぞれカスタマイズ方法が違うってなかなかカオスな感じですね。

ScalatraのMimeTypesは、ファイルの内容に基づいてMIME Typeを推測する機能も有るので、次回はその方法について紹介します。

スッキリわかるJava入門 第2版 (スッキリシリーズ)

スッキリわかる Java入門 実践編 第2版 (スッキリシリーズ)

スッキリわかる サーブレット&JSP入門 (スッキリシリーズ)

Scalatra-JSONの使い方

ScalatraのJSONサポート

ScalatraにはJSONのサポートが用意されていていて、JSONのリクエストやレスポンスにまつわる種々の機能を提供してくれます。

一応公式ドキュメントにはざっと使い方が説明されていますが、ちょっと端折り過ぎ感が有るのと、内部構造を理解した方がメリットをより理解できる部分も有るので、改めて使い方をまとめてみました。

長くなってきたので、今回は環境準備と、リクエストのJSONをパースするところまでです。JSONのレスポンスを生成する方は次回に。

2018/5/20 18:30追記

Json4sがパースエラーを起こした時の挙動が正確ではなかったので、修正しました。例外はWebアプリケーションまで伝搬せず、JNothingが返って来るだけです。

環境準備

プロジェクト作成

まずはsbt newコマンドでScalatraプロジェクトのひな形を用意します。

$ sbt new scalatra/scalatra.g8

色々と質問が出てきますが、今回は全てデフォルトのままで進めましょう(つまり、全てリターンキーを押下して進める)。

ちなみにsbt newコマンドはgitリポジトリの作成までは面倒を見てくれないので、プロジェクトが作成されたら忘れずにgit initをしておきましょう。

build.sbtの追記

build.sbtに必要なアーティファクトを追加します。

ScalatraのJSONモジュールはscalatra-jsonという別モジュールで提供されているので、まずはそれを追加します。バージョンはScalatra本体に必ず合わせて下さい。

またscalatra-jsonjson4sをベースで作られていて、JSONパーサとしてJacksonか、Lift-Json由来のNativeの2種類のうち、どちらかを選択して使用するようになっています。

どちらを使用するかに合わせてbuild.sbtに下記のアーティファクトを追加します。

  • Jacksonをパーサに使う場合は、json4s-jacksonを指定
  • Nativeパーサを使う場合は、json4s-nativeを指定

なお、json4sのバージョンはScalatraが内部で指定しているバージョンに合わせる必要が有ります。例えばScalatra 2.6.3はjson4s 3.5.2を採用しているので、同じようにbuild.sbtには3.5.2を指定する必要が有ります。バージョンが不一致の場合コンパイルエラーになります。この辺りはドキュメントには書かれていないので、必ずScalatra本体のDependencies.scalaを参照して下さい。

Scalatra 2.6.3で、Jacksonパーサーを使う場合のbuild.sbtは以下のようになります。

val ScalatraVersion = "2.6.3"

libraryDependencies ++= Seq(
  "org.scalatra" %% "scalatra" % ScalatraVersion,
  "org.scalatra" %% "scalatra-json" % ScalatraVersion,
  "org.json4s" %% "json4s-jackson" % "3.5.2",
...
)

これで準備ができました。

JSONを受け取る

HTTP RequestにJSONが含まれている場合、parsedBodyというメソッドを使うとrequestにJSONが含まれているか否かの判定と、JSONのパースまでを一気にやってくれるので便利です。

parsedBodyの使い方

先ほどの環境設定でパーサにJacksonとNativeのうち、どちらを利用するか選択しましたが、選択したパーサに合わせて、JacksonJsonSupport trait又はNativeJsonSupport traitをmix-inします。

例えばJacksonJsonSupportを使う場合は、以下のようなコードになります。

jsonFormatsは、json4sのパーサが必要とする変換方法の指定です。

package com.example.app

import org.scalatra._
import org.scalatra.json._

import org.json4s._
import org.json4s.{DefaultFormats, Formats}

class MyScalatraServlet extends ScalatraServlet with JacksonJsonSupport {
  protected implicit lazy val jsonFormats: Formats = DefaultFormats

  post("/") {
    val ast = parsedBody

    ...
  }

parsedBodyContent-TypeJSONの場合にのみパースを行うので、もしJSON以外が指定されている場合はJNothing(Json4sでのNone型に相当)が返ります。また、正常にパースできない、つまりinvalidなJSONデータの場合もJNothingが返ります。

そのため、JNothingが返却される可能性について対応するコードを用意しておく必要が有ります(この肝心なことが公式ドキュメントに書かれていない!!)

ちなみに、Json4sはパースエラー時には例外を送出しますが、Scalatra内でその例外をキャッチしエラーログへ出力し、JNothingを返しているので、Webアプリケーション側には例外は返ってきません。パースエラーになった原因はログから追跡しましょう。

case classにマッピングする

parsedBodyはJson4sのASTを返すメソッドなので、ASTが得られれば後は通常のJson4sの使い方と同じようにJSONデータをcase class等にマッピングして使用します。

case class Person(id: Int, name: String)

class MyScalatraServlet extends ScalatraServlet with JacksonJsonSupport {
  protected implicit lazy val jsonFormats: Formats = DefaultFormats

  post("/create") {
    val ast = parsedBody

    val person = ast.extract[Person]

    person.name
  }

case classにマッピングできない場合は例外が送出されますので、やはり例外に対応したコードを書いておく必要が有ります。ここは素のJson4sなので、Json4sのドキュメントを参照し、色々と試してみて下さい。

またJson4sのcase classへのマッピングはリフレクションを使用していますが、リフレクションの制約によりクラス内で定義されたcase classでは正しく動作しません。必ずトップレベルでcase classを定義するようにして下さい。小さなアプリケーションではうっかりServlet Classの中でcase classを定義してしまいそうになりますが、それは誤りです(初めて使ったとき、これが分からず数時間悩みました)。

下記のissueが参考になるでしょう。

https://github.com/json4s/json4s/issues/125

JsonValueReader

パースしたASTを簡易にサーチするためのヘルパークラスとしてJsonValueReaderというクラスが用意されています。パスをドットで連結したものをreadメソッドに渡すと、ASTを辿って行って、オブジェクトを取得してくれます。

case class Person(id: Int, name: String)
case class Group(name: String, person: Person)

class MyScalatraServlet extends ScalatraServlet with JacksonJsonSupport {
  protected implicit lazy val jsonFormats: Formats = DefaultFormats

  post("/create") {
    val ast = parsedBody
    val reader = new JsonValueReader(ast)
    val name = reader.read("person.name")

    name
  }
}

ドキュメントもテストもないので、これを使うくらいだったらJson4sが提供するクエリ構文を使った方がお勧めです。

XML

Json4sはJSONXMLの相互変換をサポートしているため、scalatra-jsonでもXMLをサポートしています。XMLをリクエストで受け取った場合は、前述の流れと同じようにJson4sのJSON ASTを得ることができます(Content-Typeがxml、つまり'application/xml'でもparsedBodyは有効です)。

ですが、Json4sを使わなくても…という感じなので、そこは普通にXMLとしてパースした方が良いでしょう。

Content-Typeの指定

parsedBodyはContent-Typeヘッダを元にJSONか否かを判定しています(具体的にはapplication/json)。そのため、例えばContent-Typeにapplication/x-www-form-urlencodedが指定されていると、body部をJSONとしてパースはしません(判別できないので当たり前ですが…)。更にServletの仕様により、bodyの内容がパラメータに格納されているパターンも有るので要注意です。

blog.magnolia.tech

クライアントからのContent-Typeに注意しましょう(特にcurlはPOST時はapplication/x-www-form-urlencodedがデフォルトです)。

おわりに

長くなってきたので、ここまで。次回はJSONのレスポンスを生成する箇所をやります。

ScalatraのJSONサポートはJson4sに強く依存し過ぎていて(XMLサポートとか必要?)、昨今の「リフレクションを使っているJson4sを避けた方が良い」という流れも有って、別のJSONモジュールをベースに作り直した方が良い、という時期に来ています(Scalatra-Json2?)。

JSONレスポンスの解説が終わったら、ScalaJsonモジュール一覧の紹介をやろうと思います。

Scalatra in Action

Scalatra in Action

  • 作者: Ivan Porto Carrero,Ross A. Baker,Dave Hrycyszyn,Stefan Ollinger,Jared Armstrong
  • 出版社/メーカー: Manning Pubns Co
  • 発売日: 2014/01/28
  • メディア: ペーパーバック
  • この商品を含むブログを見る

Webを支える技術 -HTTP、URI、HTML、そしてREST (WEB+DB PRESS plus)

Webを支える技術 -HTTP、URI、HTML、そしてREST (WEB+DB PRESS plus)