GAE/Java8試行（その8：「Cloud Endpoints Frameworks」）

# お題
WebAPIを楽に開発しようと思った時、素のサーブレットを使うのはしんどいので、[Cloud Endpoints Frameworks](https://cloud.google.com/endpoints/?hl=ja)を導入する。
楽なポイントとしては、↓

 - Mavenコマンド一発でスケルトン（やチュートリアル）プロジェクトの生成が可能
 - アノテーションベースでAPI定義が可能
 - APIメソッドの引数をオブジェクトで受け取るようにすると勝手にJSON構造をマッピングしてくれる
 - APIキーによるアクセス制限がアノテーションの属性一発で可能
 - JSON Web TokenやFirebase Authenticationといったユーザー認証の仕組みもアノテーションベース

# GAE試行Index
- [GAE/Java8試行（その0：「App Engineについて」）](https://qiita.com/sky0621/items/6175d578d0521858a944)
- [GAE/Java8試行（その1：「Java8でWebアプリ作ってデプロイ」）](https://qiita.com/sky0621/items/8437da4e96bffef69250)
- [GAE/Java8試行（その2：「Javaアプリ解説」）](https://qiita.com/sky0621/items/65ce0406fb895f547ae5)
- [GAE/Java8試行（その3：「Javaアプリテストコード解説」）](https://qiita.com/sky0621/items/874c8b6462fdc5a0829d)
- [GAE/Java8試行（その4：「Datastoreへのアクセスロジック」）](https://qiita.com/sky0621/items/c150f6e66320e240add8)
- [GAE/Java8試行（その5：「ローカル開発用コンソール」）](https://qiita.com/sky0621/items/70a17e243d6e5175ed20)
- [GAE/Java8試行（その6：「デプロイ失敗」）](https://qiita.com/sky0621/items/d89c7473fd5207edcfd5)
- [GAE/Java8試行（その7：「ウォームアップリクエスト(ServletContextListener使用版)」）](https://qiita.com/sky0621/items/33fb5de9a228a77338e8)

# 開発環境
### # OS
```
$ cat /etc/os-release 
NAME="Ubuntu"
VERSION="17.10 (Artful Aardvark)"
```

### # Java
```
$ java -version
java version "1.8.0_181"
Java(TM) SE Runtime Environment (build 1.8.0_181-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.181-b13, mixed mode)
```
### # IDE
みんな大好き[IntelliJ IDEA](https://www.jetbrains.com/idea/)

# 実践

ソースの全量は↓
https://github.com/sky0621/java-webapi-for-gae-study/tree/d67ad8d3ea5816f26c5bfad3bb72215963042f60/com.example.sky0621.fs

## ■Mavenプロジェクト生成

今回は、一からCloud Endpoints Frameworksを試すので最小のスケルトンでMavenプロジェクトを生成。
（groupId, artifactId, packageすべて「com.example.sky0621.fs」を入力）

```
$ mvn archetype:generate -Dappengine-version=1.9.67 -Djava8=true -Dfilter=com.google.appengine.archetypes:
[INFO] Scanning for projects...
　　　　　・・・
[INFO] No archetype defined. Using maven-archetype-quickstart (org.apache.maven.archetypes:maven-archetype-quickstart:1.0)
Choose archetype:
1: remote -> com.google.appengine.archetypes:appengine-flexible-archetype (A basic Java application with Google App Engine flexible.)
2: remote -> com.google.appengine.archetypes:appengine-skeleton-archetype (A skeleton application with Google App Engine)
3: remote -> com.google.appengine.archetypes:appengine-standard-archetype (A basic Java application with Google App Engine Standard)
4: remote -> com.google.appengine.archetypes:endpoints-skeleton-archetype (A skeleton project using Cloud Endpoints Frameworks with Google App Engine Standard)
5: remote -> com.google.appengine.archetypes:guestbook-archetype (A guestbook application with Google App Engine)
6: remote -> com.google.appengine.archetypes:hello-endpoints-archetype (A simple starter application using Cloud Endpoints Frameworks with Google App Engine Standard)
7: remote -> com.google.appengine.archetypes:skeleton-archetype (Archetype with a README about Google App Engine archetypes)
Choose a number or apply filter (format: [groupId:]artifactId, case sensitive contains): : 4
```

すると、こんな感じでプロジェクトが出来上がる。

```
$ tree
.
├── LICENSE
├── README.md
├── build.gradle
├── pom.xml
└── src
    └── main
        ├── java
        │   └── com
        │       └── example
        │           └── sky0621
        │               └── fs
        │                   └── YourFirstAPI.java
        └── webapp
            └── WEB-INF
                ├── appengine-web.xml
                ├── logging.properties
                └── web.xml
```

唯一生成されたJavaクラス(`YourFirstAPI.java`)の中身は「`@Api`アノテーション」とクラス定義のみ。

```java
@Api(name = "skeleton-api",
     version = "v1")
public class YourFirstAPI {
}
```

https://github.com/sky0621/java-webapi-for-gae-study/tree/a4dd386a5c19d4bfd9d9c953e2c4d8d076eba556/com.example.sky0621.fs

## ■CRUD-API作成

### 設計

というほどの大層なものではなく、単にAPIとして扱うリソースを決め、機能を定義する。

#### リソース

書籍 - book

| 論理名 | 物理名 | 型 |
| :--- | :--- | :--- |
| ASIN | asin | string |
| 書籍名 | bookName | string |
| 著者名 | authors | string[] |
| 価格 | price | int |

#### 機能

| 機能名 | HTTPメソッド | URL | リクエスト | レスポンス |
| :--- | :--- | :--- | :--- | :--- |
| 登録 | POST | {hostname}/_ah/api/fs/v1/books | bookオブジェクト | none |
| 参照 | GET | {hostname}/_ah/api/fs/v1/books/{asin} | none | bookオブジェクト |
| 一覧 | GET | {hostname}/_ah/api/fs/v1/books | none | bookオブジェクトリスト |
| 更新 | PUT | {hostname}/_ah/api/fs/v1/books/{asin} | bookオブジェクト | none |
| 削除 | DELETE | {hostname}/_ah/api/fs/v1/books/{asin} | none | none |

### 実装

登録時や更新時のリクエストパラメータ並びに一覧取得時のレスポンスに用いる構造。

```java
public class Book {
    private String asin;
    private String bookName;
    private List<String> authors;
    private int price;

    public Book() {
    }

    public Book(String asin, String bookName, List<String> authors, int price) {
        this.asin = asin;
        this.bookName = bookName;
        this.authors = authors;
        this.price = price;
    }

    public String getAsin() {
        return asin;
    }

    public void setAsin(String asin) {
        this.asin = asin;
    }

    public String getBookName() {
        return bookName;
    }

    public void setBookName(String bookName) {
        this.bookName = bookName;
    }

    public List<String> getAuthors() {
        return authors;
    }

    public void setAuthors(List<String> authors) {
        this.authors = authors;
    }

    public int getPrice() {
        return price;
    }

    public void setPrice(int price) {
        this.price = price;
    }

}
```

あと、とりあえず永続化はメモリにしておくので、そのための器を適当に作っておく。

```java
public class BookSaver {
    private static List<Book> bookList = new ArrayList<>();

    public static void add(Book book) {
        bookList.add(book);
    }

    public static Book get(String asin) {
        for (Book b : bookList) {
            if (b.getAsin().equals(asin)) {
                return b;
            }
        }
        return null;
    }

    public static List<Book> list() {
        return bookList;
    }

    public static void update(Book book) {
        List<Book> bl = new ArrayList<>();
        for (Book b : bookList) {
            if (b.getAsin().equals(book.getAsin())) {
                bl.add(book);
            } else {
                bl.add(b);
            }
        }
        bookList = bl;
    }

    public static void delete(String asin) {
        List<Book> bl = new ArrayList<>();
        for (Book b : bookList) {
            if (!b.getAsin().equals(asin)) {
                bl.add(b);
            }
        }
        bookList = bl;
    }
}
```

最後に、APIメソッドを定義。

```java
@Api(name = "fs",
     version = "v1")
public class YourFirstAPI {
    @ApiMethod(name = "books", path = "books", httpMethod = ApiMethod.HttpMethod.POST)
    public void addBook(Book book) {
        BookSaver.add(book);
    }

    @ApiMethod(name = "books", path = "books/{asin}", httpMethod = ApiMethod.HttpMethod.GET)
    public Book getBook(@Named("asin") String asin) {
        return BookSaver.get(asin);
    }

    @ApiMethod(name = "books", path = "books", httpMethod = ApiMethod.HttpMethod.GET)
    public List<Book> listBooks() {
        return BookSaver.list();
    }

    @ApiMethod(name = "books", path = "books/{asin}", httpMethod = ApiMethod.HttpMethod.PUT)
    public void updateBook(@Named("asin") String asin, Book book) {
        BookSaver.update(book);
    }

    @ApiMethod(name = "books", path = "books/{asin}", httpMethod = ApiMethod.HttpMethod.DELETE)
    public void deleteBook(@Named("asin") String asin) {
        BookSaver.delete(asin);
    }
}
```

これだけ。

### 動作確認

まずビルド。

```
$ mvn clean package
```

続いて

```
$ mvn endpoints-framework:openApiDocs
```

さらに、ローカルでWebAPIサーバ起動。

```
$ mvn appengine:run
```

試しに、↓の２つを「`http://localhost:8080/_ah/api/fs/v1/books`」にPOSTする。

```
{
	"asin": "4873117585",
	"bookName": "ゼロから作るDeep Learning",
	"authors": ["斎藤 康毅"],
	"price": 3672
}
```

```
{
	"asin": "B07J6FP6NQ",
	"bookName": "Vue.js入門",
	"authors": ["川口 和也", "喜多 啓介", "野田 陽平", "手島 拓也", "片山 真也"],
	"price": 3650
}
```

「`http://localhost:8080/_ah/api/fs/v1/books`」にGETすると、↓のような結果が返る。

```
{
    "items": [
        {
            "asin": "4873117585",
            "bookName": "ゼロから作るDeep Learning",
            "authors": [
                "斎藤 康毅"
            ],
            "price": 3672
        },
        {
            "asin": "B07J6FP6NQ",
            "bookName": "Vue.js入門",
            "authors": [
                "川口 和也",
                "喜多 啓介",
                "野田 陽平",
                "手島 拓也",
                "片山 真也"
            ],
            "price": 3650
        }
    ]
}
```

「`http://localhost:8080/_ah/api/fs/v1/books/4873117585`」にGETすると、指定の１件が返る。

```
{
    "asin": "4873117585",
    "bookName": "ゼロから作るDeep Learning",
    "authors": [
        "斎藤 康毅"
    ],
    "price": 3672
}
```

「`http://localhost:8080/_ah/api/fs/v1/books/4873117585`」にDELETEすると、指定の１件が削除される。

再度「`http://localhost:8080/_ah/api/fs/v1/books`」にGETすると、DELETEした書籍以外の登録情報が返る。

```
{
    "items": [
        {
            "asin": "B07J6FP6NQ",
            "bookName": "Vue.js入門",
            "authors": [
                "川口 和也",
                "喜多 啓介",
                "野田 陽平",
                "手島 拓也",
                "片山 真也"
            ],
            "price": 3650
        }
    ]
}
```

更新は省略。

# 参考
https://cloud.google.com/endpoints/docs/frameworks/java/annotate-code?hl=ja

# まとめ
今回のは、あくまでCloud Endpoints Frameworksのさわりでしかないけど、WebAPI作るにはデフォルトで用いてもいいくらい楽。
今後、APIキーや認証といった、より便利さが実感できる機能も試してみよう。