Spark Framework使い方メモ

ちょっとした Web アプリケーションを作るのに便利な Spark の使い方メモ。

Apache Spark ではない（紛らわしい...）。

#インストール
```groovy:build.gradle
dependencies {
    compile 'com.sparkjava:spark-core:2.0.0'
}
```

#Hello World
```java:Main.java
package sample.spark;

import static spark.Spark.*;

public class Main {
    
    public static void main(String[] args) {
        get("/hello", (request, response) -> "<h1>Hello Spark!!</h1>");
    }
}
```

実行したら、 Web ブラウザを立ち上げて `http://localhost:4567/hello` にアクセスする。

![spark.JPG](https://qiita-image-store.s3.amazonaws.com/0/28302/1334942d-5eb8-bd7b-1af1-c61793305796.jpeg)

#ポートを変更する
```java:
package sample.spark;

import static spark.Spark.*;

public class Main {
    
    public static void main(String[] args) {
        setPort(7654);

        get("/hello", (request, response) -> "<h1>Hello Spark!!</h1>");
    }
}
```

- `Spark.setPort()` でポートを変更できる。
- デフォルトは `4567` ポート。

#サーバーを停止する
```java:
package sample.spark;

import static spark.Spark.*;

public class Main {
    
    public static void main(String[] args) {
        get("/stop", (request, response) -> {
            stop();
            return null;
        });
    }
}
```

- `Spart.stop()` メソッドでサーバーを停止できる。
- `stop()` メソッドを実行してもすぐには停止せず、少ししてから `InterruptedException` がスローされて終了する。

#ルーティングの定義
##３つの要素
```java:
get("/hello", (request, response) -> "Hello Spark!!");
```

- ルーティングの定義は、以下の３つの要素で構成されている。
    - HTTP メソッド（`get`, `post`, `put`, `delete`, `head`, `trace`, `options`, `connect`）
    - パス（`/hello`, `/stop`）
    - コールバック（`(request, response) -> "Hello Spark!!"`）

##パスパラメータ
```java:
package sample.spark;

import static spark.Spark.*;

public class Main {
    
    public static void main(String[] args) {
        get("/hello/:param", (request, response) -> "param = " + request.params("param"));
    }
}
```

![spark.JPG](https://qiita-image-store.s3.amazonaws.com/0/28302/74679779-4aa9-eabd-dfcd-518a466abf9c.jpeg)

- `:<名前>` という形でパスにパラメータを定義することができる。
- パラメータは、リクエストの `params()` メソッドで取得できる。

##パスにワイルドカードを指定する
```java:
package sample.spark;

import static spark.Spark.*;

public class Main {
    
    public static void main(String[] args) {
        get("/hello/*/sample/*", (request, response) -> {
            String[] splat = request.splat();
            
            String text = "splat().length = " + splat.length + "<br>"
                         + "splat()[0] = " + splat[0] + "<br>"
                         + "splat()[1] = " + splat[1];
            
            return text;
        });
    }
}
```

ブラウザを立ち上げて、 `http://localhost:4567/hello/hoge/sample/test` にアクセスする。

![spark.JPG](https://qiita-image-store.s3.amazonaws.com/0/28302/a51ad2e6-bd61-6f2b-f6d1-73699f482ff9.jpeg)

- ワイルドカード（`*`）を使ってパスを定義できる。
- ワイルドカードにあたる部分のパスは、リクエストの `splat()` メソッドで `String` の配列として取得できる。

#リクエスト
```java:
request.body();               // リクエストボディを取得する（String）
request.cookies();            // クッキーを取得する（Map<String, String>）
request.contentLength();      // リクエストボディの長さを取得する（int）
request.contentType();        // リクエストボディのコンテツタイプを取得する（String）
request.headers();            // HTTP ヘッダーの一覧を取得する（Set<String>）
request.headers("BAR");       // HTTP ヘッダーの "BAR" の値を取得する（String）
request.attributes();         // attribute の一覧を取得する（Set<String>）
request.attribute("foo");     // attribute の "foo" の値を取得する（String）
request.attribute("A", "V");  // attribute の "A" に "V" を設定する
request.host();               // サーバーのホスト名（localhost:4567）を取得する（String）
request.ip();                 // クライアントの IP アドレスを取得する（String）
request.pathInfo();           // リクエストのあったパスを取得する（String）
request.params("foo");        // パスパラメータの "foo" の値を取得する（String）
request.params();             // 全てのパスパラメータを Map で取得する（Map<String, String>）
request.port();               // サーバーのポートを取得する（int）
request.queryMap();           // クエリマップを取得する（QueryParamsMap）
request.queryMap("foo");      // クエリマップの "foo" を取得する（QueryParamsMap）
request.queryParams("FOO");   // クエリパラメータの "FOO" の値を取得する（String）
request.queryParams();        // クエリパラメータの一覧を取得する（Set<String>）
request.raw();                // 生の HttpServletRequest を取得する（HttpServletRequest）
request.requestMethod();      // HTTP リクエストメソッドを取得する（String）
request.scheme();             // URL のスキーム（http）を取得する（String）
request.session();            // セッション情報を取得する（spark.Session）
request.splat();              // パスのワイルドカード部分の情報を取得する（String[]）
request.url();                // リクエストされた URL 全体を取得する（String）
request.userAgent();          // ユーザーエージェントを取得する（String）
```

クエリマップについては後述。

#レスポンス
```java:
response.body("Hello");        // コンテンツに "Hello" をセットする
response.header("FOO", "bar"); // ヘッダーの "FOO" に "bar" をセットする
response.raw();                // 生の HttpServletResponse を取得する（HttpServletResponse）
response.redirect("/example"); // 指定した URL にリダイレクトさせる
response.status(401);          // ステータスコードに 401 を設定する
response.type("text/xml");     // コンテンツタイプを設定する
```

#クエリマップ
```java:
package sample.spark;

import static spark.Spark.*;
import spark.QueryParamsMap;

public class Main {
    
    public static void main(String[] args) {
        get("/hello", (request, response) -> {
            QueryParamsMap map = request.queryMap();
            
            String html = "user[name] = " + map.get("user", "name").value() + "<br>"
                         + "user[age] = " + map.get("user", "age").integerValue();
            
            return html;
        });
    }
}
```

ブラウザで `http://localhost:4567/hello?user[name]=hoge&user[age]=14` にアクセスする

![spark.JPG](https://qiita-image-store.s3.amazonaws.com/0/28302/291f0c81-46d3-52fd-700b-8a2f282717c0.jpeg)

- クエリマップとは、 `user[name]` のように、マップ形式で指定されたクエリパラメータのことを指している。
- `QueryParamsMap` を使うと、この形式のクエリパラメータに簡単にアクセスできる。

#クッキー
```java:
request.cookies();                              // 全てのクッキーを Map で取得する（Map<String, String>）
request.cookie("foo");                          // クッキーの "foo" の値を取得する（String）
response.cookie("foo", "bar");                  // "foo" という名前で "bar" という値をクッキーに設定する
response.cookie("foo", "bar", 3600);            // 有効期限（秒）付きでクッキーを設定する
response.cookie("foo", "bar", 3600, true);      // セキュア属性を true にする（https の時にだけクッキーを送信する）
response.removeCookie("foo");                   // "foo" をクッキーから削除する
```

#セッション
```java:
request.session(true)                            // HttpServletRequest.getSession(true) と同じ
request.session().attribute("user")              // "user" 属性を取得する（T : 自動でキャストされる）
request.session().attribute("user", "foo")       // "user" 属性に "foo" を設定する
request.session().removeAttribute("user")        // "user" 属性を削除する
request.session().attributes()                   // 全ての属性を取得する（Set<String>）
request.session().id()                           // セッションIDを取得する（String）
request.session().isNew()                        // セッションが新規に作成されたものかどうかを確認する（boolean）
request.session().raw()                          // 生の HttpSession を取得する
```

#リクエストの処理を直ちに中断する
```java:
package sample.spark;

import static spark.Spark.*;

public class Main {
    
    public static void main(String[] args) {
        get("/hello", (request, response) -> {
            System.out.println("実行される");
            halt();
            System.out.println("実行されない");
            
            return null;
        });
    }
}
```

```text:コンソール出力
実行される
```

- `halt()` メソッドを実行することで、そこで処理を中断できる。
- `halt(404)` や `halt(404, "File Not Found")` とすることで、ステータスコードとレスポンスボディを設定することもできる。

#リクエストの前後に処理を挟む
```java:
package sample.spark;

import static spark.Spark.*;

public class Main {
    
    public static void main(String[] args) {
        
        before((request, response) -> {
            System.out.println("before");
        });
        
        get("/hello", (request, response) -> {
            System.out.println("/hello");
            return "Hello Spark!!";
        });
        
        after((request, response) -> {
            System.out.println("after");
        });
    }
}
```

```text:コンソール出力
before
/hello
after
```

- `before()` と `after()` でリクエストの前後に処理を挟むことができる。
- `before()` で認証チェックをして、認証エラーの場合は `halt()` で処理を中断させる、といった実装ができる。

##処理を挟むパスを限定させる
```java:
package sample.spark;

import static spark.Spark.*;

public class Main {
    
    public static void main(String[] args) {
        
        before("/hoge", (request, response) -> {
            System.out.println("before hoge");
        });
        
        get("/hoge", (request, response) -> {
            System.out.println("/hoge");
            return "Hoge";
        });
        
        get("/fuga", (request, response) -> {
            System.out.println("/fuga");
            return "Fuga";
        });
        
        after("/fuga", (request, response) -> {
            System.out.println("after fuga");
        });
    }
}
```

```text:コンソール出力
before hoge
/hoge
/fuga
after fuga
```

- パスを指定して、そのパスのときだけ処理を挟むようにできる。
- ワイルドカードの指定も可能。

#例外発生時の処理を定義する
```java:
package sample.spark;

import static spark.Spark.*;

public class Main {
    
    public static void main(String[] args) {
        
        get("/null", (request, response) -> {
            throw new NullPointerException("Test NullpointerException");
        });
        
        exception(NullPointerException.class, (e, request, response) -> {
            response.status(200);
            response.body(e.getMessage());
        });
    }
}
```

![spark.JPG](https://qiita-image-store.s3.amazonaws.com/0/28302/6c1501a2-af5f-b709-8c0b-43262104053a.jpeg)

- `exception()` で、指定した例外が発生した場合の処理を定義できる。

#静的ファイルを公開する
##クラスパス上のファイルを公開する
```text:プロジェクト構成
`-src/main/java/
    |-sample/spark/
    |  `-Main.java
    `-webapp/
       `-index.html
```

```java:Main.java
package sample.spark;

import static spark.Spark.*;

public class Main {
    
    public static void main(String[] args) {
        staticFileLocation("/webapp");
        
        get("/hello", (request, response) -> "Hello Spark!!");
    }
}
```

```html:index.html
<html>
  <head>
    <title>Sample Spark Framework</title>
  </head>
  <body>
    <h1>Hello Spark!!</h1>
  </body>
</html>
```

ブラウザで `http://localhost:4567/` にアクセス。

![spark.JPG](https://qiita-image-store.s3.amazonaws.com/0/28302/a70e9bd7-0bd5-337a-b8ef-c5b52b0dba06.jpeg)

- `staticFileLocation()` で、クラスパス上の場所を指定して静的ファイルとして公開できる。

##クラスパス外のディレクトリを公開する
```java:
package sample.spark;

import static spark.Spark.*;

public class Main {
    
    public static void main(String[] args) {
        
        externalStaticFileLocation("F:/tmp/spark");
        
        get("/hello", (request, response) -> "Hello Spark!!");
    }
}
```

```html:F：/tmp/spark/index.html
<html>
  <head>
    <title>External Directory</title>
  </head>
  <body>
    <h1>F:\tmp\spark\index.html</h1>
  </body>
</html>
```

![spark.JPG](https://qiita-image-store.s3.amazonaws.com/0/28302/dcd82709-8692-ee2b-3665-740898cb5090.jpeg)

- `externalStaticFileLocation()` で、クラスパス外のローカルのディレクトリを指定して静的ファイルとして公開できる。

#レスポンスの形式を変換する
Jackson を使って、レスポンスを JSON 形式に変換する。

```java:
package sample.spark;

import static spark.Spark.*;

import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

import spark.ResponseTransformer;

import com.fasterxml.jackson.databind.ObjectMapper;

public class Main {
    
    public static void main(String[] args) {
        
        get("/json", (request, response) -> {
            Map<String, Object> map = new HashMap<>();
            map.put("str", "Hoge");
            map.put("bool", true);
            map.put("list", Arrays.asList(1, 2, 3, 4));
            
            return map;
        }, new MyJsonTranformer());
        
    }
    
    private static class MyJsonTranformer implements ResponseTransformer {
        
        private ObjectMapper mapper = new ObjectMapper();
        
        @Override
        public String render(Object model) throws Exception {
            return this.mapper.writeValueAsString(model);
        }
    }
}
```

![spark.JPG](https://qiita-image-store.s3.amazonaws.com/0/28302/55a6d525-46fd-82e3-5f87-d1ea304af7be.jpeg)

- `ResponseTransformer` を実装したクラスを作成し、ルーティングを定義する関数の最後の引数に渡すことで、レスポンスの形式を変換する処理を挟むことができる。

#ビューにテンプレートエンジンを使用する
Mustache をテンプレートエンジンとして使用して、ビューを表示させてみる。

##Mustache 用のプラグインを追加する
```groovy:build.gradle
dependencies {
    compile 'com.sparkjava:spark-template-mustache:1.0.0'
}
```

##実装
```text:フォルダ構成
`-src/main/
  |-java/sample/spark/
  |  `-Main.java
  `-resources/templates/
     `-hello.mustache
```

```java:
package sample.spark;

import static spark.Spark.*;

import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import spark.ModelAndView;
import spark.template.mustache.MustacheTemplateEngine;

public class Main {
    
    public static void main(String[] args) {
        
        get("/mustache", (request, response) -> {
            List<User> users = Arrays.asList(new User("sato", 19), new User("suzuki", 18), new User("tanaka", 20));
            
            Map<String, Object> model = new HashMap<>();
            model.put("users", users);
            
            return new ModelAndView(model, "hello.mustache");
        }, new MustacheTemplateEngine());
        
    }
    
    public static class User {
        private String name;
        private int age;
        
        public User(String name, int age) {
            this.name = name;
            this.age = age;
        }
        
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
        public int getAge() {
            return age;
        }
        public void setAge(int age) {
            this.age = age;
        }
    }
}
```

```html:hello.mustache
<html>
  <head>
    <title>Mustache Template</title>
  </head>
  <body>
    <h1>Mustache Template</h1>
    
    {{#users}}
      <h2>Name</h2>
      <p>{{name}}</p>
      
      <h2>Age</h2>
      <p>{{age}}</p>
      
      <hr>
    {{/users}}
  </body>
</html>
```

- `hello.mustache` は、クラスパス上の `/templates` の下に配置する。

##動作確認
![spark.JPG](https://qiita-image-store.s3.amazonaws.com/0/28302/3c32015b-cf6d-5b5c-f0b2-f53fed02f20b.jpeg)

##説明
- テンプレートは、 Freemarker, Verocity, Mustache の３つがサポートされている。
- 各テンプレートエンジンごとにプラグインの jar が用意されているので、使用するテンプレートエンジンに合わせてクラスパスに追加する。
- ルーティング関数では、以下のように実装する。
    - 最後の引数に、各テンプレートごとの `TemplateEngine` インスタンスを渡す。
    - ルーティング関数は、 `ModelAndView` インスタンスを返すようにする。


#Web アプリとしてサーブレットコンテナに配備する
##実装
```text:フォルダ構成
spark/
  |-src/main/
  |  |-java/sample/spark/
  |  |  `-MySparkApplication.java
  |  `-webapp/WEB-INF/
  |     `-web.xml
  `-build.gradle
```

```groovy:build.gradle
apply plugin: 'jetty'
apply plugin: 'war'

sourceCompatibility = '1.8'
targetCompatibility = '1.8'

repositories {
    mavenCentral()
}

dependencies {
    compile 'com.sparkjava:spark-core:2.0.0'
}
```

```xml:web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://java.sun.com/xml/ns/javaee"
         xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
                             http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" 
         version="3.0">
  
  <filter>
    <filter-name>SparkFilter</filter-name>
    <filter-class>spark.servlet.SparkFilter</filter-class>
    <init-param>
      <param-name>applicationClass</param-name>
      <param-value>sample.spark.MySparkApplication</param-value>
    </init-param>
  </filter>

  <filter-mapping>
    <filter-name>SparkFilter</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>

</web-app>
```

```java:MySparkApplication.java
package sample.spark;

import static spark.Spark.*;
import spark.servlet.SparkApplication;

public class MySparkApplication implements SparkApplication {
    
    @Override
    public void init() {
        get("/hello", (request, response) -> "Hello Spark as WebApp.");
    }
}
```

##動作確認
```text:
> gradle jettyRun
:compileJava
:processResources UP-TO-DATE
:classes
:jettyRun
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
> Building 75% > :jettyRun > Running at http://localhost:8080/spark
```

ブラウザで `http://localhost:8080/spark/hello` にアクセス。

![spark.jpg](https://raw.githubusercontent.com/opengl-8080/opengl-8080.github.io/master/img/qiita/spark/spark_webapp.jpg)

##説明
- `SparkApplication` を実装したクラスを作成し、 web.xml で `SparkFilter` の設定を記述することで、通常の Web アプリケーションとして Spark を利用することができる。

#参考
- [Spark Framework - Documentation](http://www.sparkjava.com/documentation.html)