ひだまりソケットは壊れない

ソフトウェア開発に関する話を書きます。 最近は主に Android アプリ、Windows アプリ (UWP アプリ)、Java 関係です。

まじめなことを書くつもりでやっています。 適当なことは 「一角獣は夜に啼く」 に書いています。

仕様変更が想定される場合の Fizz Buzz のドメインモデリングについて

前の記事 「Fizz Buzz と税率とタイムゾーンの話 (ドメインレイヤとアプリケーションレイヤの話、あるいは時間変化する値をモデリングする話)」 でもちょっと言及した下記のついーと。

変化しやすい箇所を依存関係の外側に定義するというのはクリーンアーキテクチャの教えとして正しいのだけど、ドメインユースケースを捉えるという意味では「仕様変更されやすそうならユースケース」 というのは本質的ではなく、その点については賛成できない。

一方で「仕様変更されやすそうなときはどうするの?」 というのは良い問題提起だなーと感じた。 というわけで 『今後 「4 の倍数なら huzz が返る」 的な仕様追加が容易に想像できる』 場合に自分だったらどうするか考えてみる。

自分だったらどうするか?

想定される変化に今のドメインモデルでは対応できない、ということは、ドメインモデル上で本来変更されやすくなっているべき箇所 *1 が変更されやすくなっていないということだ。 おそらくドメインモデルに改善の余地があると思われるので、ドメインを捉えなおしてドメインモデリングをやり直したい。

「4 の倍数なら huzz が返る」 的な仕様追加が容易に想像できる、という感じであれば、Fizz Buzz 問題を下記のように捉えなおすことができそうである。

条件と文字列の組 (「設定項目」 と呼ぶ) が複数与えられる。 数字を数え上げていき、その数字がいずれかの設定項目の条件に合致する場合はその設定項目の文字列を表示すること。 合致するものがない場合はその数字をそのまま表示すること。 ただし、合致する条件を持つ設定項目が複数ある場合は、それらの文字列をすべて連結して表示すること。

ここで、設定項目は次のとおりである。

  • 条件 : 数値が 3 の倍数; 文字列 : Fizz
  • 条件 : 数値が 5 の倍数; 文字列 : Buzz

問題を上のように捉えなおすことでドメインモデルも変化する。 以下のような感じである。

class FizzBuzzConfigItem(
        /** この条件が真になる場合に [expression] が出力に含まれる。 */
        val predicate: (Int) -> Boolean,
        /** 条件が真の場合に出力される文字列。 */
        val expression: String
)

class FizzBuzzCalculator(private val config: List<FizzBuzzConfigItem>) {
    fun calculate(inputNumber: Int): String = run {
        val expressions =
                config.filter { it.predicate(inputNumber) }
                        .map(FizzBuzzConfigItem::expression)
         if (expressions.isEmpty()) {
            inputNumber.toString()
        } else {
            expressions.joinToString("")
        }
    }
}

これで通常の Fizz Buzz に対応することも 「4 の倍数なら Huzz」 という仕様を追加することも容易になった。

        // 3 の倍数なら Fizz、5 の倍数なら Buzz を返す。
        val fizzBuzzCalculator = FizzBuzzCalculator(listOf(
                FizzBuzzConfigItem({ it % 3 == 0 }, "Fizz"),
                FizzBuzzConfigItem({ it % 5 == 0 }, "Buzz")
        ))

        // 3 の倍数なら Fizz、4 の倍数なら Huzz、5 の倍数なら Buzz を返す。
        val fizzHuzzBuzzCalculator = FizzBuzzCalculator(listOf(
                FizzBuzzConfigItem({ it % 3 == 0 }, "Fizz"),
                FizzBuzzConfigItem({ it % 4 == 0 }, "Huzz"),
                FizzBuzzConfigItem({ it % 5 == 0 }, "Buzz")
        ))

「3 の倍数なら Fizz」 というような設定項目をどこに定義するかは難しいところだが、カスタマイズ性がそこまで高くなくて良いなら (仕様変更がたまにであれば) ドメイン層の中にべた書きしてしまって良いだろう。 もしユースケースごとに異なるのであればユースケース層に定義すると良いし、ユーザーごとに変更する必要があるならそもそもコード上に定義するのではなくてストレージに保存することになるだろう。

コード全体は fizzbuzz_configurable.kt においてある。

