SalesforceからBoxへCSVファイル連携を実装する方法

## 概要
本記事では、SalesforceとBoxを連携させ、ApexからBoxへのファイル連携を実現する方法について解説します。具体的には、Salesforce側のリモートサイト登録から、Box側のカスタムアプリ作成、そしてApexコードによるBox APIへの連携手順を紹介します。

## 前提
- Box 連携時に使用する認証方式
  - https://ja.developer.box.com/guides/authentication/select/ 
  - ユーザー認証方式を使います
    - よくあるユーザー許可画面で同意してアクセストークンを取得する形式です

本来はサーバー内でアクセストークンの取得を完了させたいため、クレデンシャルクライアントフローを利用した連携が望ましいのですが、Boxの無料プランではこの方式が使用できないため、ここではユーザー認証方式をもとに説明します。

## 対象読者

- SalesforceからとりあえずBox APIを叩いてみたい方
- Salesforce開発者で、Apexを用いた連携実装に興味がある方
- Box APIを利用してクラウドストレージとシステム連携を検討している方
- Salesforceと外部システムの連携における基本的な設定や実装方法を学びたい方

## アクセストークンを取得してBox APIをコールするまでの流れ
- ユーザー認証フローだと下記の通りにしてアクセストークンを取得してBox APIをコールしていきます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3531653/4aa932dd-0630-4821-b917-c77d38edd43b.png)



## Salesforceリモートサイト登録
Salesforceから外部のBox APIにコールアウトするためには、まずBoxのAPIエンドポイントをリモートサイトとして登録する必要があります。

### 設定手順
1.  Salesforceの設定メニューから「リモートサイト設定」を開きます。
2.  「新規リモートサイト」をクリックし、下記2つのリモートサイトURLを登録します。
     - https://api.box.com ← Box認可サーバーのエンドポイント
     - https://upload.box.com ← Boxファイルアップロード処理のエンドポイント
  ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3531653/6d42de95-26da-436c-a20e-b261aa557e21.png)

## Box のカスタムアプリ作成
Boxとの連携には、Box側でカスタムアプリを作成する必要があります。
本来、クレデンシャルクライアントフロー（Client Credentials Flow）を利用するのが望ましいですが、Boxの無料プランではこの機能が利用できません。したがって、今回はユーザー認証（User Authentication）方式でカスタムアプリを作成し、アクセストークンを取得する方法を採用します。
詳細な仕様は以下の公式ドキュメントをご参照ください:
- https://ja.developer.box.com/guides/authentication/select/ 

### 設定手順
1. [Boxの開発者向けのコンソール画面](https://app.box.com/developers/console)を開きます
2. 「マイPlatformアプリ」画面を開き、「Platformアプリの作成」をクリック 
 ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3531653/dfe52556-bd04-4d4e-a48c-e1ddeeb9785e.png)
3. 「カスタムアプリ」を選択
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3531653/af55c4e0-1351-4b83-b141-5595de0b9472.png)
4. アプリ名など適宜入力
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3531653/005aa437-72a6-4241-a436-9415d3008402.png)
5.認証方法として「ユーザー認証」を選択。（本来は「」サーバー認証クライアント資格情報許可」にしたいところ）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3531653/5d2b3719-2f75-49b0-bf47-31c19e4ced0f.png)

## 認証情報のコピーとリダイレクトURLの指定

- 作成したアプリの「構成」タブに行くと、クライアントIdとクライアントシークレットがあるので保存しておいてください。
- リダイレクトURIには`http://localhost/`を登録しておいてください。
  - ユーザー認証方式なので、ブラウザで同意画面に同意した後のリダイレクト先として自身のホストを指定しておきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3531653/ce3cb507-525b-44ee-abb9-53b7ef3687bb.png)

## ApexからBox連携する際に必要なアクセストークンを取得するサンプルコード

以下は、ApexからBox APIに対してHTTPコールアウトを行い、ユーザー認証方式に基づいてアクセストークンを取得するサンプルコードです。
（※クレデンシャルクライアントフローが利用できない場合、ユーザー認証方式で実装する点に注意してください。）

```apex
public class BoxAuthService {
    // カスタム設定から取得する場合
    private  final String BOX_AUTH_URL = 'https://api.box.com/oauth2/token'
    // 必要に応じてカスタム設定やハードコーディングの値に変更してください
    private  final String CLIENT_ID = '{コピーしたクライアントID}';
    private  final String CLIENT_SECRET = '{コピーしたクライアントシークレット}';

    // 認可コードを使用してアクセストークンを取得
    public String getAccessTokenCodeGrant(String code) {
        HttpRequest req = new HttpRequest();
        req.setEndpoint(BOX_AUTH_URL);
        req.setMethod('POST');
        req.setHeader('Content-Type', 'application/x-www-form-urlencoded');

        String body = 'grant_type=authorization_code' +
                      '&client_id=' + CLIENT_ID +
                      '&client_secret=' + CLIENT_SECRET +
                      '&code=' + code;
        req.setBody(body);

        Http http = new Http();
        HTTPResponse res = http.send(req);

        if (res.getStatusCode() == 200) {
            System.debug('Response: ' + res.getBody());
            Map<String, Object> responseJson = (Map<String, Object>) JSON.deserializeUntyped(res.getBody());
            return (String) responseJson.get('access_token');
        } else {
            System.debug('Error: ' + res.getBody());
            throw new CalloutException('Failed to get Access Token from Box');
        }
    }
}
```

### どうやって認可コードを取得＋アクセストークンを取得していくのか？

- 今回は非常にローカルなやり方で取得します
1. ブラウザでBoxの認可サーバーへリクエストする
    ```shell
    https://account.box.com/api/oauth2/authorize?response_type=code&client_id={取得したクライアントId}&redirect_uri=http://localhost/
    ```
