外国株の情報を教えるAlexaスキル（Java）

#概要
アメリカの上場企業の現値を教えてくれるAlexaのスキルを作ります。
対話の流れとしては、

ユーザ「ibmの株価は？」
Alexa「ibmの株価は146.04ドルだよ」

といったものを作ります。
インターネット上にはNode.jsを用いたAlexaスキル開発の情報が溢れかえっているので、今回はあえてNode.jsではなくJavaを用いて実装したいと思います。
#必要な準備
Amazon Developerのアカウント及びAWSのアカウントが必要となります。どちらも無料で取得することができます。
アカウントの作成は、それぞれ公式サイトを見ると難なくできると思います。
- [Amazon Developerのアカウント登録](https://dev.classmethod.jp/voice-assistant/how-to-safety-create-alexa-developers-account/)（厳密には公式サイトではないですが、クラスメソッドが作ったサイトなので半分公式サイトみたいなものです）
- [AWSのアカウント登録](https://aws.amazon.com/jp/register-flow/)

加えて、今回はJavaを用いてコードを書き、作成物をAWSのLambdaにアップロードすることになるので、AWS Toolkit for Eclipseを入れておくと便利です。公式サイトは[こちら](https://aws.amazon.com/jp/eclipse/)です。
尚、AWS Toolkit for Eclipseを用いなくとも作成物のJarファイルを作って手動でLambdaにアップロードする手段もあります。その際の手順は[こちら](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/lambda-java-how-to-create-deployment-package.html)。

#ユーザインターフェースの作成
前の章でAmazon Developerのアカウントが作成できたと思うので、ユーザインターフェースを作っていきます。
Amazon Developer上で定義するべきものは、**①インテント**と**②インテントスロット**です。①インテントとは、機能のようなもので、例えば今回作成する株価応答スキルであれば「株価応答インテント」のようなものを作る必要があります。②インテントスロットとは、ユーザが発話した単語を保持する変数のようなもので、今回であれば「企業名」のような変数が必要です。
抽象的でわかりにくい説明になりましたが、以下で具体的な手順を説明していく中で徐々にわかっていくと思います。

まず、AmazonDeveloperのコンソールにログインして、以下の画像の赤線で囲われた「スキルの作成」ボタンをクリックします。
![Screen Shot 2018-08-26 at 18.34.11.png](https://qiita-image-store.s3.amazonaws.com/0/283226/9fb3c6ec-f370-68b0-b362-a60acba28a4b.png)
その次の画面では、「スキル名」の欄に好きなスキル名を、「デフォルトの言語」を日本語とし、以下の画像内で赤線で囲われた「スキルを作成」ボタンをクリックします。
![Screen Shot 2018-08-26 at 18.47.17.png](https://qiita-image-store.s3.amazonaws.com/0/283226/6a9bce74-1290-c088-c879-8a74f65b053b.png)
スキルが無事作成されたと思います。遷移した画面で、赤線で囲われたボタンをクリックしインテントを作成します。
![Screen Shot 2018-08-26 at 18.52.09.png](https://qiita-image-store.s3.amazonaws.com/0/283226/74118aaf-9ea9-7b05-944b-88ba22be1012.png)
インテント名を"stockPrice"とし、インテントを作成します。
![Screen Shot 2018-08-26 at 18.54.38.png](https://qiita-image-store.s3.amazonaws.com/0/283226/f56cbe1f-ac6b-4686-cd29-e22d9750d16d.png)
インテントが作成できたら、まずインテントスロット（変数）を作成します。今回は"stockName"とします。スロットタイプ（変数の種類）は、企業名であるAMAZON.Corporationとします。
![Screen Shot 2018-08-26 at 19.00.13.png](https://qiita-image-store.s3.amazonaws.com/0/283226/6ecdcf80-bbb6-2386-a919-a2ea6fac03e3.png)
そして最後に、ユーザが発話するであろうことをサンプル発話に書き出していきます。スロットインテントを入れたい場合は、"{}"で囲って記述します。
![Screen Shot 2018-08-26 at 19.03.58.png](https://qiita-image-store.s3.amazonaws.com/0/283226/ebcf821a-3146-2051-b75b-c3dfeee8403a.png)

ここまでできたら、一旦Amazon Developerコンソールは閉じて、AWSのLambda関数に取り掛かります。

#情報の取得元
コードを書く前に、今回情報を取得する元について説明します。外国株の情報は、[IEX API](https://iextrading.com/developer/docs/)から取得します。
このAPIからはアメリカの上場企業の株価が無料で取得できます。キーだとかアカウントだとかは必要ありません。

リクエストはHTTPのGET通信で行います。リクエストのURLの例は、`https://api.iextrading.com/1.0/stock/aapl/delayed-quote`のようなものとなります。
上の例はアップルの現値のディレイの値を要求するものですが、これに対するレスポンスは、

```json
{
  "symbol":"AAPL",
  "delayedPrice":216.16,
  "high":216.9,
  "low":215.11,
  "delayedSize":1368917,
  "delayedPriceTime":1535140800463,
  "processedTime":1535141700732
}
```
のようなJSON形式のものになります。
#実際にコードを書く
[Alexa Skill Kitをダウンロード](https://github.com/alexa/alexa-skills-kit-sdk-for-java)しビルドパスに入れ、コードを作成します。
今回は①ForeignStocksSpeechletRequestStreamHandler.javaと②GetInfo.javaの二つのファイルを作ります。
まずは①から見ていきます。

```java:ForeignStocksSpeechletRequestStreamHandler.java
package com.amazon.asksdk.foreignStocks;

import java.util.HashSet;
import java.util.Set;

import com.amazon.speech.speechlet.Speechlet;
import com.amazon.speech.speechlet.lambda.SpeechletRequestStreamHandler;

public class ForeignStocksSpeechletRequestStreamHandler extends SpeechletRequestStreamHandler  {
    private static final Set<String> supportedApplicationIds;

    static {
        supportedApplicationIds = new HashSet<String>();
    }

        //一番最初に呼び出されるメソッド
	public ForeignStocksSpeechletRequestStreamHandler(Speechlet speechlet, Set<String> supportedApplicationIds) {
                //後述するGetInfoクラスのインスタンスを作る
		super(new GetInfo(), supportedApplicationIds);
	}
	
	public ForeignStocksSpeechletRequestStreamHandler() {
		super(new GetInfo(), supportedApplicationIds);
	}

}

```
後述するGetInfoクラスのインスタンスを作成しています。
一番最初に呼び出されるこのクラスは、お決まり的な書き方が多いです。カスタマイズしたい場合は、`new GetInfo()`の部分を書き換える以外はあまりいじることができないです。

以下にGetInfo.javaのコードを記します。

```java:GetInfo.java
package com.amazon.asksdk.foreignStocks;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.amazon.speech.json.SpeechletRequestEnvelope;
import com.amazon.speech.slu.Intent;
import com.amazon.speech.slu.Slot;
import com.amazon.speech.speechlet.IntentRequest;
import com.amazon.speech.speechlet.LaunchRequest;
import com.amazon.speech.speechlet.SessionEndedRequest;
import com.amazon.speech.speechlet.SessionStartedRequest;
import com.amazon.speech.speechlet.SpeechletResponse;
import com.amazon.speech.speechlet.SpeechletV2;
import com.amazon.speech.ui.PlainTextOutputSpeech;
import com.amazon.speech.ui.Reprompt;
import com.amazon.speech.ui.StandardCard;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;

import org.json.JSONArray;
import org.json.JSONObject;


public class GetInfo implements SpeechletV2  {
    private static final Logger log = LoggerFactory.getLogger(GetInfo.class);

	@Override
    //インテントが呼び出された時の処理
	public SpeechletResponse onIntent(SpeechletRequestEnvelope<IntentRequest> requestEnvelope) {
    //IntentRequest型の変数requestの中に、ユーザからのリクエスト内容が保持される
        IntentRequest request = requestEnvelope.getRequest();
    //ログを出力する処理
    //ログはAWSのCloudWatchから確認することができます
        log.info("onIntent requestId={}, sessionId={}", request.getRequestId(),
                requestEnvelope.getSession().getSessionId());

    //呼び出されたインテントの名前を、requestの中から取得する
        Intent intent = request.getIntent();
        String intentName = (intent != null) ? intent.getName() : null;
        
    //stockPriceインテントが呼び出された時の処理
        if ("stockPrice".equals(intentName)) {
    //インテントスロット（変数）の値を取得し、slotValueに保持
            Slot slot = intent.getSlot("stockName");
            String slotValue = slot.getValue();
    //IEX APIから情報を取得するgetStockInfoを呼び出す（引数はスロットの値）
            String speechOutput = getStockInfo(slotValue);
    //一定時間返答がなかった場合、repromptという発話がなされる
            String repromptText = "他の株価も知りたい？";
    //画面がある場合、企業ロゴを表示する
            StandardCard card = getCardContent(slotValue);
    //ユーザへ返事を返す
            return newAskResponse(speechOutput, repromptText,card);
        }else if ("AMAZON.HelpIntent".equals(intentName)) {
    //ユーザが「ヘルプ」と発話した場合
            String speechOutput =
                    "こんにちは、このスキルは外国株の値段を教えてあげられるよ。どの銘柄の株価が知りたい？";
            String repromptText = "わからないことがあったら、「ヘルプ」って言ってね";
            StandardCard card = new StandardCard();
            card.setTitle("ヘルプ");
            card.setText("わからないことがあったら、「ヘルプ」って言ってね");

            return newAskResponse(speechOutput, repromptText,card);
    //ユーザが「ストップ」などと発話した場合
        } else if ("AMAZON.StopIntent".equals(intentName)) {
            PlainTextOutputSpeech outputSpeech = new PlainTextOutputSpeech();
            outputSpeech.setText("はい、黙ります");

            return SpeechletResponse.newTellResponse(outputSpeech);
    //上とほぼ同じ
        } else if ("AMAZON.CancelIntent".equals(intentName)) {
            PlainTextOutputSpeech outputSpeech = new PlainTextOutputSpeech();
            outputSpeech.setText("ばいばい。明日の相場は上向きだといいね");

            return SpeechletResponse.newTellResponse(outputSpeech);
    //インテント名が上記のどれにも当てはまらなかった場合
        } else {
        	PlainTextOutputSpeech outputSpeech = new PlainTextOutputSpeech();
            outputSpeech.setText("ごめんなさい、なんだかよくわからないです");
            
            return SpeechletResponse.newTellResponse(outputSpeech);
        }
	}

	@Override
    //「{スキル名}を開いて」などとユーザが発話し、スキルが起動した時に呼び出される処理
	public SpeechletResponse onLaunch(SpeechletRequestEnvelope<LaunchRequest> requestEnvelope) {
        log.info("onLaunch requestId={}, sessionId={}", requestEnvelope.getRequest().getRequestId(),
                requestEnvelope.getSession().getSessionId());

        String speechOutput =
                "こんにちは、このスキルは外国株の値段を教えてあげられるよ。どの銘柄の株価が知りたい？";
        String repromptText = "わからないことがあったら、「ヘルプ」って言ってね";
        StandardCard card = new StandardCard();
        card.setTitle("外国株");
        card.setText("どの銘柄の株価が知りたい？");

        return newAskResponse(speechOutput, repromptText, card);
	}

	@Override
    //セッション終了時の処理（今回はカラ）
	public void onSessionEnded(SpeechletRequestEnvelope<SessionEndedRequest> requestEnvelope) {
		log.info("onSessionEnded requestId={}, sessionId={}", requestEnvelope.getRequest().getRequestId(),
                requestEnvelope.getSession().getSessionId());
		
	}

	@Override
    //セッション起動時の処理（今回はカラ）
	public void onSessionStarted(SpeechletRequestEnvelope<SessionStartedRequest> requestEnvelope) {
        log.info("onSessionStarted requestId={}, sessionId={}", requestEnvelope.getRequest().getRequestId(),
                requestEnvelope.getSession().getSessionId());
		
	}
	
    //ユーザが発話した企業名を引数として受けつけ、それを銘柄コードに変換する(i.e. "アップル" => "aapl")
    //結果を変数stockに入れて、返す
	private String convertStockCode(String slotValue) {
		String stock = null;
		
		if(slotValue != null && (slotValue.contains("アップル") || slotValue.contains("apple")||(slotValue.contains("あっぷる")))) {
	        stock = "aapl";
	    } else if(slotValue.contains("ibm") || slotValue.contains("アイビーエム")||(slotValue.contains("アイ・ビー・エム"))){
	        stock = "ibm";
	    } else if(slotValue.contains("exon") || slotValue.contains("エクソン")||(slotValue.contains("モービル"))) {
	    	stock = "xom";
	    } else if(slotValue.contains("facebook") || slotValue.contains("フェースブック")||(slotValue.contains("フェイスブック"))) {
	    	stock = "fb";
	    } else if(slotValue.contains("twitter") || slotValue.contains("ツイッター")||(slotValue.contains("ついったー"))) {
	    	stock = "twtr";
	    } else if(slotValue.contains("goldman sachks") || slotValue.contains("ゴールドマン")||(slotValue.contains("サックス"))) {
	    	stock = "gs";
	    } else if(slotValue.contains("microsoft") || slotValue.contains("マイクロ")||(slotValue.contains("マイクロソフト"))) {
	    	stock = "msft";
	    } else if(slotValue.contains("マクドナルド") || slotValue.contains("マック")||(slotValue.contains("マクド"))) {
	    	stock = "mcd";
	    } else if(slotValue.contains("インテル") || slotValue.contains("intel")||(slotValue.contains("いんてる"))) {
	    	stock = "intc";
	    } else if(slotValue.contains("ダウデュポン") || slotValue.contains("デュポン")||(slotValue.contains("ダウ・デュポン"))) {
	    	stock = "dwdp";
	    } else if(slotValue.contains("boeing") || slotValue.contains("ボーイング")||(slotValue.contains("ボイング"))) {
	    	stock = "ba";
	    } else if(slotValue.contains("ウォルトディズニー") || slotValue.contains("ディズニー")||(slotValue.contains("disney"))) {
	    	stock = "dis";
	    } else if(slotValue.contains("verizon") || slotValue.contains("ベライゾン")||(slotValue.contains("ヴェライゾン"))) {
	    	stock = "vz";
	    }
		
		return stock;
	}
	
    //APIから情報を取得する
    //ユーザが発話した企業名を引数として受け取る
	public String getStockInfo(String slotValue) {
		String resultToReturn = null;
    //上で行った、企業名を銘柄コードに変換する処理を呼び出す
		String stock = convertStockCode(slotValue);
		
	    if(stock != null) {
    //IEX APIと通信する処理を呼び出す
	    	String temp = IEXApi(stock);
	    	if(temp.equals("ERROR")) {
	    		resultToReturn = "ごめんなさい、情報の取得に失敗しました";
	    	}else {
	    		resultToReturn = slotValue + "の株価は" + IEXApi(stock) + "ドルだよ";
	    	}
	    }else {
    //銘柄コードにうまく変換できなかった場合
	    	resultToReturn = "ごめんなさい、銘柄の名前が聞き取れないです";
	    }
	    return resultToReturn;
	}
	
    //IEX APIにリクエストを送り、レスポンスを受け取る
    //銘柄コードを引数として受け取る
	private String IEXApi(String slotValue) {
    //要求内容をurlに組み込む
		String IEX_URL = "https://api.iextrading.com/1.0/stock/" + slotValue + "/delayed-quote";
		String resultToReturn = null;
		try {
    //コネクションを作り、通信をする
    //この辺りはお決まりの書き方に従って書く
			URL url = new URL(IEX_URL);
			HttpURLConnection conn = (HttpURLConnection) url.openConnection();
			conn.setRequestMethod("GET");
			InputStream in = conn.getInputStream();
			BufferedReader reader = new BufferedReader(new InputStreamReader(in));
			StringBuilder output = new StringBuilder();
			String line;
			while ((line = reader.readLine()) != null) {
				output.append(line);
			}
			JSONObject data = new JSONObject(output.toString());
			double temp = data.getDouble("delayedPrice");
			resultToReturn = Double.toString(temp);
			return resultToReturn;
		}catch(IOException e) {
			return "ERROR";
		}
	}
	
    //画面がある場合に、企業のロゴを表示する
    //引数として、ユーザが発話した企業名を受け取る
	private StandardCard getCardContent(String slotValue) {
		StandardCard card = new StandardCard();
		String stock = convertStockCode(slotValue);
		String IMAGE_URL = null;
		
		if(stock != null) {
    //企業ロゴを取得するためのurlを作成
			IMAGE_URL = "https://api.iextrading.com/1.0/stock/" + stock + "/logo";
			try {
    //通信を行う
				URL url = new URL(IMAGE_URL);
				HttpURLConnection conn = (HttpURLConnection) url.openConnection();
				conn.setRequestMethod("GET");
				InputStream in = conn.getInputStream();
				BufferedReader reader = new BufferedReader(new InputStreamReader(in));
				StringBuilder output = new StringBuilder();
				String line;
				while ((line = reader.readLine()) != null) {
					output.append(line);
				}
				JSONObject data = new JSONObject(output.toString());
				String companyLogo = data.getString("url");
				com.amazon.speech.ui.Image image = new com.amazon.speech.ui.Image();
				image.setSmallImageUrl(companyLogo);
				image.setLargeImageUrl(companyLogo);
				card.setImage(image);
				card.setTitle(slotValue);
			}catch(IOException e) {
				card.setTitle("エラー");
				card.setText("画像が取得できませんでした");
			}
		}
		return card;
		
	}
	
	
    //発話内容、reprompt内容、画面表示内容を引数として受け取り、ユーザへの返答内容を組み立てる
    private SpeechletResponse newAskResponse(String stringOutput, String repromptText, StandardCard card) {
        PlainTextOutputSpeech outputSpeech = new PlainTextOutputSpeech();
        outputSpeech.setText(stringOutput);

        PlainTextOutputSpeech repromptOutputSpeech = new PlainTextOutputSpeech();
        repromptOutputSpeech.setText(repromptText);
        Reprompt reprompt = new Reprompt();
        reprompt.setOutputSpeech(repromptOutputSpeech);

        return SpeechletResponse.newAskResponse(outputSpeech, reprompt, card);
    }
	

}

```

#動かしてみる
上記のコードが書けたら、AWSのLambda関数にコードをアップロードします。
公式サイトの[このページ](https://developer.amazon.com/ja/blogs/alexa/post/6e716e5c-55b0-445b-b936-9cfac4712e7b/training-1)を見れば、Lambda関数作成方法とAmazonDeveloper上のスキルの紐付け方がわかると思います。
ただし、今回はランタイムがJavaであることに注意してください。

Lambda関数が作成できたら、コードをアップロードします。
Eclipseでプロジェクトのフォルダを右クリックし、Amazon Web Service => Upload function to AWS Lambdaを選択します（以下に参考画像を貼ります）。
![Screen Shot 2018-08-26 at 20.14.34.png](https://qiita-image-store.s3.amazonaws.com/0/283226/d4ac3683-b9c2-f643-4950-2fb5ad5087ab.png)
その後遷移する画面でRegionはAsia Pacificを、アップロード先は先ほど作成したLambda関数を選択します。
![Screen Shot 2018-08-26 at 20.23.09.png](https://qiita-image-store.s3.amazonaws.com/0/283226/877e14c1-ce16-408f-6459-02c6072f9e88.png)

Nextボタンを押したら、最後にS3 Bucketを選択しFinishを押下します。始めた作成する場合はS３ Bucketがないと思うので、「Create」を押して作成してください。

ここまで出来たらテストができます。
動かしてみるとご覧の通り、ちゃんと株価を取得できていることがわかります。

![Screen Shot 2018-08-26 at 19.52.09.png](https://qiita-image-store.s3.amazonaws.com/0/283226/c3a0a020-b61d-5588-eb73-868b002d94f0.png)