ここまで書いたけど

ドメインレイヤの API が変わらないなら別に実装が変わっても困らないので、必ずしも上のようにカスタマイズ性を高める必要はない気もする。 それよりも、Fizz Buzz をさらに抽象的にして 「数字を数え上げて、文字列を出力するゲーム」 みたいに捉えて、ドメインモデルとしては数字から文字列の変換のインターフェイスと実装を別に提供して切り替えやすくする、みたいな感じにしても良さそう。

(追記) ここでは事前に仕様変更について書いていたが、「2019-02-16 UseCase とは何か | wada811.com」 に書かれている通り、問題領域に変更が入ってからドメインモデリングしなおすというのでも全然問題ないと思う。

実際の開発におけるドメインモデルの変更

実際の開発においてもドメインモデルの再構築が必要になることはままある。 ドメインモデルの再構築が必要だと気付くのは大体開発の途中なので、再構築するのはちょっと大変だったりする。 だけど場当たり的な対応をするとどんどん崩壊していく (繰り返すうちにどんどん変更が難しくなっていく) ので、必要になったらしっかりドメインモデルの再構築をしていく方が良いと思う。 きれいなドメインモデルを保っていれば変更もそこまで苦ではないはず。

ちなみに前の記事で時刻によって状態が変化する場合のドメインモデリングについて書いたが、これは今開発している製品でもともと時刻に応じた状態を持っていなかったところに時刻に応じた状態を持たせるというドメインモデルの変更を行う必要があって、そのときにいろいろ考えたときのことをベースに書いた。 (具体的に言うと、人と組織の所属関係としてもともと 「所属していない」 「所属している」 「所属していた」 という状態しかもっていなかったところに、本来は 「所属期間」 という概念が必要だったということがわかってドメインモデルを変更した。) そのときはもともとのドメインレイヤの設計がめちゃくちゃだったのですごく苦労したのだけど、きれいなドメインモデルを作って実装に落とし込めていれば多分そこまで苦労せずにドメインモデルを変更できたはず。

*1:変化が想定されているということは、ドメインモデル上でも変化しやすくなっていた方が良いはず。

Fizz Buzz と税率とタイムゾーンの話 (ドメインレイヤとアプリケーションレイヤの話、あるいは時間変化する値をモデリングする話)

Twitter で見かけて面白そうだったのでちょっと考えてみた。

Fizz Buzz のロジックをドメインレイヤ (Entities 層) とアプリケーションレイヤ (Use Case 層) のどちらに書くか

(実際に作るアプリケーションにおいてはいろいろ考慮して設計することになると思うけど) Fizz Buzz の問題を素直に捉えると 「3 の倍数のときは fizz が返る」 というのは問題領域に存在するロジックなので (例えば、ソフトウェアを実装せずに人が手で Fizz Buzz を書きだす場合でもこのロジックは使われる)、自分だったらドメイン層に書くなーと思った。

(文字列を返すドメインサービスにするのか、バリューオブジェクトを定義するのか、みたいなところは場合によって分かれそう。)

アプリケーション層 (Use Case 層) にはアプリケーションとしてどう使われるかに応じたロジックを書く。 例えば 「1 から指定された値までの Fizz Buzz をコンソールに表示する」 だったらコンソールにアウトプットするための処理を書く *1 し、例えば 「1 秒ごとに Epoch 秒に応じた Fizz Buzz の値をどこかに送る」 みたいなアプリケーションだったら Epoch 秒を取得してどこかに送るみたいな処理を書くことになる。 (個人の見解。)

『Clean Architecture』 によると

Clean Architecture 達人に学ぶソフトウェアの構造と設計 (アスキードワンゴ)

Clean Architecture 達人に学ぶソフトウェアの構造と設計 (アスキードワンゴ)

Uncle Bob によって書かれた書籍にはエンティティについて下記のように書かれている。

エンティティは、企業全体の最重要ビジネスルールをカプセル化したものだ。エンティティは、メソッドを持ったオブジェクトでも、データ構造と関数でも構わない。企業にあるさまざまなアプリケーションから使用できるなら、エンティティは何であっても問題はない。

企業が存在せず、単一のアプリケーションを作成しているだけなら、エンティティはアプリケーションのビジネスオブジェクトになるだろう。それは、最も一般的で、最上位レベルのルールをカプセル化したものである。

