IntelliJ+Jersey+Spring FrameworkでRESTfulAPIのマルチモジュールサンプル

以下の記事の続きです。

Jersey+Spring FrameworkでRESTfulAPIの最小構成サンプル
https://qiita.com/kasa_le/items/59ebd6b5490945dd5580

Mavenでマルチモジュール構成にする(Jersey RESTful)
https://qiita.com/kasa_le/items/db0d84e3e868ff14bc2b

今回は上の２つの記事で作成したプロジェクトをマージするような作業です。
が、一応ゼロからプロジェクトを作るていで説明します。

# 目的
**Jsersey**と**Spring Framework**(not Boot)でRESTfulAPIを実装する。

# ゴール
- Jersey+Spring FrameworkでRESTfulAPIが一通り(CRUD)が動く。
- マルチモジュール構成でDIが出来る。

# 環境など

|ツールなど|バージョンなど|
|:--|:--|
| MacbookPro |macOS Mojave 10.14.5|
|IntelliJ IDEA |Ultimate 2019.3.3|
|Java |AdoptOpenJDK 11|
|apache maven |3.6.3|
|Jersey|2.30.1|
|JUnit|5.6.0|
|Tomcat|apache-tomcat-8.5.51|
|Postman|7.19.1|
|Spring Framework|5.2.4-RELEASE|

# プロジェクト設定

IntelliJで、`Mavenプロジェクト`を新規作成します。

## 1.ルートpom

