CAP JavaでリモートのREST APIを利用する

## はじめに
CAP JavaでリモートのREST APIを利用したい場合、CAP JavaはRESTプロトコルをサポートしていないので、SAP Cloud SDKを使うことが選択肢となります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/43b51b16-255b-c69f-7b9d-6e779e00596c.png)
https://cap.cloud.sap/docs/guides/using-services#supported-protocols

SAP Coud SDKには[OpenAPI Client](https://sap.github.io/cloud-sdk/docs/java/features/rest/generate-rest-client)という、APIにアクセスするためのクラスを生成してくれるツールが用意されています。生成されたクラスを使用することで、APIのエンドポイントや受け渡すデータ構造をあまり意識せずにコードを書くことができます。

この記事では、CAP + Cloud SDKを使い、[Authorization and Trust ManagementのAPI](https://api.sap.com/api/AuthorizationAPI/overview)に対してGETおよびPOSTリクエストをする方法について検証します。ソースコードは以下のGitリポジトリにあります。

https://github.com/miyasuta/cap-openapi

## 前提
- BTPサブアカウントにAPIにアクセスするためのDestinationを登録していること
設定に必要な情報はxsuaaのタイプ"apiaccess"のサービスインスタンスから取得

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/647246a1-1424-f0ea-d30a-03d4a68719de.png)

## 手順
1. API仕様（yamlファイル）を取得
1. API仕様をインポート
1. サービスの定義
1. イベントハンドラの実装
1. デプロイ
1. 開発環境からリモートサービスに接続

## 1. API仕様（yamlファイル）を取得
[SAP Business Accelerator Hub](https://api.sap.com/api/AuthorizationAPI/overview)にログインした状態で、以下の画面からAPI仕様をダウンロードします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/3d6f0415-fb6a-c1e4-e00f-c94fd6beca4f.png)

## 2. API仕様をインポート
CAPプロジェクト側で以下の設定を行います。
### 2.1. pom.xmlの設定
プロジェクトルートのpom.xmlに以下の設定を追加します。

```xml:pom.xml
	<properties>
		<cloud.sdk.version><最新のSDKバージョン></cloud.sdk.version>
        ...
  	</properties>

	<dependencyManagement>
		<dependencies>
            ...
			<dependency>
				<groupId>com.sap.cloud.sdk</groupId>
				<artifactId>sdk-modules-bom</artifactId>
				<version>${cloud.sdk.version}</version>
				<type>pom</type>
				<scope>import</scope>
			</dependency>
   		</dependencies>
   	</dependencyManagement>
```

srv/pom.xmlに以下のdependencyを追加します。
```xml:srv/pom.xml
		<dependency>
			<groupId>com.sap.cloud.sdk.datamodel</groupId>
			<artifactId>openapi-core</artifactId>
		</dependency>
		<dependency>
			<groupId>com.sap.cloud.sdk</groupId>
			<artifactId>sdk-core</artifactId>
		</dependency>
```

さらに、srv/pom.xmlに以下のプラグインを追加します。
```xml:srv/pom.xml
			<plugin>
				<groupId>com.sap.cloud.sdk.datamodel</groupId>
				<artifactId>openapi-generator-maven-plugin</artifactId>
				<version>${cloud.sdk.version}</version>
				<executions>
					<execution>
						<goals>
							<goal>generate</goal>
						</goals>
						<phase>generate-sources</phase>
					</execution>
				</executions>
				<configuration>
					<inputSpec>${project.basedir}/api-specs/AuthorizationAPI.yaml</inputSpec>
					<outputDirectory>${project.basedir}/src/gen/java</outputDirectory>
					<apiPackage>customer.openapi.generated.authorization.api</apiPackage>
					<modelPackage>customer.openapi.generated.authorization.model</modelPackage>
					<apiMaturity>released</apiMaturity>
				</configuration>
			</plugin>	
```
:::note warn
`outputDirectory`はsrc/gen/javaまたはsrc/main/java配下でないと、生成されたクラスをイベントハンドラから見つけられませんでした

:::


### 2.2. API仕様をインポート
`inputSpec`で指定したディレクトリにダウンロードしたyamlファイルを置き、以下のコマンドを実行します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/c50c38c1-8dec-aa3b-ed94-3cace6cef876.png)

```
mvn clean install
```
その結果、`outputDirectory`で指定したパスにクラスが生成されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/c05c3fa8-68cb-3db1-6f6e-5a875360d0da.png)

## 3. サービスの定義
以下のサービスを定義しました。疎通確認のため、簡単なファンクションとアクションのみ定義しています。

- **function getRoleCollections**: すべてのロールコレクションを取得
- **action createRoleCollection**: ロールコレクションを登録
※ロールコレクションは本来割り当てるロールを指定して登録するものですが、簡単にするため名前と説明のみ指定して登録します

```srv/
type SimpleRoleCollection {
    name: String;
    description: String; 
}

service AuthorizationService {
    function getRoleCollections() returns array of  SimpleRoleCollection;
    action createRoleCollection(roleCollection:SimpleRoleCollection) returns SimpleRoleCollection;
}
```

## 4. イベントハンドラの実装
### 4.1. クラスの形
Cloud SDKを使用してAPIを呼び出す場合の特徴として、`DestinationAccessor`および`HttpDestination`を使用してBTPに登録したDestinationを取得するところがあります。
また、API仕様をインポートすることで登録された`RoleCollectionsApi`のクラスを使用してAPIにアクセスします。

