B-Teck!

お仕事からゲームまで幅広く

【Kotlin】JerseyをKotlinで動かす その2 パラメータの取扱編

前回の記事 beatdjam.hatenablog.com

前回はJerseyの環境づくりと起動方法、Kotlinへの変換について書きました。
今回は各種リクエストパラメータをどうやって取り扱うかを説明します。

準備

今回の記事の内容を扱うにあたり、新しいResouceクラスを作成しましょう。
下記の形でParametersResource.ktを作成します。
このクラスには @Path("parameters") アノテーションがついているので、
http://localhost:8080/myapp/parameters に対応するクラスとなります。
クラスに@Pathがついている状態でメソッドにも@Pathがついているとき、
クラスのpath/メソッドのpathに対応します。

package com.example

import com.example.form.BeanParamSample
import java.nio.charset.Charset
import javax.validation.Valid
import javax.ws.rs.*
import javax.ws.rs.core.MediaType

/**
 * Root resource (exposed at "myresource" path)
 */
@Path("parameters")
class ParametersResource {

}

HTTPリクエストのクエリ

HTTPリクエストのクエリに含まれている値を利用するには、@QueryParam を使います。
例)
@QueryParam("parameter") parameter: String?

http://localhost:8080/myapp/parameters/queryparam?parameter=hoge

    @GET
    @Path("/queryparam")
    @Produces(MediaType.TEXT_PLAIN)
    fun queryParam(@QueryParam("parameter") parameter: String?): String {
        return if (parameter.isNullOrEmpty()) {
            "Parameter is Empty."
        } else parameter
    }

クエリをdata classに対応させる

前述したQueryParamをdata classのフィールドに定義します。
これを、対応させたいResourceクラスで@BeanParam アノテーションをつけて記述すると、
リクエスト時に自動でdata classに格納してくれるようになります。
後述する @PathParam@FormParamでも利用できるので覚えておきましょう。

また、JerseyではJavaEEのBean Validationという仕組みが利用できます。
@field:NotNull@field:NotEmpty などのバリデーション用のアノテーションを設定すると、
正しくないリクエストに対して、自動で400 Bad Requestを返却してくれるものです。

data class BeanParamSample (
        @QueryParam("parameter1")
        val parameter1: String?,

        @QueryParam("parameter2")
        @field:NotNull
        val parameter2: String?,

        @QueryParam("parameter3")
        @field:NotEmpty
        val parameter3: String?
)

 

@GET
@Path("/beanparam")
@Produces(MediaType.TEXT_PLAIN)
fun beanparam(@BeanParam @Valid parameter: BeanParamSample): String {
    return buildString {
        appendln(
                if (parameter.parameter1.isNullOrEmpty()) {
                    "Parameter is Empty."
                } else parameter.parameter1
        )
        appendln(parameter.parameter2)
        appendln(parameter.parameter3)
    }
}

Pathに含まれる値

HTTPリクエストのパスに含まれる値を利用するためには @PathParam アノテーションを利用します。
また、@Path の指定時にパラメータとしたい箇所を{}でくくる必要があります。
例)
@Path("/pathparam/{parameter1}
@PathParam("parameter") parameter: String?

http://localhost:8080/myapp/parameters/pathparam/hogehoge

@GET
@Path("/pathparam/{parameter}")
@Produces(MediaType.TEXT_PLAIN)
fun pathParam(@PathParam("parameter") parameter: String?): String {
    return if (parameter.isNullOrEmpty()) {
        "Parameter is Empty."
    } else parameter
}

リクエストパスからパターンに対応した値を取り出す

パターンに対応する文字列から複数を取り出すような事もできます。
{parameter1}.{parameter2} のようなパターンを設定すると、
0000.1234 のようなパスの00001234 をそれぞれ取得できます。
例)
@Path("/pathparam/{parameter1}.{parameter2}")
@PathParam("parameter1") parameter1: String?
@PathParam("parameter2") parameter2: String?

http://localhost:8080/myapp/parameters/pathparam/1234.5678

@GET
@Path("/pathparam/{parameter1}.{parameter2}")
@Produces(MediaType.TEXT_PLAIN)
fun pathParamSplit(
        @PathParam("parameter1") parameter1: String?,
        @PathParam("parameter2") parameter2: String?
): String {
    return (parameter1 ?: "") + "." +  (parameter2 ?: "")
}

リクエストパスから正規表現に対応した値を取り出す

パターンは正規表現で記述することもできます。
例)
@Path("/pathparam/regex/{regexMatched:.*}")
@PathParam("regexMatched") regexMatched: String?

http://localhost:8080/myapp/parameters/pathparam/regex/hogefuga/hoge

@GET
@Path("/pathparam/regex/{regexMatched:.*}")
@Produces(MediaType.TEXT_PLAIN)
fun pathParamRegex(@PathParam("regexMatched") regexMatched: String?): String {
    return if (regexMatched.isNullOrEmpty()) {
        "Not matched by regex."
    } else regexMatched
}

