JavaでClovaスキルを作成してみた

# はじめに

Javaユーザーグループ北海道（Java Do）が共催した[【9/3@札幌】LINE BOOT AWARDS - Clova スキルハンズオン](https://linedev.connpass.com/event/96793/)は、ドタ参加の方もいらっしゃって盛況のうちに終了しました。開催にむけてご協力いただいた皆さんありがとうございました。

ハンズオンでは node.js を使っていたのですが、せっかくJUGが共催しているので、ハンズオンを聞きながらほぼ同じ内容を [Clova CEK SDK Java](https://github.com/line/clova-cek-sdk-java) で実装していました。

この記事ではこの実装メモを残しておきます。

# 手順

ここでは、次のような構成を実現します。

[あなた] ----- [Clova] ----- [Spring Boot 上の EchoHandler クラス]

## 下準備

ハンズオンのテキストとして用いられた、のびすけさんの[Clova CEKでのスキル開発の始め方〜Node.jsで開発スタート編〜](https://dotstud.io/blog/clova-cek-nodejs-tutorial/#%E4%BD%BF%E3%81%84%E5%A7%8B%E3%82%81%E3%81%AE%E7%94%B3%E8%AB%8B)のうち、

```
3. 使い始めの申請
4. 対話モデルの作成
     インテントとスロット
     スロット
     インテント
5. 対話モデルのビルド
```

のみっつを同じように進めます。

ここで作成したインテント名 `CurreySearchIntent` が重要なので、メモしておきます。

## Java (Spring Boot) でのClovaスキル実装

のびすけさんの資料では node.js で Clova スキルを実装していますが、ここでは Java （Spring Boot） を使って実装します。

### Spring Boot プロジェクトの作成

[Spring Initializr](https://start.spring.io/) にアクセスし、以下の項目を設定します。

- Spring Bootのバージョン： `2.0.4`
- Artifact： `clova_handson`
- Search for dependencies： `Web`, `Devtools` （ひとつずつ追加）<br />![fig01.png](https://qiita-image-store.s3.amazonaws.com/0/8064/48f96389-3134-27b8-dbed-568d4e10cebf.png)

`Generate Project` ボタンで clova.zip をダウンロードします。

clova.zip を展開し、IDE（IntelliJ, Eclipse, Netbeansなど）でMavenプロジェクトとして開きます。

### pom.xml の設定

pom.xmlの `<dependencies>` 〜 `</dependencies>` の間に、 Clova CEK SDK Java の dependency を追加します、

```xml:pom.xml
<dependencies>

  （中略）

  <dependency>
    <groupId>com.linecorp.clova</groupId>
    <artifactId>clova-extension-boot-web</artifactId>
    <version>1.0.0</version>
  </dependency>

</dependencies>
```

### application.properties

Clovaからのリクエストを受け付けるサーバーのファイルパスを設定します。

```properties:application.properties
cek.api-path=/clova
```

これで、 `https://xxxx.../clova` でClovaからのPOSTリクエストを受け取れるように設定されます。

（必要があれば `cek.client.default-locale` なども設定できます）

### リクエストハンドラクラスを作成

ClovaからのPOSTリクエストを処理するリクエストハンドラクラスを作成します。

このクラスは、[Clova CEK SDK Javaの実装例](https://github.com/line/clova-cek-sdk-java/blob/master/samples/echo/src/main/java/com/linecorp/clova/extension/sample/hello/EchoHandler.java) を参考に作ってあります。

```java:EchoHandler.java
package com.example.clova;

import com.linecorp.clova.extension.boot.handler.annotation.*;
import com.linecorp.clova.extension.boot.message.response.CEKResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Optional;

import static com.linecorp.clova.extension.boot.message.speech.OutputSpeech.text;

@CEKRequestHandler
public class EchoHandler {

  private static final Logger log = LoggerFactory.getLogger(EchoHandler.class);

  @LaunchMapping
  CEKResponse handleLaunch() {
    return CEKResponse.builder()
      .outputSpeech(text("カレー屋さんを探します。"))
      .shouldEndSession(false)
      .build();
  }

  @IntentMapping("CurreySearchIntent")
  CEKResponse handleRepeatIntent(@SlotValue Optional<String> area) {
    String outputSpeechText = area
      .map(this::callbackShop)
      .orElse("聞き取れませんでした。");
    return CEKResponse.builder()
      .outputSpeech(text(outputSpeechText))
      .shouldEndSession(false)
      .build();
  }

  private String callbackShop(String inArea) {
    switch (inArea) {
      case "秋葉原":
        return inArea + "のオススメのカレー屋は フジヤマドラゴンカレー です。";
      case "神田":
        return inArea + "のオススメのカレー屋は 共栄堂 です。";
      default:
        return "ごめんなさい、わかりませんでした。";
    }
  }

  @IntentMapping("Clova.CancelIntent")
  CEKResponse handleCancelIntent() {
    return CEKResponse.builder()
      .outputSpeech(text("カレー屋さんの検索を終了します。"))
      .shouldEndSession(true)
      .build();
  }

  @SessionEndedMapping
  CEKResponse handleSessionEnded() {
    log.info("カレー屋検索スキルを終了しました。");
    return CEKResponse.empty();
  }

}
```

いくつか重要な点を解説します。

#### IntentMapping, SlotValue アノテーション

`@IntentMapping` は、カスタムインテント名・[ビルトインインテント](https://clova-developers.line.me/guide/#/CEK/References/CEK_API.md#BuiltinIntent)名に対応する処理（メソッド）に付与するアノテーションです。

この例では、`@IntentMapping(Clova.CancelIntent)` が設定されたメソッド `handleCancelIntent()` は、スキルがキャンセル（終了）された時のビルトインインテント `Clova.CancelIntent` に対応します。

また、`@IntentMapping(CurreySearchIntent)` が設定されたメソッド `handleRepeatIntent(@SlotValue...)` は、前準備で作成した「秋葉原(area)のカレー屋さん教えて」というClovaへの問いかけに対応します。

引数の `@SlotValue` は、スロットの値が渡される引数であることを表します。この例では、「秋葉原(area)のカレー屋さん教えて」の `area` スロットの値が渡されます。

#### CEKResponse クラス

戻り値に設定されている `CEKResponse` クラスは、Clovaの返答を表します。builderでインスタンス化します。

`outputSpeech` は、返答する文字列です。`shouldEndSession`は、スキルの終了を行うべきかどうかを true/false で表します。

#### そのほか

`@LaunchMapping` は、スキルの起動時に対応するメソッドに付与するアノテーションです。

`@SessionEndedMapping` は、スキルの終了時に対応するメソッドに付与するアノテーションです。

## 動作確認


Spring Bootの ClovaApplication クラスを実行します。

のびすけさんの[Clova CEKでのスキル開発の始め方〜Node.jsで開発スタート編〜](https://dotstud.io/blog/clova-cek-nodejs-tutorial/#%E4%BD%BF%E3%81%84%E5%A7%8B%E3%82%81%E3%81%AE%E7%94%B3%E8%AB%8B) のうち、

```
 7. ngrokでホスティングせずに疎通確認
10. 実機テスト
```

の二つを同じように進めます（ngrokは、npmを使わなくても、[本家サイト](https://ngrok.com/)からダウンロード&実行したり、macOS の場合は homebrew を利用してもよいです）。

- あなた: 「ねぇクローバ」
- Clova: 「ポンっ（LEDが緑色に光る）」
- あなた: 「カレー情報を起動して」
- Clova: 「カレー屋さんを探します。」 (← Java で記述している)
- Clova: 「ポンっ（LEDが緑色に光る）」
- あなた: 「秋葉原のカレー屋さんを教えて」
- Clova: 「秋葉原のオススメのカレー屋はフジヤマドラゴンカレーです。」 (← Java で記述している)

の様にやりとりできれば成功です。

# おわりに

Bot（Messaging API）を作成する [line-bot-sdk-java](https://github.com/line/line-bot-sdk-java) と同じように、Clova でも [Clova CEK SDK Java](https://github.com/line/clova-cek-sdk-java) を使えば簡単にJavaでClovaスキルを作成できそうです。

なお、対応するスロット（札幌, 千歳）や応答するメッセージが異なりますが、同等のソースコードを [gishi-yama/ClovaSample](https://github.com/gishi-yama/ClovaSample) に配置してあります。

引き続き、Botとの連携方法なども挑戦してみたいです。

**宣伝**

[Java で LINE Bot を作るハンズオン資料](https://github.com/gishi-yama/linebot-java-handson)を公開しています！