基本的に、依存関係やプラグインの設定は、[Jersey+Spring Frameworkの最小構成](https://qiita.com/kasa_le/items/59ebd6b5490945dd5580)プロジェクトのものをコピペすればOK。

```xml:pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">

    <modelVersion>4.0.0</modelVersion>

    <groupId>my.example.jerseyspring</groupId>
    <artifactId>jersey-spring-restful</artifactId>
    <packaging>war</packaging>
    <version>1.0-SNAPSHOT</version>
    <name>Jersey and Spring Framework RESTfulAPI Sample</name>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.0</version>
                <inherited>true</inherited>
                <configuration>
                    <source>11</source>
                    <target>11</target>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.22.2</version>
                <configuration>
                    <additionalClasspathElements>
                        <additionalClasspathElement>src/test/java/</additionalClasspathElement>
                    </additionalClasspathElements>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-site-plugin</artifactId>
                <version>3.7.1</version>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-report-plugin</artifactId>
                <version>3.0.0-M4</version>
            </plugin>
        </plugins>
    </build>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.glassfish.jersey</groupId>
                <artifactId>jersey-bom</artifactId>
                <version>${jersey.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <!-- Jersey -->
        <!-- https://mvnrepository.com/artifact/org.glassfish.jersey.core/jersey-server -->
        <dependency>
            <groupId>org.glassfish.jersey.core</groupId>
            <artifactId>jersey-server</artifactId>
        </dependency>
        <dependency>
            <groupId>org.glassfish.jersey.inject</groupId>
            <artifactId>jersey-hk2</artifactId>
        </dependency>
        <dependency>
            <groupId>org.glassfish.jersey.media</groupId>
            <artifactId>jersey-media-json-binding</artifactId>
        </dependency>

        <!-- Spring dependencies -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-core</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-web</artifactId>
            <version>${spring.version}</version>
        </dependency>

        <!-- Jersey + Spring -->
        <!-- https://mvnrepository.com/artifact/org.glassfish.jersey.ext/jersey-spring5 -->
        <dependency>
            <groupId>org.glassfish.jersey.ext</groupId>
            <artifactId>jersey-spring5</artifactId>
            <version>${jersey.version}</version>
        </dependency>

        <!-- JAXBはJDK9から外されました -->
        <!-- https://mvnrepository.com/artifact/javax.xml.bind/jaxb-api -->
        <dependency>
            <groupId>javax.xml.bind</groupId>
            <artifactId>jaxb-api</artifactId>
            <version>2.3.1</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/javax.activation/activation -->
        <dependency>
            <groupId>javax.activation</groupId>
            <artifactId>activation</artifactId>
            <version>1.1.1</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.glassfish.jaxb/jaxb-runtime -->
        <dependency>
            <groupId>org.glassfish.jaxb</groupId>
            <artifactId>jaxb-runtime</artifactId>
            <version>2.3.2</version>
        </dependency>

        <!-- テスト -->
        <!-- https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-api -->
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-api</artifactId>
            <version>${junit.jupiter.version}</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-engine</artifactId>
            <version>${junit.jupiter.version}</version>
            <scope>test</scope>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.glassfish.jersey.test-framework.providers/jersey-test-framework-provider-grizzly2 -->
        <dependency>
            <groupId>org.glassfish.jersey.test-framework.providers</groupId>
            <artifactId>jersey-test-framework-provider-grizzly2</artifactId>
            <version>2.30.1</version>
            <scope>test</scope>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.assertj/assertj-core -->
        <dependency>
            <groupId>org.assertj</groupId>
            <artifactId>assertj-core</artifactId>
            <version>3.15.0</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-params</artifactId>
            <version>${junit.jupiter.version}</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <properties>
        <spring.version>5.2.4.RELEASE</spring.version>
        <jersey.version>2.30.1</jersey.version>
        <junit.jupiter.version>5.6.0</junit.jupiter.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
</project>
```

## 2.サブモジュールの作成

### (1)ルートの`src`フォルダを削除

手動で削除します。

### (2)サブモジュールの追加

IntelliJのメニュー[File]-[New]-[Module...]から、Mavenプロジェクトで以下の２つを追加します。

- repository
- serverapi

サブモジュールの名称は任意です。**Parent**の設定を忘れないでください。(ルートのプロジェクトを選んでくださいね。)

そうすると、親`pom.xml`の`<packaging>`が`pom`に変わるはずです。
変わっていなければ手動で直してください。

```xml:root/pom.xml
    <groupId>my.example.jerseyspring</groupId>
    <artifactId>jersey-spring-restful</artifactId>
    <packaging>pom</packaging>
```

サブモジュールのそれぞれの`pom.xml`は次のようにします。

```xml:repository/pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>jersey-spring-restful</artifactId>
        <groupId>my.example.jerseyspring</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>repository</artifactId>
    <packaging>jar</packaging>
</project>
```

```xml:serverapi/pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>jersey-spring-restful</artifactId>
        <groupId>my.example.jerseyspring</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>serverapi</artifactId>
    <packaging>war</packaging>

    <dependencies>
        <dependency>
            <groupId>my.example.jerseyspring</groupId>
            <artifactId>repository</artifactId>
            <version>${project.version}</version>
        </dependency>

        <!-- for spring test-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>${spring.version}</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>
```

serverapiモジュールは、Spring TestがJunitテストで必要なので`<test>`スコープで入れています。
ただ、他のサブモジュールでも必要な場合は、ルートの`pom.xml`に入れてしまって良いでしょう。

なお、どちらも、自分のバージョンは指定していません。親のバージョンをそのまま使いたいので。
サブモジュールごとにバージョン管理をしない方針です。する必要がある場合は、`<version>`タグでそれぞれにバージョン指定をしてください。

### (3)フォルダ構成
現在のフォルダ構成はこうなっているはずです。

```
$ tree
.
├── SpringJerseyRest.iml
├── pom.xml
├── repository
│   ├── pom.xml
│   └── src
│       ├── main
│       │   ├── java
│       │   └── resources
│       └── test
│           └── java
└── serverapi
    ├── pom.xml
    └── src
        ├── main
        │   ├── java
        │   └── resources
        └── test
            └── java
```

## 2.repositoryモジュールの設定

### (1)モデルクラス
パッケージ`my.example.jerseyspring.repository.models`に以下のクラスを作成します。

```Employee.java
package my.example.jerseyspring.repository.models;

import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement
public class Employee {
    private int id;
    private String firstName;

    public Employee() {
    }

    public Employee(int id, String firstName) {
        this.id = id;
        this.firstName = firstName;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }
}
```

### (2)リポジトリクラス
パッケージ`my.example.jerseyspring.repository`に`EmployeeRepository`クラスを作成します。

```EmployeeRepository.java
package my.example.jerseyspring.repository;


import my.example.jerseyspring.repository.exceptions.DuplicateIdException;
import my.example.jerseyspring.repository.exceptions.EmployeeNameNotFoundException;
import my.example.jerseyspring.repository.exceptions.EmployeeNotFoundException;
import my.example.jerseyspring.repository.models.Employee;
import org.springframework.stereotype.Repository;

import java.util.ArrayList;
import java.util.List;

@Repository
public class EmployeeRepository {

    private List<Employee> employeeList;

    public EmployeeRepository() {
        employeeList = new ArrayList<>();
        employeeList.add(new Employee(3, "Cupcake"));
        employeeList.add(new Employee(4, "Donuts"));
        employeeList.add(new Employee(5, "Eclair"));
        employeeList.add(new Employee(8, "Froyo"));
        employeeList.add(new Employee(9, "Gingerbread"));
    }

    public List<Employee> selectAll() {
        return employeeList;
    }

    public Employee select(int id) {
        for (Employee employee : employeeList) {
            if (employee.getId() == id) {
                return employee;
            }
        }
        throw new EmployeeNotFoundException();
    }

    public synchronized void insert(int id, String firstName) {
        try {
            select(id);
        } catch (EmployeeNotFoundException e) {
            // いなければ追加できる
            employeeList.add(new Employee(id, firstName));
            return;
        }
        // 同じIDが存在したら追加できない
        throw new DuplicateIdException();
    }

    public synchronized void update(int id, String firstName) {
        Employee employee = select(id);
        employee.setFirstName(firstName);
    }

    public synchronized void delete(int id) {
        Employee employee = select(id);
        employeeList.remove(employee);
    }

    public List<Employee> search(String name) {
        List<Employee> list = new ArrayList<>();
        for (Employee employee : employeeList) {
            if (employee.getFirstName().contains(name)) {
                list.add(employee);
            }
        }
        if (list.size() > 0) return list;
        throw new EmployeeNameNotFoundException(name);
    }
}
```

以前の[マルチモジュールのサンプル](https://qiita.com/kasa_le/items/db0d84e3e868ff14bc2b)では、シングルトン実装を自前で入れていましたが、Spring FrameworkのDIは基本的にシングルトンになるということで、その実装を削除しています。

### (3)例外クラス

`EmployeeRepository`クラスで使っている例外クラスをそれぞれ作成します。
パッケージは`repository.exceptions`としました。

例)`EmployeeNotFoundException`クラス

```EmployeeNotFoundException.java
package my.example.jerseyspring.repository.exceptions;

public class EmployeeNotFoundException extends RuntimeException {
    public EmployeeNotFoundException() {
        super("そのIDのEmployeeは見つかりません。");
    }
}
```

他のクラスも同じように作成します。

また、[Jersey+Springの最小構成](https://qiita.com/kasa_le/items/59ebd6b5490945dd5580)のプロジェクトで使った`transacation`パッケージも一応持ってきます。
`repository.transaction`パッケージで良いでしょう。

### (4)Junitテストクラス

例外クラスのテストクラスを作成します。

例として、`EmployeeNotFoundException`の場合を挙げておきます。

```EmployeeNotFoundExceptionTest.java
package my.example.jerseyspring.repository.exceptions;

import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

class EmployeeNotFoundExceptionTest {

    @Test
    void getMessage() {
        EmployeeNotFoundException e = new EmployeeNotFoundException();
        assertThat(e.getMessage()).isEqualTo("そのIDのEmployeeは見つかりません。");
    }
}
```

IntelliJ上から、Junitテストの実行して通過を確認しておきましょう。

### (5)最終構成
最終的に、次のようなフォルダ構成になっているはずです。

```console
$ tree
.
├── pom.xml
└── src
    ├── main
    │   ├── java
    │   │   └── my
    │   │       └── example
    │   │           └── jerseyspring
    │   │               └── repository
    │   │                   ├── EmployeeRepository.java
    │   │                   ├── exceptions
    │   │                   │   ├── DuplicateIdException.java
    │   │                   │   ├── EmployeeNameNotFoundException.java
    │   │                   │   └── EmployeeNotFoundException.java
    │   │                   ├── models
    │   │                   │   └── Employee.java
    │   │                   └── transaction
    │   │                       ├── TransactionBo.java
    │   │                       └── impl
    │   │                           └── TransactionBoImpl.java
    │   └── resources
    └── test
        └── java
            └── my
                └── example
                    └── jerseyspring
                        └── repository
                            └── exceptions
                                ├── DuplicateIdExceptionTest.java
                                ├── EmployeeNameNotFoundExceptionTest.java
                                └── EmployeeNotFoundExceptionTest.java

19 directories, 11 files
```

## 3.serverapiモジュールの設定

### (1)サービスクラス

パッケージ`my.example.jerseyspring.rest`に`EmployeeService`クラスを作成します。

```EmployeeService.java
package my.example.jerseyspring.rest;

import my.example.jerseyspring.repository.EmployeeRepository;
import my.example.jerseyspring.repository.models.Employee;
import org.springframework.stereotype.Service;

import javax.ws.rs.*;
import javax.ws.rs.core.*;
import java.util.List;

@Service
@Path("/employees")
public class EmployeeService {

    final EmployeeRepository employeeRepository;

    public EmployeeService(EmployeeRepository employeeRepository) {
        this.employeeRepository = employeeRepository;
    }

    @Context
    UriInfo uriInfo;

    @GET
    @Path("/all")
    @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
    public List<Employee> getAll() {
        return employeeRepository.selectAll();
    }

    @GET
    @Path("/{id}")
    @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
    public Employee getEmployee(@PathParam("id") int id) {
        return employeeRepository.select(id);
    }

    @GET
    @Path("/search")
    @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
    public List<Employee> searchEmployee(@QueryParam("name") String name) {
        return employeeRepository.search(name);
    }

    @POST
    @Consumes({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
    public Response addEmployee(Employee employee) {
        employeeRepository.insert(employee.getId(), employee.getFirstName());

        UriBuilder builder = uriInfo.getAbsolutePathBuilder();
        builder.path(String.valueOf(employee.getId()));
        return Response.created(builder.build()).build();
    }

    @PUT
    @Consumes({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
    public Response updateEmployee(Employee employee) {
        employeeRepository.update(employee.getId(), employee.getFirstName());
        // 新規作成した場合はcreatedを返す必要があるが、このサンプルではエラーとするため、常にokを返す
        return Response.ok().build();
    }

    @DELETE
    @Path("/{id}")
    @Consumes({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
    public Response deleteEmployee(@PathParam("id") int id) {
        employeeRepository.delete(id);
        // Entityの状態を返す場合はokを返す。
        // 受け付けたが処理が終わっていない場合は(キューに乗っただけなど)acceptedを返す
        // このサンプルでは削除が完了して該当コンテントがなくなったことだけ返す
        return Response.noContent().build();
    }
}
```

また、[Jersey+Springの最小構成](https://qiita.com/kasa_le/items/59ebd6b5490945dd5580)のプロジェクトで使った`PaymentService`クラスも一応持ってきます。

### (2)例外ハンドラクラス
例外マッピング用のクラスを`rest.handlers`パッケージに作ります。
repositoryモジュールで作った例外クラスごとに必要です。
例外のマッピングについては、[こちら](https://qiita.com/kasa_le/items/3c7e7a426acf846ee64e#%E4%BE%8B%E5%A4%96%E3%83%8F%E3%83%B3%E3%83%89%E3%83%AA%E3%83%B3%E3%82%B0)をご参照ください。

例) `EmployeeNotFoundException`クラスのマッパー

```NotFoundExceptionHandler.java
package my.example.jerseyspring.rest.handler;


import my.example.jerseyspring.repository.exceptions.EmployeeNotFoundException;

import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;

@Provider
public class NotFoundExceptionHandler implements ExceptionMapper<EmployeeNotFoundException> {

    public Response toResponse(EmployeeNotFoundException ex) {
        return Response.status(Response.Status.NOT_FOUND).build();
    }
}
```

### (3)Beanの設定
`src/main/resources`下に、`applicationContext.xml`を作成し、以下のような内容にします。

```xml:applicationContext.xml
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
	http://www.springframework.org/schema/beans/spring-beans-3.0.xsd 
	http://www.springframework.org/schema/context
	http://www.springframework.org/schema/context/spring-context-3.0.xsd">

    <context:component-scan base-package="my.example.jerseyspring"/>

    <bean id="transactionBo" class="my.example.jerseyspring.repository.transaction.impl.TransactionBoImpl"/>

    <bean id="employeeRepository" class="my.example.jerseyspring.repository.EmployeeRepository"/>

</beans>
```

### (4)`webapp`フォルダの設定

`src/main/webapp`フォルダを作ります。

- `webapp/WEB-INF`下に`web.xml`を作成して次の内容にする

```xml:web.xml
<?xml version="1.0" encoding="UTF-8"?>
<!-- This web.xml file is not required when using Servlet 3.0 container,
     see implementation details http://jersey.java.net/nonav/documentation/latest/jax-rs.html -->
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
    <display-name>Restful Web Application</display-name>

    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:applicationContext.xml</param-value>
    </context-param>

    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

    <servlet>
        <servlet-name>jersey-servlet</servlet-name>
        <servlet-class>org.glassfish.jersey.servlet.ServletContainer</servlet-class>
        <init-param>
            <param-name>jersey.config.server.provider.packages</param-name>
            <param-value>my.example.jerseyspring</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>jersey-servlet</servlet-name>
        <url-pattern>/rest/*</url-pattern>
    </servlet-mapping>
</web-app>
```

- `webapp/`下に`index.jsp`を作成して次のような内容にする

```jsp:index.jsp
<html>

<body>
<h2>Jersey + Spring RESTful Web Application!</h2>
<p><a href="rest/payment/mkyong">Jersey resource</a>
    <br>
<p><a href="rest/employees/all">All Employee List</a>
<p><a href="rest/employees/3">get id=3 employee</a>

</body>

</html>
```

### (5)Junitテストクラス

`EmployeeServiceTest`クラス、`PaymentServiceTest`クラスを作成してテストを書きます。

```EmployeeServiceTest.java
package my.example.jerseyspring.rest;

import my.example.jerseyspring.repository.EmployeeRepository;
import my.example.jerseyspring.repository.models.Employee;
import my.example.jerseyspring.rest.handler.DuplicateExceptionHandler;
import my.example.jerseyspring.rest.handler.NameNotFoundExceptionHandler;
import my.example.jerseyspring.rest.handler.NotFoundExceptionHandler;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.test.JerseyTest;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;

import javax.ws.rs.client.Entity;
import javax.ws.rs.core.Application;
import javax.ws.rs.core.GenericType;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.List;
import java.util.stream.Stream;

import static org.assertj.core.api.Assertions.assertThat;

@ExtendWith(SpringExtension.class)
@ContextConfiguration("classpath:/applicationContext.xml")
public class EmployeeServiceTest extends JerseyTest {

    @Autowired
    EmployeeRepository employeeRepository;

    @Override
    protected Application configure() {
        return new ResourceConfig(EmployeeService.class)
                .register(DuplicateExceptionHandler.class)
                .register(NameNotFoundExceptionHandler.class)
                .register(NotFoundExceptionHandler.class)
                .register(this);
    }

    @BeforeEach
    @Override
    public void setUp() throws Exception {
        super.setUp();
    }

    @AfterEach
    @Override
    public void tearDown() throws Exception {
        super.tearDown();
    }

    @ParameterizedTest
    @ValueSource(strings = {MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
    public void getAll(String mediaType) {
        final Response response = target("/employees/all").request().accept(mediaType).get();
        assertThat(response.getHeaderString("Content-Type"))
                .isEqualTo(mediaType);

        List<Employee> content = response.readEntity(new GenericType<>() {
        });
        assertThat(content.size()).isEqualTo(5);
        assertThat(content.get(0)).isEqualToComparingFieldByField(new Employee(3, "Cupcake"));
        assertThat(content.get(1)).isEqualToComparingFieldByField(new Employee(4, "Donuts"));
        assertThat(content.get(2)).isEqualToComparingFieldByField(new Employee(5, "Eclair"));
        assertThat(content.get(3)).isEqualToComparingFieldByField(new Employee(8, "Froyo"));
        assertThat(content.get(4)).isEqualToComparingFieldByField(new Employee(9, "Gingerbread"));
    }

    @ParameterizedTest
    @MethodSource("getParamProvider")
    public void getEmployee(int id, String mediaType) {
        String urlPath = String.format("/employees/%d", id);
        final Response response = target(urlPath).request().accept(mediaType).get();
        assertThat(response.getHeaderString("Content-Type"))
                .isEqualTo(mediaType);

        Employee employee = response.readEntity(Employee.class);
        Employee expect = employeeRepository.select(id);
        assertThat(employee).isEqualToComparingFieldByField(expect);
    }

    static Stream<Arguments> getParamProvider() {
        return Stream.of(
                Arguments.of(3, MediaType.APPLICATION_JSON),
                Arguments.of(4, MediaType.APPLICATION_JSON),
                Arguments.of(5, MediaType.APPLICATION_JSON),
                Arguments.of(8, MediaType.APPLICATION_JSON),
                Arguments.of(9, MediaType.APPLICATION_JSON),
                Arguments.of(3, MediaType.APPLICATION_XML),
                Arguments.of(4, MediaType.APPLICATION_XML),
                Arguments.of(5, MediaType.APPLICATION_XML),
                Arguments.of(8, MediaType.APPLICATION_XML),
                Arguments.of(9, MediaType.APPLICATION_XML)
        );
    }

    @ParameterizedTest
    @ValueSource(strings = {MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
    public void searchEmployee(String mediaType) {
        final Response response = target("/employees/search")
                .queryParam("name", "a")
                .request()
                .accept(mediaType)
                .get();
        assertThat(response.getHeaderString("Content-Type"))
                .isEqualTo(mediaType);

        List<Employee> content = response.readEntity(new GenericType<>() {
        });
        assertThat(content.size()).isEqualTo(3);
        assertThat(content.get(0)).isEqualToComparingFieldByField(new Employee(3, "Cupcake"));
        assertThat(content.get(1)).isEqualToComparingFieldByField(new Employee(5, "Eclair"));
        assertThat(content.get(2)).isEqualToComparingFieldByField(new Employee(9, "Gingerbread"));
    }

    @ParameterizedTest
    @MethodSource("postRawProvider")
    public void addEmployee(int id, String bodyRaw, String mediaType) {

        final Response response = target("/employees").request()
                .post(Entity.entity(bodyRaw, mediaType));
        assertThat(response.getStatus()).isEqualTo(201);
        assertThat(response.getHeaderString("Location"))
                .isEqualTo("http://localhost:9998/employees/" + id);
    }

    static Stream<Arguments> postRawProvider() {
        final String json = "{\"firstName\":\"Honeycomb\",\"id\":11}";
        final String xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
                "<employee><firstName>KitKat</firstName><id>19</id></employee>";
        return Stream.of(
                Arguments.of(11, json, MediaType.APPLICATION_JSON),
                Arguments.of(19, xml, MediaType.APPLICATION_XML)
        );
    }

    @ParameterizedTest
    @MethodSource("putRawProvider")
    public void updateEmployee(int id, String bodyRaw, String mediaType) {
        final Response response = target("/employees").request()
                .put(Entity.entity(bodyRaw, mediaType));
        assertThat(response.getStatus()).isEqualTo(200);

        Employee employee = target("/employees/" + id).request().get(Employee.class);
        Employee expected = employeeRepository.select(id);
        assertThat(employee).isEqualToComparingFieldByField(expected);
    }

    static Stream<Arguments> putRawProvider() {
        final String json = "{\"firstName\":\"Frozen yogurt\",\"id\":8}";
        final String xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
                "<employee><firstName>Cup Cake</firstName><id>3</id></employee>";
        return Stream.of(
                Arguments.of(8, json, MediaType.APPLICATION_JSON),
                Arguments.of(3, xml, MediaType.APPLICATION_XML)
        );
    }

    @Test
    public void deleteEmployee() {
        final Response response = target("/employees/9")
                .request().delete();
        assertThat(response.getStatus()).isEqualTo(204);
    }

    @Test
    public void exception_selectEmployee() {
        final Response response = target("/employees/1").request().get();
        assertThat(response.getStatus()).isEqualTo(404);
    }

    @Test
    public void exception_searchEmployee() {
        final Response response = target("/employees/search?name=android").request().get();
        assertThat(response.getStatus()).isEqualTo(404);
    }

    @ParameterizedTest
    @MethodSource("putRawProvider")
    public void exception_addEmployee(int id, String bodyRaw, String mediaType) {
        final Response response = target("/employees").request()
                .post(Entity.entity(bodyRaw, mediaType));
        assertThat(response.getStatus()).isEqualTo(409);
    }

    @ParameterizedTest
    @MethodSource("putExceptionProvider")
    public void exception_updateEmployee(int id, String bodyRaw, String mediaType) {
        final Response response = target("/employees").request()
                .put(Entity.entity(bodyRaw, mediaType));
        assertThat(response.getStatus()).isEqualTo(404);
    }

    static Stream<Arguments> putExceptionProvider() {
        final String json = "{\"firstName\":\"Lollipop\",\"id\":21}";
        final String xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
                "<employee><firstName>Jelly Bean</firstName><id>17</id></employee>";
        return Stream.of(
                Arguments.of(21, json, MediaType.APPLICATION_JSON),
                Arguments.of(3, xml, MediaType.APPLICATION_XML)
        );
    }

    @Test
    public void exception_deleteEmployee() {
        final Response response = target("/employees/1").request().get();
        assertThat(response.getStatus()).isEqualTo(404);
    }
}
```

[マルチモジュールのサンプル](https://qiita.com/kasa_le/items/db0d84e3e868ff14bc2b)で`EmployeeRepository.getInstance`としてシングトンオブジェクトを取得していた部分は、`    @Autowired`で宣言したメンバー変数へのアクセスと変更しています。
`@Autowired`アノテーションにより、SpringがDIしてくれますが、実はそれだけだと、サービスで実際に動く方とテストの方で別のインスタンスが作られてしまうらしく(シングルトンのはずだけど、おそらくテストのコンテキストがアプリケーションコンテキストとは違うせいかと)、`ResourceConfig#register(this)`をすれば大丈夫という記事を見かけてそのようにしています。
ただ、これで動いてしまうのはバグではないか？だって警告文が出ているよ？という意見もあるので、正しいのかどうかわかりません。
https://stackoverflow.com/questions/34453448/how-to-access-spring-bean-from-jerseytest-subclass

今回は、そもそもリポジトリクラスは本題ではなくて仮のデータアクセスを提供しているに過ぎないので、いったんはこれで良しとしています。ただ、SpringまたはJerseyのバージョンが上がると、動かなくなる可能性はあるので要注意です。

`PaymentServiceTest`クラスも、[最小構成のサンプル](https://qiita.com/kasa_le/items/59ebd6b5490945dd5580)から持ってきます。
また、DIのモック差し替えテスト用に作った`TransactionBoMock`クラスも持ってきておきます。

そして、Beanを差し替えられるように、`test`フォルダの`resources`フォルダにも`applicationContext.xml`を置いて、次のようにします。

```xml:test/resources/applicationContext.xml
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
	http://www.springframework.org/schema/beans/spring-beans-3.0.xsd 
	http://www.springframework.org/schema/context
	http://www.springframework.org/schema/context/spring-context-3.0.xsd">

    <context:component-scan base-package="my.example.jerseyspring"/>

    <bean id="transactionBo" class="my.example.jerseyspring.repository.transaction.impl.TransactionBoMock"/>
    <bean id="employeeRepository" class="my.example.jerseyspring.repository.EmployeeRepository"/>

</beans>
```

EmployeeRepositoryはインターフェースも作ってないので今回特に差し替えませんが、本来であれば、DBなどからデータを取得する本来の実装に対し、テスト用にMockしたクラスを使うべきでしょうね。

### (6)最終構成

`serverapi`フォルダの構成は次のようになっているはずです。

```
.
├── pom.xml
└── src
    ├── main
    │   ├── java
    │   │   └── my
    │   │       └── example
    │   │           └── jerseyspring
    │   │               └── rest
    │   │                   ├── EmployeeService.java
    │   │                   ├── PaymentService.java
    │   │                   └── handler
    │   │                       ├── DuplicateExceptionHandler.java
    │   │                       ├── NameNotFoundExceptionHandler.java
    │   │                       └── NotFoundExceptionHandler.java
    │   ├── resources
    │   │   └── applicationContext.xml
    │   └── webapp
    │       ├── WEB-INF
    │       │   └── web.xml
    │       └── index.jsp
    └── test
        ├── java
        │   └── my
        │       └── example
        │           └── jerseyspring
        │               ├── repository
        │               │   └── transaction
        │               │       └── impl
        │               │           └── TransactionBoMock.java
        │               └── rest
        │                   ├── EmployeeServiceTest.java
        │                   └── PaymentServiceTest.java
        └── resources
            └── applicationContext.xml

21 directories, 13 files
```

## 4.実行、テスト

### (1)JUnitTest
JUnitTestを実行して、すべてのテストが通過するか確認します。

```console
$ mvn clean test
```

### (2)動作確認
Tomcatを設定してデプロイします。

以下のようなページが表示され、リンクをクリックするとAPIの戻り値が表示されるはずです。

|<img width="609" alt="jersey-spring-restful.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103886/4f0cea6b-7990-88e5-0770-da9d0ce527bf.png">|
|:--|

POST系や、Json/Xmlの入出力を確認したい場合は、CurlやPostmanなどを使って確認してみてください。

ここまでのプロジェクトは、以下にアップしてあります。
https://github.com/le-kamba/spring-jersey-sample/tree/spring_jersey

# 感想
シングルトンの箇所以外ではハマることなくすんなり出来ました。

ちなみに、フォルダ構成は、macにbrewで**tree**コマンドを入れて出しました。
そもそもHomebrewのインストールからやらなきゃならなかったですが。(入れてなかったっけ？)

```console
# Homebrewのインストール
$ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"

# treeのインストール
$ brew install tree
```

# 参考
SpringのDIとnewってなにがちがうんだっけ？
https://qiita.com/uqichi/items/5f59817beb3dff9c0c1e