ユースケースについては下記。

ユースケースのレイヤーのソフトウェアには、アプリケーション固有のビジネスルールが含まれている。ここには、システムのすべてのユースケースカプセル化・実装されている。ユースケースは、エンティティに入出力するデータの流れを調整し、ユースケースの目標を達成できるように、エンティティに最重要ビジネスルールを使用するように指示を出す。

これに照らしてみても、Fizz Buzz の数値から文字列を生成するロジックは問題の最上位レベルのルールなのでエンティティで、入出力回りなどをユースケースに記述する、って感じになりそう。

税率

という Fizz Buzz だけの話だったらブログに書くほどのものでもないのだけど、税率の話 (というか時間変化する状態のモデリング) については最近考えていたこともあったので吐き出しておく。

(税率とは関係ないけど、*2 上のついーとのリプ先に 『仕様追加が容易に想像できるならばUseCaseにします』 って書かれてるのは本質的ではない話で、ちゃんと問題領域をとらえてドメイン層を定義すべきでしょ、って思った。)

この話って、抽象的には 「時間変化する状態をアプリケーション上でどう扱うか」 という話 (ではなかった)*3 で、時刻が変化したときに状態がどう変化するか (例えば 2019 年 10 月までは税率 8 % で、2019 年 10 月からは税率 10 % になる) という部分も含めてドメインモデリングするのがうまい方法なんだと思う *4

つまり、ドメイン層には 「指定の商品について、指定された時刻における税込み価格を計算する」 みたいなロジックを定義して、アプリケーション層の方でそのロジックに現在時刻を渡してやることで、その時点での税込み価格を計算する、みたいな感じになる。

時間変化のモデリングの例 : Date and Time API におけるオフセット時間の時間変化

時刻によって変化する値のモデリングの例として Java の Date and Time API (JSR 310) がある。 Date and Time API は 「時刻やタイムゾーン」 を問題領域とする API であり、時刻についてのドメインモデルとして捉えて良いだろう。

このドメインにおける 「時間変化する値」 としてはタイムゾーンごとのオフセット時間がある。 タイムゾーンごとのオフセット時間は時刻によって変化するが、それがうまくモデリングされて、きれいな API が提供されている。

最近あった身近なオフセット時間の変化としては、平壌時間における 2015 年と 2018 年のオフセット時間の変化がある。

協定世界時 (UTC) より9時間進んでおり (UTC+9)、大韓民国(韓国)で使用されている韓国標準時 (KST) や、日本で使用されている日本標準時 (JST) と同じである。2015年8月15日から2018年5月4日まではUTC+8:30と定められていた。

平壌時間 - Wikipedia

こういうオフセット時間の時間変化も Date and Time API の実装が面倒を見てくれるので、API を使う側はオフセット時間の時間変化について知らずとも任意の時刻の指定のタイムゾーンでの現地時刻を取得することができる。

/** 渡された時刻を平壌時間での [LocalDateTime] にして返す。 */
fun calculatePyongyangLocalDateTime(instant: Instant) =
    instant.atZone(ZoneId.of("Asia/Pyongyang")).toLocalDateTime()

fun main() {
    val pyongyang2015 =
        calculatePyongyangLocalDateTime(Instant.parse("2015-01-01T12:00:00Z"))
    val pyongyang2017 =
        calculatePyongyangLocalDateTime(Instant.parse("2017-01-01T12:00:00Z"))
    val pyongyang2019 =
        calculatePyongyangLocalDateTime(Instant.parse("2019-01-01T12:00:00Z"))

    println(pyongyang2015) //=> 2015-01-01T21:00
    println(pyongyang2017) //=> 2017-01-01T20:30
    println(pyongyang2019) //=> 2019-01-01T21:00
}

(問題領域に税率を含み、時間の概念も持つアプリケーションを開発するのであれば) Date and Time API におけるタイムゾーンと同様に、税率を時間変化する値としてドメインモデル上で定義すると、ドメインモデルを使う側からはいい感じに税率の時間変化が隠蔽されて良い感じになるのではなかろうか。

ドメインとかユースケースとか