Formから送信されたリクエストを取得する

application/x-www-form-urlencoded 形式で送られたリクエストのパラメータは、
@FormParamで取り出すことができます。
例)
@FormParam("form1") form1param : String?
@FormParam("form2") form2param : String?

POSTなのでブラウザから直接たたけないため、CLI上で下記を叩いてください。
curl -d "form1=form1text" -d "form2=form2text" http://localhost:8080/myapp/parameters/formparam

@POST
@Path("/formparam")
@Produces(MediaType.TEXT_PLAIN)
fun formParam(
        @FormParam("form1") form1param : String?,
        @FormParam("form2") form2param : String?
): String {
    return buildString {
        appendln("form1 : $form1param")
        appendln("form2 : $form2param")
    }
}

ファイルアップロードに対応する

ほぼ以前書いた下記記事のままです。
【Kotlin/Java】Jersey2でファイルアップロードを扱う - B-Teck!

POSTなのでブラウザから直接たたけないため、CLI上で下記を叩いてください。
curl --header "Content-Type:application/octet-stream" -d "{"hoge": "fuga"}" http://localhost:8080/myapp/parameters/upload

@POST
@Path("/upload")
@Consumes(MediaType.APPLICATION_OCTET_STREAM)
@Produces(MediaType.TEXT_PLAIN)
fun upload(input : ByteArray): String {
    return input.toString(Charset.defaultCharset())
}

ここまでの作業

下記のタグまでが今回の記事の作業分です。
https://github.com/beatdjam/Jersey-On-Kotlin-Sample/tree/chapter2

作成したファイル
https://github.com/beatdjam/Jersey-On-Kotlin-Sample/blob/chapter2/src/main/kotlin/com/example/ParametersResource.kt
https://github.com/beatdjam/Jersey-On-Kotlin-Sample/blob/chapter2/src/main/kotlin/com/example/form/BeanParamSample.kt

【Kotlin】JerseyをKotlinで動かす その1 導入編

Jersey+KotlinでAPIサーバーを構築する方法と、実装例をご紹介していこうと思います。
とりあえず第一回目としてJersey公式の雛形からProjectを作成し、Kotlin化するところまでを書きます。

基本的な導入の手順は公式の Chapter 1. Getting Started を参考に書いていきます。

Jerseyの雛形を作る

  1. mavenのインストール
  2. 任意のフォルダの配下で下記コマンドを実行する
mvn archetype:generate -DarchetypeArtifactId=jersey-quickstart-grizzly2 \
-DarchetypeGroupId=org.glassfish.jersey.archetypes -DinteractiveMode=false \
-DgroupId=com.example -DartifactId=simple-service -Dpackage=com.example \
-DarchetypeVersion=2.28

これを実行するとフォルダ内に下記構成のディレクトリが生成されます。

simple-service
├── pom.xml
└── src
    ├── main
    │   └── java
    │       └── com
    │           └── example
    │               ├── Main.java
    │               └── MyResource.java
    └── test
        └── java
            └── com
                └── example
                    └── MyResourceTest.java

Kotlin化する

Add gitignore · beatdjam/Jersey-On-Kotlin-Sample@8296a0f · GitHub

pomの記述変更

Convert Java file to Kolin · beatdjam/Jersey-On-Kotlin-Sample@a64a0d7 · GitHub

主にやっていることは

  • dependenciesに下記を追記
    • kotlin-stdlib-jdk8
    • kotlin-test
  • buildに下記を追記
    • sourceDirectory
    • testSourceDirectory
  • pluginに下記を追記
    • kotlin-maven-plugin

ファイルのKotlin化

下記構成になるようファイルをKotlin化する。
IntelliJの機能でJavaからKotlinに変換したあと、記述をKotlinっぽくしたりした。
あと、 HttpServer.stop() がdeprecatedだったので HttpServer.shutdownNow() に書き換えたりした。

simple-service
├── pom.xml
└── src
    ├── main
    │   └── kotlin
    │       └── com
    │           └── example
    │               ├── Main.kt
    │               └── MyResource.kt
    └── test
        └── kotlin
            └── com
                └── example
                    └── MyResourceTest.kt

Main.java → Main.kt

Main.java
Main.kt

MyResource.java → MyResource.kt

MyResource.java
MyResource.kt

MyResourceTest.java → MyResourceTest.kt

MyResourceTest.java
MyResourceTest.kt

サーバーの起動

Jerseyの雛形にはGrizzlyという軽量なHttpServerが付属していて、下記コマンドで起動できます。

mvn clean test;mvn exec:java;

無事起動できたら、このような表示がCLI上に表示されます。

Jersey app started with WADL available at http://localhost:8080/myapp/ application.wadl
Hit enter to stop it...

http://localhost:8080/myapp/myresourceにアクセスして、 Got it! と表示されれば完了です。