```java
package customer.cap_openapi.handlers;

...
import com.sap.cloud.sdk.cloudplatform.connectivity.DestinationAccessor;
import com.sap.cloud.sdk.cloudplatform.connectivity.HttpDestination;

import customer.openapi.generated.authorization.api.RoleCollectionsApi;
import customer.openapi.generated.authorization.model.RoleCollection;
import customer.openapi.generated.authorization.model.RoleCollectionImport;

@Component
@ServiceName(AuthorizationService_.CDS_NAME)
class AuthorizationServiceHandler implements EventHandler{

    Logger logger = LoggerFactory.getLogger(AuthorizationServiceHandler.class);
    HttpDestination destination = DestinationAccessor.getDestination("xsuaa").asHttp();
    RoleCollectionsApi rolecollectionApi = new RoleCollectionsApi(destination);

}
```

### 4.2. GETリクエスト
すべてのロールコレクションを取得するためのイベントハンドラを実装します。RoleCollectionsApiの`getRoleCollections`というメソッドを使用してリクエストを行います。

```java
    @On(event = GetRoleCollectionsContext.CDS_NAME)
    public void getRoleCollections(GetRoleCollectionsContext context) {
        logger.info("getRoleCollection handler");
        //Send request
        List<RoleCollection> response = rolecollectionApi.getRoleCollections();
        
        //Map response to SimpleRoleCollection Type
        List<SimpleRoleCollection> roleCollections = response.stream()
                .map(roleCollection -> {
                    SimpleRoleCollection simpleRoleCollection = SimpleRoleCollection.create();
                    simpleRoleCollection.setName(roleCollection.getName());
                    simpleRoleCollection.setDescription(roleCollection.getDescription());
                    return simpleRoleCollection;
                })
                .collect(Collectors.toList());
        context.setResult(roleCollections);
    }
```
API Clientを作成したことで、APIで定義されたエンドポイントをメソッドとして提案してくれます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/cf2f9bfa-243e-f014-1f53-3b3d8af68931.png)

### 4.3. POSTリクエスト
ロールコレクションを登録するためのイベントハンドラを実装します。ロールコレクションを登録するためのメソッド`createRoleCollection`は`RoleCollectionImport`というオブジェクトを引数にとるので、オブジェクトを作成して渡されたパラメータをマッピングしておきます。

```java
    @On(event = CreateRoleCollectionContext.CDS_NAME)
    public void createRoleCollection(CreateRoleCollectionContext context) {
        logger.info("createRoleCollection handler");
        //Map input to RoleCollectionImport Type
        RoleCollectionImport roleCollection = new RoleCollectionImport();
        roleCollection.setName(context.getRoleCollection().getName());
        roleCollection.setDescription(context.getRoleCollection().getDescription());
        
        //Send request
        RoleCollection created = rolecollectionApi.createRoleCollection(roleCollection);
        
        //Map response to SimpleRoleCollection Type
        SimpleRoleCollection simpleRoleCollection = SimpleRoleCollection.create();
        simpleRoleCollection.setName(created.getName());
        simpleRoleCollection.setDescription(created.getDescription());
        context.setResult(simpleRoleCollection);
    }
```

## 5. デプロイ
### 5.1. xsuaa, destinationを追加
以下のコマンドでxsuaaとdestinationサービスを使用するための設定を追加します。
```
cds add xsuaa,destination --for production
```

### 5.2. mta.yamlを追加
以下のコマンドでmta.yamlファイルを追加します。
```
cds add mta
```
### 5.3. ビルド、デプロイ
以下のコマンドでビルド、デプロイします。
```
mbt build -t gen --mtar mta.tar
cf deploy gen/mta.tar
```

## 6. 開発環境からリモートサービスに接続
以下の記事の7.1～7.2のステップを実行し、xsuaa, destinationのサービスインスタンスにローカルのCAPプロジェクトをバインドします。

https://qiita.com/tami/items/441aae3330cb1e678ef2

以下のコマンドでサービスを実行します。
```
cds bind --exec mvn spring-boot:run
```
`test.http`ファイルを作成し、リクエストを実行するとBTPに登録されたロールコレクションの取得、およびロールコレクションの登録ができます。

```
@server=http://localhost:8080

###
GET {{server}}/odata/v4/AuthorizationService/getRoleCollections()

###
POST {{server}}/odata/v4/AuthorizationService/createRoleCollection
Content-Type: application/json

{
    "roleCollection": {
        "name": "MyTest1",
        "description": "Created from BAS"
    }
}
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/feeee0e8-b68a-2359-6040-b9bbf67e706c.png)

Cloud SDK単体だとローカル実行時は[認証なし、またはベーシック認証しか使用できない](https://sap.github.io/cloud-sdk/docs/java/features/connectivity/running-locally)ということですが、CAPと組み合わせて使うことでハイブリッド実行ができるのは便利ですね。

## 参考
- [Generate a Typed OpenAPI Client | SAP Cloud SDK](https://sap.github.io/cloud-sdk/docs/java/features/rest/generate-rest-client)
- [How to use the OpenAPI client Generator of SAP Cloud SDK | SAP Blog](https://community.sap.com/t5/technology-blogs-by-sap/how-to-use-the-openapi-client-generator-of-sap-cloud-sdk/ba-p/13474926)