最近 「ドメイン」 とか 「ユースケース」 って言葉を聞くことが多いのだけど、人によって解釈が全然違ったりするなー、ということをよく感じる。 そもそも明確な定義みたいなのもないと思うのだけど、個人的には下記の書籍を読んだことで理解が進んだので、「ドメイン」 とか 「ユースケース」 についてよくわかんないけどなんか書籍を読みたい、って人は下記みたいな書籍を読むと良さそう。 (私は DDD に関する本はいろいろ読んだけどユースケースについて言及している書籍は下記の 1 冊しか読んだことがないので、他におすすめがあれば教えてください!)

ユースケース駆動開発実践ガイド

ユースケース駆動開発実践ガイド

この書籍では、要件定義からアプリケーションの実装までの開発のライフサイクルを扱っている。 まずアプリケーションが扱う問題領域から重要な言葉を抜き出してドメイン辞書を作る。 それからユースケース (アクター *5 とシステムの相互作用についての記述) を記述する。 ドメイン辞書は静的な設計の出発点で、ユースケースは動的な設計の出発点。 ここから初めて実装に落とし込むまでの流れを説明する、という感じ。

で、この書籍で説明されているユースケースと Clean Architecture でいうユースケース層の関係としては、ユースケース記述に書かれている文章と同じぐらいの粒度でユースケース層のメソッドを実装する、って感じになる。

なので、単純なユースケースについては 『ユースケース図の吹き出しで出てくるぐらいの粒度のものがその名の通り1UseCaseというイメージ』 となるはず *6。 実際のところは、画面をまたがるユースケースがあったり、イベント駆動な設計になっている場合もあったりするので、1 ユースケースユースケース層の 1 メソッドや 1 クラスに相当するということはなくて、複数メソッドに分散したりするはず。

*1:実際のコンソールへのアウトプットを行う処理自体はさらに上のレイヤから渡すべきで、アプリケーション層では抽象化された出力操作を行う。

*2:下の注釈に書いたとおり、関係あるということに気づいた。

*3:Fizz Buzz の話の流れで書かれていたので、この文を書いていた時には 「Fizz Buzz の入力数字を時刻、出力の文字列を税率と見立てて、税率の時間変化をどう扱うか」 という問題提起なのかなーと思ったんだけど、今読み返すと 「仕様追加が容易に想像できるならばUseCaseにします」 への応答だったんだな、と思った。 文脈が読めてなかった……><

*4:もちろんアプリケーションに求められるものによって税率などをどこで扱うのがいいかは違ってくるとは思っていて、例えば 「表示時にシュッと税込み価格を計算して表示すればいいだけで、ドメインロジックとして税率を扱う必要はない」 みたいなアプリケーションだったらプレゼンテーション層に税率計算の処理を入れちゃってもいい話だし。

*5:システムを使う人やモノ。 ユーザー。

*6:ユースケース図の吹き出しが、1 つのユースケース記述に相当する。

GraphQL サーバー on Kotlin ことはじめ (DroidKaigi 2019 に参加して GraphQL について学んだ)

DroidKaigi 2019 にスポンサー枠で参加しました。 弊社では Android アプリエンジニアをはじめとして各分野のソフトウェアエンジニアを募集しております。 人生を豊かにするプロダクトの開発や、大きな企業でのソフトウェア開発をいかに改善していくかといったところに興味がある方はぜひお声がけください! いっしょにやっていきましょう!

f:id:nobuoka:20190210163903j:plain
DroidKaigi 2019

それはともかく DroidKaigi、様々なセッションがあって素晴らしかったですね。 運営、スピーカー、スポンサー、そして参加者の皆様、ありがとうございました。

私個人としては、GraphQL についての知見を得られたのが大きな収穫でした。

本記事について

本記事は、GraphQL サーバーを Kotlin で立てるにあたって、仕組みを学んだ軌跡を残すものです。 GraphQL 自体の初心者が、Kotlin で GraphQL サーバーを実装する際には参考になると思います。 本番環境で使える技術とかを紹介しているわけではないのでご注意ください。

DroidKaigi 2019 と GraphQL と私

GraphQL についてはこれまでも雰囲気では知っていたのですが、はてなid:takuji31 さんと id:funnelbit さんの発表で改めて GraphQL の良さを感じました。

ところで、上記の発表では主にクライアントサイド視点での GraphQL の話が主で、サーバーサイドの実装がどうなるのかが非常に気になるところです。 クエリのパースから実際のデータの取得処理まで、素朴に実装しようとすると難しそうに感じます。 懇親会のときに id:takuji31 さんと id:funnelbit さんに聞いてみたところ、「サーバーサイドの実装者ではないのでそこまで詳しくはないが、言語ごとにいい感じのライブラリがあるから、割と何とかなるっぽい」 とのことでした。