Grizzly上ではなく通常のJavaEEアプリケーションとして動かす場合

通常のJavaEEアプリケーションとしてプロジェクトを作成する場合は、
1.4. Creating a JavaEE Web Application を参考にしてください。

ここまでの作業

下記のタグまでが今回の記事の作業分です。
https://github.com/beatdjam/Jersey-On-Kotlin-Sample/tree/chapter1

【Kotlin】Kotlinで競プロする

PaizaやってたときはPHP+JS、AtCoderでは主にJavaで解いてたんですが、
ここ半年の案件でずっとKotlinを書いていたらJavaが書けなくなってしまいました。
なので、KotlinでAtCoderを解く準備をするなどしています。

注意点として、AtCoderのKotlinは記事執筆時点で1.0.0なので、新し目の機能が使えなかったりしますが…。

そんな感じで、Kotlinで標準入出力を行う方法などをご紹介します。

Scannerとprintln()で入出力する

一番基本的な入出力のとり方です。
Kotlinで書くとこうなります。

import java.util.Scanner
fun main(args: Array<String>) {
    // Scannerを使う
    val sc = Scanner(System.`in`)
    // 文字列で取得
    println(sc.next())
    // 数値で取得
    println(sc.nextInt())
}

ただ、Scannerは結構遅いので、ある程度問題解いてくると不満が出てくると思います。
ので、自分は次に書くような形で書いています。

readLine()とPrintWriterで入出力する

Java競技プログラミングメモ - Qiita
この記事にもあるようにScannerはあまり早くないです。
また、都度println()を行うことでIOに割かれる時間が増えて、TLEの原因になります。

なので、内部的にBufferedReaderを使用しているreadLine()を使って行ごとに入力を読み込み、
PrintWriterで最後にまとめて出力をflush()する形をとっています。

import java.io.PrintWriter

fun main(args : Array<String>) {
    // PrintWriterを使う
    val pw = PrintWriter(System.out)
    // 文字列で取得
    pw.println(readLine())
    // 数値で取得
    pw.println(readLine()!!.toInt())
    // まとめて出力
    pw.flush()
}

その他Kotlinで書いて嬉しいところ

あと、Kotlinでは分解宣言が使えるので、1行入力の複数の値を一発で取るのがすごい楽です。

import java.io.PrintWriter

fun main(args : Array<String>) {
    val pw = PrintWriter(System.out)
    // A, B, C の形で入力された文字列を一発で各変数に入れる
    val (a, b, c) = readLine()!!.split(" ")
    pw.println(a)
    pw.println(b)
    pw.println(c)
    // X, Y, Z の形で入力された数字を一発で各変数に入れる
    val (x, y, z) = readLine()!!.split(" ").map{ it.toInt() }
    pw.println(x)
    pw.println(y)
    pw.println(z)
    pw.flush()
}

Collectionの取り回しもよく、ListとかMapを取り回すような問題では見通しよく書けるという利点もありました。

普段使ってる解答用テンプレ

これらを踏まえて、こういうテンプレを使って問題を解いています。
ちょっとKotlinで解いてみようかな?ってときに参考になればと思います。

import java.io.PrintWriter
val pw = PrintWriter(System.out)
fun main(args : Array<String>) {
    func()
    pw.flush()
}

fun func() {
    // 回答をここに書く
}

// 入力取得
fun next() = readLine()!!
fun nextInt() = next().toInt()
fun nextLong() = next().toLong()
fun nextDouble() = next().toDouble()
fun listOfString() = next().split(" ")
fun listOfInt() =listOfString().map { it.toInt() }
fun listOfLong() =listOfString().map { it.toLong() }
fun listOfDouble() =listOfString().map { it.toDouble() }

// 約数のList
fun divisor(value : Long) : List<Long> {
    val max = Math.sqrt(value.toDouble()).toLong()
    return (1..max)
        .filter { value % it == 0L }
        .map { listOf(it, value / it) }
        .flatten()
        .sorted()
}

// 範囲内の素数を取得
// fromだけ指定すると戻り値の個数で素数判定ができる
fun prime(from : Long, to : Long = from) : List<Long> {
    return (from..to).filter { i ->
        val max = Math.sqrt(i.toDouble()).toLong()
        (2..max).all { j ->  i % j != 0L }
    }
}

// 素因数分解
fun decom(value : Long) : List<Long>{
    if (value == 1L) return listOf(1)
    val max = Math.sqrt(value.toDouble()).toLong()
    return prime(2, max).filter { value % it == 0L }
}

// 最大公約数
fun gcd(a : Long, b : Long) : Long {
    return if (a % b == 0L) b else gcd(b, a % b)
}

// 文字列を入れ替え
fun swap(base : String, a : String, b : String) : String {
    return base.map {
        when (it) {
            a.toCharArray()[0] -> b
            b.toCharArray()[0] -> a
            else -> it.toString()
        }
    }.joinToString()
}

fun println(value : Any) {
    pw.println(value)
}