Spring の RestTemplate で Yahoo!ショッピング商品検索(v3) の API をコールするサンプルコード

## 概要

- Spring が提供する HTTP クライアント RestTemplate を使って、Yahoo!ショッピング商品検索 (v3)の API をコールするサンプルプログラムを作る

## 今回の環境

- Java 11 (AdoptOpenJDK 11.0.7+10)
- Spring Web 5.2.7
- Jackson Databind 2.11.0
- Gradle 6.5
- macOS Catalina

## ソースコード

### ソースコード一覧

```
├── build.gradle
└── src
    └── main
        └── java
            └── com
                └── example
                    ├── ErrorResponse.java
                    ├── ItemSearch.java
                    └── ResultSet.java
```

### build.gradle

```groovy
plugins {
  id 'application'
  id 'java'
}

group 'org.example'
version '0.0.1'
mainClassName = "com.example.ItemSearch"
sourceCompatibility = '11'

repositories {
  mavenCentral()
}

dependencies {

  // RestTemplate を使うために必要
  // https://mvnrepository.com/artifact/org.springframework/spring-web
  implementation 'org.springframework:spring-web:5.2.7.RELEASE'

  // JSON とクラスのマッピングに必要
  // https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind
  implementation 'com.fasterxml.jackson.core:jackson-databind:2.11.0'
}
```

### src/main/java/com/example/ItemSearch.java

```java
package com.example;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.HttpHeaders;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestClientResponseException;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;

import java.net.URI;

/**
 * ショッピング:商品検索（v3） - Yahoo!デベロッパーネットワーク
 * https://developer.yahoo.co.jp/webapi/shopping/shopping/v3/itemsearch.html
 */
public class ItemSearch {

  public static void main(String[] args) {

    try {
      String appid = args[0];
      String query = args[1];
      System.out.println("appid: " + appid); // アプリケーションID
      System.out.println("query: " + query); // 検索キーワード

      // 商品を検索
      ResultSet rs = new ItemSearch().search(appid, query);
      for (ResultSet.Hit hit : rs.hits) {
        System.out.println("**************************************************");
        System.out.println("商品名: " + hit.name);
        System.out.println("商品説明: " + hit.description);
        System.out.println("キャッチコピー: " + hit.headLine);
        System.out.println("76×76サイズの画像URL: " + hit.image.small);
        System.out.println("146×146サイズの画像URL: " + hit.image.medium);
        System.out.println("商品URL: " + hit.url);
        System.out.println("価格: " + hit.price);
      }
    } catch (Exception e) {
      System.out.println("エラー発生: " + e);
    }
  }

  /**
   * 商品を検索する。
   * @param appid アプリケーションID
   * @param query 検索キーワード
   * @return 検索結果
   * @throws Exception エラー発生時
   */
  public ResultSet search(String appid, String query) throws Exception {

    try {
      // API コール用の URL を組み立てる
      String baseurl = "https://shopping.yahooapis.jp/ShoppingWebService/V3/itemSearch";
      String url = UriComponentsBuilder.fromUri(new URI(baseurl))
        .queryParam("query", query) // 検索キーワード
        .queryParam("results", 2) // 検索結果2件まで
        .build(false)
        .encode()
        .toUriString();
      System.out.println("URL: " + url);

      // HTTP リクエスト情報を構築
      HttpHeaders reqHeaders = new HttpHeaders();
      reqHeaders.set("User-Agent", "Yahoo AppID: " + appid); // アプリケーションID
      RequestEntity req = RequestEntity.get(new URI(url))
        .headers(reqHeaders)
        .build();

      // API をコールして結果を取得
      RestTemplate restTemplate = new RestTemplate();
      ResponseEntity<ResultSet> res = restTemplate.exchange(req, ResultSet.class);
      return res.getBody();

    } catch (RestClientResponseException e) {

      // レスポンスのステータス情報を出力
      int statusCode = e.getRawStatusCode();
      String statusText = e.getStatusText();
      System.out.println("RestClientResponseException.RawStatusCode: " + statusCode);
      System.out.println("RestClientResponseException.StatusText: " + statusText);

      // エラー情報を出力
      String body = e.getResponseBodyAsString();
      ObjectMapper mapper = new ObjectMapper();
      ErrorResponse errorResponse = mapper.readValue(body, ErrorResponse.class);
      System.out.println("エラーメッセージ: " + errorResponse.error.message);

      throw e;

    } catch (Exception e) {
      throw e;
    }
  }
}
```

### src/main/java/com/example/ResultSet.java

検索結果のレスポンスを表すクラス。

```java
package com.example;

import java.util.List;

/**
 * 検索結果。
 */
public class ResultSet {

  public List<Hit> hits;

  public static class Hit {

    // hits/name string 商品名
    public String name;

    // hits/description string 商品説明
    public String description;

    // hits/headLine string キャッチコピー
    public String headLine;

    // hits/image
    public Image image;

    // hits/url string 商品URL
    public String url;

    // hits/price integer 価格
    public int price;
  }

  public static class Image {

    // hits/image/small string 76×76サイズの画像URL
    public String small;

    // hits/image/medium string 146×146サイズの画像URL
    public String medium;
  }
}
```

### src/main/java/com/example/ErrorResponse.java

エラーレスポンス情報を表すクラス。

```java
package com.example;

import com.fasterxml.jackson.annotation.JsonProperty;

/**
 * エラー情報。
 */
public class ErrorResponse {

  @JsonProperty("Error")
  public Error error;

  public static class Error {

    @JsonProperty("Message")
    public String message;
  }
}
```