ということで GraphQL の Kotlin サーバーに詳しい人に教えを請いたい〜、と思っていたら shiraji さんが導いてくれて gfx さんや taka さんたちから知見を吸わせてもらえました。 神か! (ありがとうございました。)

知見

自分で動かして試してみる

上記のようなページを見ることでなんとなく Kotlin での GraphQL サーバーサイド実装をどうすれば良いのかが見えてきましたが、Spring Boot を使った上記の例では便利ライブラリが詳細を隠ぺいしてくれているので細かな理解ができませんでした。 というわけで自分でも手を動かして試してみます。

graphql-java を試す

graphql-java は GraphQL のスキーマやクエリのパース、バリデーション、実行といった機能を提供するものです。 HTTP のインターフェイスは提供しません。 Kotlin で GraphQL の挙動を試したい場合は、まずは graphql-java を生で触るのが良いでしょう。 下記のドキュメントを見ながら進めると良いと思います。

外部 IO 無しで簡単に試せるので、最初のとっかかりとしては非常にやりやすいと思います。 私は下記のようなコードを書いて試していました。

まだリソース間に関連がある場合などは試せていません。

HTTP インターフェイスとつなぎこむ

GraphQL のサーバー・クライアント間の通信プロトコルについては、(おそらく) 仕様上は何も定められていなさそうです。 公式サイトに、HTTP での通信についてベストプラクティスとして書かれています。

これに従ってサーバーサイドの HTTP のエンドポイントを実装し、graphql-java とつなぎこむ、という感じにすれば良さそうです。 もともとの GraphQL のクライアント・サーバー間のやり取りがシンプルなので、HTTP のエンドポイントとしての実装もシンプルになります。

細かなエラーハンドリングなどはできていませんが、Ktor で実装すると以下のような感じになりました。

GraphiQL を使ってみる

ここまでくると自分のサーバーで GraphiQL も提供したいですよね?

GraphiQL の JS ライブラリは、npm パッケージとして配信されています。

自分で HTML ファイルや CSS ファイル、JS ファイルをビルドすることもできますが、既にビルドされている JS ファイルを使って簡単に GraphiQL のエンドポイントを生成することもできます。 使い方の例は GraphiQL のリポジトリに含まれています。

上記 HTML ファイルを自分のサーバーから配信すれば、GraphiQL が提供されます。 ただし、依存するリソースである graphiql.css と graphiql.js をどうにかする必要があります。 (上記の HTML そのままの場合は、これらのファイルも同じサーバーから配信する必要があります。)

今回は手軽に試したかったので CDN で配信されているものを使うようにしました。 下記 CDN で graphiql.css と graphiql.js が配信されています。

これを使うように書き換えた HTML をサーバーから配信すると、GraphiQL を使えるようになります。


おわり

というわけで初心者が GraphQL を学んでいる軌跡でした。 ここに書かれていない知見等ありましたら是非教えてくださいませ〜〜。

CloudFront のプライベートコンテンツ配信を試した (Kotlin で signed cookie を生成する)

  • AWSCDN である CloudFront でプライベートコンテンツを配信する方法として、signed URL を用いる方法と、signed cookie を用いる方法がある。
  • 本記事では、signed cookie の生成処理を Kotlin (Java) で行う方法を説明する。

事前準備 (CloudFront 側の準備)

  • アクセスする側の挙動を確認するために、試験的に CloudFront 側も準備したのでその手順を書いておく。 (AWS コンソール上で操作を行った。)
  • 内容は次の 2 つ。

CloudFront のディストリビューションを用意

コンテンツへのアクセスを制限する

キーペアの作成と CloudFront への登録

CloudFront にキーペアを登録する方法としては、ローカルマシン上で作成して公開鍵をアップロードするか、AWS コンソール上で作成して秘密鍵をダウンロードするかのどちらかとのこと。 今回はローカルマシン上で作成した。 コマンドは上記参考ページに載っている。

# 秘密鍵の作成
openssl genrsa -out private_key.pem 4096

# 公開鍵の作成
openssl rsa -pubout -in private_key.pem -out public_key.pem