2. 認可許可の同意画面が出るので「Boxへのアクセス許可」を押す。（この時リダイレクト先にローカルホストが指定されていることを確認する）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3531653/37b7eb5c-b046-4554-b2dd-ef0b8368acf6.png)
3. リダイレクトURLに付与されている認可コードを取得する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3531653/ea9354b9-0f11-4c42-aaae-8151eeed5def.png)
4.開発者コンソールで上記Apexコードを実行する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3531653/fbcde247-af8f-45f1-be9b-a78ec32d6001.png)
5.実行すると下記の通りアクセストークンを取得することができる
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3531653/613a5165-9dc2-4d49-b688-056fb7604fcf.png)

もし、アクセストークンの取得が失敗したらログを見てみてください。だいたい期限切れが多いと思います。（結構すぐに認可コードの期限が切れます）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3531653/42f675ab-7118-4cec-98bc-1987e16bd7bb.png)


## CSV生成クラスとBoxアップロード用クラスのサンプルコード

### CSV生成クラス

```apex
public class CsvGenerator {
    // CSVデータを生成し、Blob形式で返す
    public static Blob generateCsv() {
        String csvContent = 'Name,Email,Phone\n';
        csvContent += 'Taro Yamada,taro@example.com,080-1234-5678\n';
        csvContent += 'Hanako Tanaka,hanako@example.com,090-8765-4321\n';
        return Blob.valueOf(csvContent);
    }
}
```

### Boxアップロード用クラス

- 書き換えてもらう部分
  - BOX_FOLDER_ID：自身が連携したいBoxフォルダのIdを設定
  - accessToken：先ほど取得したアクセストークンを設定してください
- 自身で設定を変えていただいた後に開発者コンソールなどで試してみてください。
```apex
public class BoxFileUploader {
    public static List<Results> uploadCsvToBox(String fileName) {
        String boundary = '--------------------------' + String.valueOf(Crypto.getRandomLong());

        // マルチパートフォームデータの先頭部分の作成
        Blob bodyStart = Blob.valueOf(
            '--' + boundary + '\r\n' +
            'Content-Disposition: form-data; name="attributes"\r\n' +
            'Content-Type: application/json\r\n\r\n' +
            '{"name":"' + fileName + '","parent":{"id":"' + BOX_FOLDER_ID + '"}}\r\n' +
            '--' + boundary + '\r\n' +
            'Content-Disposition: form-data; name="file"; filename="' + fileName + '"\r\n' +
            'Content-Type: text/csv\r\n\r\n'
        );

        // CSVデータの生成（CsvGeneratorクラスを利用）
        Blob csvBlob = CsvGenerator.generateCsv();

        // マルチパートフォームデータの終端部分の作成
        Blob bodyEnd = Blob.valueOf('\r\n--' + boundary + '--\r\n');

        // BlobをStringに変換して連結し、最終的なリクエストボディを生成
        String fullBodyString = bodyStart.toString() + csvBlob.toString() + bodyEnd.toString();
        Blob fullBody = Blob.valueOf(fullBodyString);

        // Access Tokenの取得
        BoxAuthService boxAuthService = new BoxAuthService();
        String accessToken = '';

        // HTTPリクエストの作成
        HttpRequest req = new HttpRequest();
        req.setEndpoint(BOX_UPLOAD_URL);
        req.setMethod('POST');
        req.setHeader('Authorization', 'Bearer ' + accessToken);
        req.setHeader('Content-Type', 'multipart/form-data; boundary=' + boundary);
        req.setHeader('Accept', 'application/json');
        req.setBodyAsBlob(fullBody);

        // リクエスト送信
        Http http = new Http();
        HTTPResponse res = http.send(req);

        Results result = new Results();
        if (res.getStatusCode() == 201) {
            System.debug('File uploaded successfully: ' + res.getBody());
            result.resultMessage = 'File uploaded successfully: ' + res.getBody();

        } else {
            System.debug('Upload failed: ' + res.getBody());
            result.resultMessage = 'Upload failed: ' + res.getBody();
            throw new CalloutException('Failed to upload CSV to Box');
        }

        return new List<Results>{result};
    }
```

少し解説すると、Box連携時のbodyデータにはマルチパートフォームデータという形式で設定する必要があります。
マルチパートフォームデータについては、[こちら](https://qiita.com/sivertigo/items/14957200af5ad953d63b)の方の記事が非常にわかりやすいので参考にしてみてください。

一応上記の例だと下記のようなイメージで送信されます。
```shell
// マルチパートフォームデータの例
// 1234567890の部分はランダムの文字列です。下記のように複数のセクション（パート）に区切って送信する感じ

// ----------------------------1234567890
// Content-Disposition: form-data; name="attributes"
// Content-Type: application/json

// {"name":"sample.csv","parent":{"id":"boxid"}}
// ----------------------------1234567890
// Content-Disposition: form-data; name="file"; filename="xxxxxxxx.csv"
// Content-Type: text/csv

// Name,Email,Phone
// Taro Yamada,taro@example.com,080-1234-5678
// Hanako Tanaka,hanako@example.com,090-8765-4321
// ----------------------------1234567890--
```

## まとめ

本記事では、SalesforceからBoxへのファイル連携を実現するための基本的な手順とサンプルコードをご紹介しました。

- **Salesforceリモートサイト登録** 
    - Box APIへのコールアウトを可能にするための設定方法を解説しました。
- **Box のカスタムアプリ作成**
    - 本来はクレデンシャルクライアントフローが望ましいものの、無料プランではユーザー認証方式を採用する必要がある点を説明しました。
- **Apexからの連携実装**
    -  サンプルコードを通して、アクセストークンの取得方法やHTTPコールアウトの実装例を紹介しました。