## 実行例

### 使用可能なアプリケーションIDと検索キーワード「猫」を指定する

検索結果のレスポンスが ResultSet クラスにマッピングされて情報が出力されている。

```
$ gradle run -q --args="your_application_id 猫"
appid: your_application_id
query: 猫
URL: https://shopping.yahooapis.jp/ShoppingWebService/V3/itemSearch?query=%E7%8C%AB&results=2
**************************************************
商品名: 猫おもちゃ 魚ロボット 猫電動おもちゃ 猫自動おもちゃ 猫おもちゃ自動 猫おもちゃ電動 猫おもちゃ魚
商品説明: 【ペット用おもちゃ】 電動魚ロボット 魚ロボット おもちゃ猫 電動おもちゃ猫         <br>【自動水泳機能】 水センサーで自動的にオン/オフします。自動ロボットフィンを使用して設計されています         <br>　　　　　　　　水中に置かれるとすぐに動き始める、取り出すと自動的にオフになり、電力を節約します。        <br>【LEDライト付】LEDライトが内蔵されているので暗闇で光ります。光って動く魚に動物の狩猟本能がくすぐられます          <br>【商品使用シーン】 自動猫おもちゃ          <br>【適用ペット】 スコティッシュフォールド メインクーン ラグドール ロシアンブルー ブリティッシュ ショー ヘア         <br>                      サイベリアン キジトラ 野良猫 アメリカンカール スコティッシュ サバトラ ハチワレ 猫ちゃん<br>
キャッチコピー: ネコちゃんの狩猟本能を刺激する電動魚ロボット
76×76サイズの画像URL: https://item-shopping.c.yimg.jp/i/c/himawaridifang-store_robo-fish1
146×146サイズの画像URL: https://item-shopping.c.yimg.jp/i/g/himawaridifang-store_robo-fish1
商品URL: https://store.shopping.yahoo.co.jp/himawaridifang-store/robo-fish1.html
価格: 880
**************************************************
商品名: 猫　ハーネス　猫用リード　猫用品　ペット用品
商品説明: 脱走対策として<br><br>猫は通院やお引越しなどで慣れない移動をさせられると、パニックを起こして逃げ出そうとしたり、暴れたりすることが多いものです。そんな時、ペットケージに入れておくだけでなく、ハーネスを装着した上でペットケージに入れるという二重の備えをしておくと、とても安心です。また万が一、大災害の被害を受けて避難しなければならなくなった時にも脱走防止に役立つので、防災グッズの中に常備しておく飼い主も増えています。<br><br>お散歩用として<br><br>脱走防止目的だけでなく、猫のお散歩用として使用するのもおすすめです。ハーネスは猫の身体の動きを邪魔しにくいので、のびのびと運動できて良いストレス発散になることでしょう。ただし、猫の性格によっては外に連れ出すことがかえってストレスとなる場合もありますので、お散歩はくれぐれも猫の様子をみつつ、交通状況などにも気を配りながら行ってください。<br><br>カラーバリエーションは、赤・青・黒・ピンクの4色です。<br>ナイロン製<br><br>首回り16〜26センチ<br>胴回り26〜36センチ<br>リードの長さ110cm<br><br>検索キーワード：猫リード・猫ハーネス・猫用リード・ポイント消化・送料無料
キャッチコピー: 猫ハーネス猫リード猫用リード猫首輪
76×76サイズの画像URL: https://item-shopping.c.yimg.jp/i/c/sam-store_0030
146×146サイズの画像URL: https://item-shopping.c.yimg.jp/i/g/sam-store_0030
商品URL: https://store.shopping.yahoo.co.jp/sam-store/0030.html
価格: 599
```

### 使用不能なアプリケーションIDと検索キーワード「猫」を指定する

エラーレスポンスが ErrorResponse クラスにマッピングされて情報が出力されている。

```
$ gradle run -q --args="invalid_application_id 猫"
appid: invalid_application_id
query: 猫
URL: https://shopping.yahooapis.jp/ShoppingWebService/V3/itemSearch?query=%E7%8C%AB&results=2
RestClientResponseException.RawStatusCode: 403
RestClientResponseException.StatusText: Forbidden
エラーメッセージ: Your Request was Forbidden
エラー発生: org.springframework.web.client.HttpClientErrorException$Forbidden: 403 Forbidden: [{
"Error" : {
"Message" : "Your Request was Forbidden"
}
} ... (512 bytes)]
```

## 参考資料

- [ショッピング:商品検索（v3） \- Yahoo\!デベロッパーネットワーク](https://developer.yahoo.co.jp/webapi/shopping/shopping/v3/itemsearch.html)
- [RestTemplate \(Spring Framework 5\.2\.7\.RELEASE API\) \- Javadoc 日本語訳](https://spring.pleiades.io/spring/docs/5.2.7.RELEASE/javadoc-api/org/springframework/web/client/RestTemplate.html)
- [UriComponentsBuilder \(Spring Framework 5\.2\.7\.RELEASE API\) \- Javadoc 日本語訳](https://spring.pleiades.io/spring/docs/5.2.7.RELEASE/javadoc-api/org/springframework/web/util/UriComponentsBuilder.html)
- [ObjectMapper \(jackson\-databind 2\.11\.0 API\)](https://fasterxml.github.io/jackson-databind/javadoc/2.11/com/fasterxml/jackson/databind/ObjectMapper.html)