# Java で使う用に秘密鍵を DER 形式に変換
openssl pkcs8 -topk8 -nocrypt -in private_key.pem -inform PEM -out cloud_front_key.der -outform DER

アカウントメニューから 「セキュリティ認証情報」 画面に行くと 「CloudFront のキーペア」 という項目があるので、ここで公開鍵をアップロードする。 登録された鍵のアクセスキー ID と、上のコマンドで作成された DER 形式の鍵が後で必要になる。

信頼された署名者をディストリビューションに追加する

上で登録した鍵を使って signed cookie を生成してプライベートコンテンツにアクセスできるように、AWS アカウントを信頼された署名者として CloudFront のディストリビューションに登録する。

(ちなみにディストリビューションを作成した AWS アカウント以外のアカウントも登録できるらしい。 今回は自分自身を登録する。)

この段階で、signed cookie や signed url を使わないアクセスはエラーになるようになる。

Signed cookie を生成してプライベートコンテンツにアクセスする (Kotlin)

上で生成された DER 形式の秘密鍵とアクセスキーを使うことで、下記のようなコードでプライベートコンテンツにアクセスできる。 (下記では Java SE 11 から新たに追加された API (java.net.http モジュール) を使用しているので、Java SE 11 でなければ動かない。)

import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
import java.nio.file.Files
import java.nio.file.Paths
import java.security.KeyFactory
import java.security.PrivateKey
import java.security.Signature
import java.security.spec.PKCS8EncodedKeySpec
import java.time.Instant
import java.time.Period
import java.util.*

// CloudFront のドメイン名
private val cloudFrontDomainName = "xxxxx.cloudfront.net"
// AWS コンソール上に表示されている CloudFront のキーペアのアクセス ID
private const val cloudFrontKeyPairId = "xxxx"
// 自分で生成した DER 形式の秘密鍵
private const val cloudFrontKeyPairFile = "cloud_front_key.der"

fun main() {
    // ポリシーステートメントを用意。
    val endingDate = Instant.now().plus(Period.ofDays(1))
    val cloudFrontPolicyStatement = """
        {
           "Statement": [
              {
                 "Resource": "http://$cloudFrontDomainName/",
                 "Condition": {
                    "DateLessThan": { "AWS:EpochTime": ${endingDate.epochSecond} }
                 }
              }
           ]
        }
    """.trimIndent()
    // CloudFront に登録された鍵の情報を用意。
    val keyFactory = KeyFactory.getInstance("RSA")
        // すべての Java 実装で RSA がサポートされる。 Java SE 8 のドキュメントで確認。
    val cloudFrontKeySpec = PKCS8EncodedKeySpec(Files.readAllBytes(Paths.get(cloudFrontKeyPairFile)))
    val cloudFrontPrivateKey = keyFactory.generatePrivate(cloudFrontKeySpec)
    val cloudFrontKey = CloudFrontKey(cloudFrontKeyPairId, cloudFrontPrivateKey)
    // Signed cookie の値を生成。
    val cloudFrontCookieValue = CloudFrontSignedCookieValue.create(cloudFrontPolicyStatement, cloudFrontKey)

    // Signed cookie を使用してプライベートコンテンツにアクセス。
    val targetUri = URI.create("http://$cloudFrontDomainName/")
    val response = request(targetUri, HttpResponse.BodyHandlers.ofString(), cloudFrontCookieValue)
    println(response.body())
}

fun <T> request(
    uri: URI,
    responseBodyHandler: HttpResponse.BodyHandler<T>,
    cloudFrontCookieValue: CloudFrontSignedCookieValue
): HttpResponse<T> {
    val client = HttpClient.newBuilder()
        .build()
    val request = HttpRequest.newBuilder()
        .uri(uri)
        .headers(
            "Cookie", "CloudFront-Policy=${cloudFrontCookieValue.policy}",
            "Cookie", "CloudFront-Signature=${cloudFrontCookieValue.signature}",
            "Cookie", "CloudFront-Key-Pair-Id=${cloudFrontCookieValue.keyPairId}"
        )
        .build()
    return client.send(request, responseBodyHandler)
}

data class CloudFrontSignedCookieValue(
    val policy: String,
    val signature: String,
    val keyPairId: String
) {
    companion object {
        fun create(policyStatement: String, key: CloudFrontKey): CloudFrontSignedCookieValue {
            val canonicalPolicyStatement = Regex("\\s").replace(policyStatement, "")
            val encodedPolicyStatement = encodeForCookieValue(canonicalPolicyStatement.toByteArray())
            val encodedSignature = sign(canonicalPolicyStatement.toByteArray(), key.privateKey)
            return CloudFrontSignedCookieValue(encodedPolicyStatement, encodedSignature, key.keyPairId)
        }

        private fun sign(base: ByteArray, privateKey: PrivateKey): String {
            val signatureInstance = Signature.getInstance("SHA1withRSA")
                // すべての Java 実装で SHA1withRSA がサポートされる。 Java SE 8 のドキュメントで確認。
            signatureInstance.initSign(privateKey)
            signatureInstance.update(base)
            val signature = signatureInstance.sign()
            return encodeForCookieValue(signature)
        }

        private fun encodeForCookieValue(value: ByteArray) =
            Base64.getEncoder().encodeToString(value)
                .replace('+', '-')
                .replace('=', '_')
                .replace('/', '~')
    }
}

data class CloudFrontKey(
    val keyPairId: String,
    val privateKey: PrivateKey
)

AWS のドキュメントには 『エンコーダが正常に機能するように、Bouncy Castle の Java 用暗号 API の jar をプロジェクトに追加してから Bouncy Castle プロバイダを追加します。』 と書かれているが、Java SE に含まれるエンコーダだけで問題なく署名できるはず。

AWS SDK for Java

AWS SDK for JavaCloudFrontCookieSigner というクラスがあり、これを使うことで signed cookie を生成することも可能。 通常はこれを使うのが良さそう。

JAX-RS のリソースから送出された例外の扱い

JAX-RS のリソースのメソッドから例外が送出された場合の挙動についてちゃんと把握していなかったので調べた。

前提知識 : JAX-RS について

JAX-RS は、RESTful Web API を提供するための JavaAPI。 もともとは Java EE の一部。 現在は Eclipse Foundation に移管されて EE4J プロジェクトの一部 (Jakarta EE の一部) となっている。

JAX-RS のリソースから送出された例外の扱いについて

今回は JAX-RS 2.1 の仕様を参照する。

3.3.4 節に書かれている。

3.3.4 Exceptions

A resource method, sub-resource method or sub-resource locator may throw any checked or unchecked exception. An implementation MUST catch all exceptions and process them in the following order:

  • 1. Instances of WebApplicationException and its subclasses MUST be mapped to a response as follows. If the response property of the exception does not contain an entity and an exception mapping provider (see Section 4.4) is available for WebApplicationException or the corresponding subclass, an implementation MUST use the provider to create a new Response instance, otherwise the response property is used directly. The resulting Response instance is then processed according to Section 3.3.3.
  • 2. If an exception mapping provider (see Section 4.4) is available for the exception or one of its superclasses, an implementation MUST use the provider whose generic type is the nearest superclass of the exception to create a Response instance that is then processed according to Section 3.3.3. If the exception mapping provider throws an exception while creating a Response then return a server error (status code 500) response to the client.
  • 3. Unchecked exceptions and errors that have not been mapped MUST be re-thrown and allowed to propagate to the underlying container.
  • 4. Checked exceptions and throwables that have not been mapped and cannot be thrown directly MUST be wrapped in a container-specific exception that is then thrown and allowed to propagate to the underlying container. Servlet-based implementations MUST use ServletException as the wrapper. JAX-WS Provider-based implementations MUST use WebServiceException as the wrapper.

Note: Items 3 and 4 allow existing container facilities (e.g. a Servlet filter or error pages) to be used to handle the error if desired.

Exception mapping provider による変換

上記の説明の中に、「exception mapping provider」 というものが出てくる。 詳細は 4.4 節に書かれている。

4.4 Exception Mapping Providers

Exception mapping providers map a checked or runtime exception to an instance of Response. An exception mapping provider implements the ExceptionMapper<T> interface and may be annotated with @Provider for automatic discovery.

When a resource class or provider method throws an exception for which there is an exception mapping provider, the matching provider is used to obtain a Response instance. The resulting Response is processed as if a web resource method had returned the Response, see Section 3.3.3. In particular, a mapped Response MUST be processed using the ContainerResponse filter chain defined in Chapter 6.

When choosing an exception mapping provider to map an exception, an implementation MUST use the provider whose generic type is the nearest superclass of the exception. If two or more exception providers are applicable, the one with the highest priority MUST be chosen as described in Section 4.1.3.

To avoid a potentially infinite loop, a single exception mapper must be used during the processing of a request and its corresponding response. JAX-RS implementations MUST NOT attempt to map exceptions thrown while processing a response previously mapped from an exception. Instead, this exception MUST be processed as described in steps 3 and 4 in Section 3.3.4.

すなわち、リソースメソッドなどから送出された例外を変換して Response オブジェクトを生成する機能を提供するものである。

Exception mapping provider は複数登録することができる。 例外が送出されると、対応する exception mapping provider が選択され、Response が生成されて、それがレスポンスとして使用される。 対応する exception mapping provider がない場合、非検査例外だとそのまま再送出されるし、検査例外だとコンテナに応じた非検査例外にラップされて送出されるとのこと。

WebApplicationException について

基本的には上に書いた通りの例外ハンドリングがなされるが、リソースメソッドなどから送出された例外が WebApplicationException の場合は、exception mapping provider を通らないことがある。 3.3.4 節の箇条書きの 1 つめがそれである。

WebApplicationExceptionresponse プロパティがエンティティを持っていない場合で、対応する exception mapping provider が存在する場合には exception mapping provider による Response 生成がなされる。 それ以外の場合は WebApplicationExceptionresponse プロパティの値がそのままレスポンスとして使われる。

サンプルコード

JAX-RS の参照実装である Jersey を使った Kotlin のサンプルコード。

import org.glassfish.jersey.jdkhttp.JdkHttpServerFactory
import org.glassfish.jersey.server.ResourceConfig
import javax.ws.rs.GET
import javax.ws.rs.Path
import javax.ws.rs.WebApplicationException
import javax.ws.rs.core.Response
import javax.ws.rs.core.UriBuilder
import javax.ws.rs.ext.ExceptionMapper

@Path("exceptions")
class MyResource {
    @GET
    @Path("re")
    fun re() {
        // RuntimeException なので対応する MyExceptionMapper で変換される。
        throw RuntimeException("RuntimeException")
    }

    @GET
    @Path("wae-without-entity")
    fun waeWithoutEntity() {
        // エンティティのない WebApplicationException なので MyExceptionMapper で変換される。
        throw WebApplicationException("WebApplicationException with response not containing entity",
            Response.status(Response.Status.BAD_REQUEST).build())
    }

    @GET
    @Path("wae-with-entity")
    fun waeWithEntity() {
        // エンティティを持つ WebApplicationException なので MyExceptionMapper で変換されない。
        throw WebApplicationException("WebApplicationException with response containing entity",
            Response.status(Response.Status.BAD_REQUEST).entity("Entity").build())
    }
}

class MyExceptionMapper : ExceptionMapper<Throwable> {
    override fun toResponse(exception: Throwable): Response {
        println(exception)
        return Response.status(Response.Status.BAD_REQUEST).entity("$exception").build()
    }
}

fun main() {
    val baseUri = UriBuilder.fromUri("http://localhost/").port(9998).build()
    val config = ResourceConfig(MyResource::class.java, MyExceptionMapper::class.java)
    JdkHttpServerFactory.createHttpServer(baseUri, config)
}

ビルドコードは以下のような感じで動くはず。 (Gradle。)

plugins {
    id 'org.jetbrains.kotlin.jvm' version '1.3.11'
}

repositories {
    mavenCentral()
}

dependencies {
    compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8"

    compileOnly "javax.ws.rs:javax.ws.rs-api:2.1"

    compile "org.glassfish.jersey.containers:jersey-container-jdk-http:2.27"
    compile "org.glassfish.jersey.inject:jersey-hk2:2.27"

    // JDK 9 以降は下記が必要。
    // See : https://stackoverflow.com/questions/43574426/how-to-resolve-java-lang-noclassdeffounderror-javax-xml-bind-jaxbexception-in-j
    compile "javax.xml.bind:jaxb-api:2.2.11"
    compile "javax.activation:activation:1.1.1"
}

compileKotlin {
    kotlinOptions.jvmTarget = "1.8"
}
compileTestKotlin {
    kotlinOptions.jvmTarget = "1.8"
}