SheetAPI v4とDriveAPIを使ってJavaでスプレッドシートを操作する

Javaのwebアプリからスプレッドシートを操作したいという要望が仕事であったのだが、SheetAPIのv3については記事がいっぱいあるのだけどv4については少なくて使い方を試すのに苦労したので備忘録として書いておく。

## 1. 事前準備

### 1.1. サービスアカウントの作成

サービスアカウントの認証情報を持ったjsonファイルを作成。
作成の手順はあちらこちらのネット上にあるが、例えば以下を参照  
※SheetAPIだけでなくDriveAPIも有効にする必要あり  
[サービスアカウントで認証してGoogleSpreadsheetからデータを取得](https://dream-yt.github.io/post/spreadsheet-via-service-account/)

### 1.2. ユーザ側の作業

ユーザは、アプリからのアクセスを許可するスプレッドシートやドライブのフォルダに対し、サービスアカウント作成時に生成されたメールアドレスを共有設定に追加する。

## 2. 環境構築

今回はJavaのSpringBootプロジェクトでWEBアプリを作る想定で構築する。
依存性についてはMavenを使用。

### 2.1. バージョン

Javaの開発環境としての条件配下の通り。
- Java1.8以降
- Gradle2.3以降
- Mavenでも可（前提バージョンは確認できず）

### 2.2. 認証情報の配置

サービスアカウント作成時に生成したjsonファイルを、Mavenプロジェクトの `src/main/resources/` に配置する。
API使用時に読み込んでいる必要がある。

### 2.3. 依存性の設定

以下のライブラリを読み込む。
- google-api-client
- google-oauth-client-jetty
- google-auth-library-oauth2-http
- google-api-services-sheets
- google-api-services-drive

pom.xmlに記載する内容（バージョンは要確認）
```xml
<dependency>
    <groupId>com.google.api-client</groupId>
    <artifactId>google-api-client</artifactId>
    <version>1.33.1</version>
</dependency>
<dependency>
    <groupId>com.google.oauth-client</groupId>
    <artifactId>google-oauth-client-jetty</artifactId>
    <version>1.32.1</version>
</dependency>
<dependency>
    <groupId>com.google.apis</groupId>
    <artifactId>google-api-services-sheets</artifactId>
    <version>v4-rev612-1.25.0</version>
</dependency>
<dependency>
    <groupId>com.google.apis</groupId>
    <artifactId>google-api-services-drive</artifactId>
    <version>v3-rev197-1.25.0</version>
</dependency>
<dependency>
    <groupId>com.google.auth</groupId>
    <artifactId>google-auth-library-oauth2-http</artifactId>
    <version>1.6.0</version>
</dependency>
```

## 3. コーディング

分かりやすくするために、認証情報を取得する部分、実際の処理を行う部分のみ以下に記載する。

スプレッドシートの編集については下記のサイトを参照した。  
[JavaからGoogleSpreadsheetに書く その2(Java側)](https://qiita.com/pilot/items/be778ed87170f96ddaf7)

### 3.1. 認証情報を取得する

サービスアカウントの認証情報を持つjsonファイルを読み込んで、クレデンシャルのオブジェクトを返却する。

```java
public class AuthUtil {
	public static final List<String> SCOPES = Arrays.asList(DriveScopes.DRIVE, SheetsScopes.SPREADSHEETS);
	public static final String CREDENTIALS_FILE_PATH = "/JSONファイル名";
	public static final JsonFactory JSON_FACTORY = GsonFactory.getDefaultInstance();

    public static HttpRequestInitializer getCredentials() throws IOException {
    	
    	GoogleCredentials credential 
    		= ServiceAccountCredentials.fromStream(AuthUtil.class.getResourceAsStream(CREDENTIALS_FILE_PATH)).createScoped(SCOPES);
    	
    	HttpRequestInitializer requestInitializer = new HttpCredentialsAdapter(credential);
    	
    	return requestInitializer;
    }
}
```
`SCOPES`に指定している文字列で、APIで操作する範囲を指定する。この例ではGoogleドライブとスプレッドシートを操作する。

なお、古いバージョンでは以下の方法でクレデンシャルを返す例が見られるが、`GoogleCredential`は非推奨のクラスとなっている。

```java
GoogleCredential credential = GoogleCredential.fromStream(AuthUtil.class.getResourceAsStream(CREDENTIALS_FILE_PATH)).createScoped(SCOPES);
return credential;
```

### 3.2. 実際の処理を行う

ここからはスプレッドシートとGoogleドライブを実際に操作する例を、単純な処理で記載する。

#### (1) スプレッドシートのセルの値を取得

```java
final NetHttpTransport HTTP_TRANSPORT = GoogleNetHttpTransport.newTrustedTransport();

Sheets service = new Sheets.Builder(HTTP_TRANSPORT, AuthUtil.JSON_FACTORY, AuthUtil.getCredentials())
        .setApplicationName("/** 任意の文字列 **/")
        .build();

final String range = "シート1!A:B";

List<List<Object>> values = service.spreadsheets().values().get(/** スプレッドシートID **/, range).execute().getValues();
for (List row : values) {
    System.out.printf("%s, %s\n", row.get(0), row.get(1));
}
```
  - `get()`メソッドでスプレッドシートを指定して取得
  - `range`に値を取得するシート名とセルを指定

#### (2) スプレッドシートのセルに出力

```java
final NetHttpTransport HTTP_TRANSPORT = GoogleNetHttpTransport.newTrustedTransport();

Sheets service = new Sheets.Builder(HTTP_TRANSPORT, AuthUtil.JSON_FACTORY, AuthUtil.getCredentials())
        .setApplicationName("/** 任意の文字列 **/"))
        .build();

Object[][] values = { { 1, "A" }, { 2.1D, "B" }, { 2.50E-3, "C" }, { "2022/04/01", "D" } };
List<List<Object>> valueList = new ArrayList<>();
for (Object[] row : values) {
    valueList.add(Arrays.asList(row));
}

ValueRange valueRange = new ValueRange().setValues(valueList).setMajorDimension("ROWS");
String range = "シート1" + "!" + bijectiveBase26(1) + 1;
service.spreadsheets().values().update("/** スプレッドシートID **/", range, valueRange).setValueInputOption("USER_ENTERED").execute();
```

  - `Sheets` オブジェクトを取得する方法はお約束の書き方となる。
  - `Object[][]` の配列に出力するデータを代入する。
  - `setMajorDimension()`には`ROWS(行)`か`COLUMNS(列)`を設定（大概は行単位に処理していくのでROWSでよいかと）
  - `range` の文字列に対象となるシート名と開始セル位置を代入する。
  - `update()` メソッドに必要な情報を渡して`excute` で実行する。  
  - `setValueInputOption()`には`USER_ENTERED`か`RAWS`のどちらかを設定  
※ `bijectiveBase26()`はセルの列を取得するユーティリティメソッド

```java
// 列番号を列番号文字列に変換する
private String bijectiveBase26(int n) {
    StringBuilder buf = new StringBuilder();
    while (n != 0) {
        buf.append((char) ((n - 1) % 26 + 'A'));
        n = (n - 1) / 26;
    }
    return buf.reverse().toString();
}
```
#### (3) スプレッドシートの行追加

```java
service.spreadsheets().values().append(spreadsheetId, "シート1" + "!" + bijectiveBase26(1) + 1001, valueRange)
    .setValueInputOption("USER_ENTERED")
    .execute();
```
  - `append`で行追加を行う

#### (4) スプレッドシートに新規シートを追加する

```java
final NetHttpTransport HTTP_TRANSPORT = GoogleNetHttpTransport.newTrustedTransport();

Sheets service = new Sheets.Builder(HTTP_TRANSPORT, AuthUtil.JSON_FACTORY, AuthUtil.getCredentials())
        .setApplicationName("/** 任意の文字列 **/")
        .build();

SheetProperties sheetProperties = new SheetProperties().setTitle("追加シート");
AddSheetRequest addSheetRequest = new AddSheetRequest().setProperties(sheetProperties);
List<Request> requests = new ArrayList<>();
requests.add(new Request().setAddSheet(addSheetRequest));
service.spreadsheets().batchUpdate("/** スプレッドシートID **/", new BatchUpdateSpreadsheetRequest().setRequests(requests)).execute();
```
  - `batchUpdate()`の`execute()`を実行する
  - 引数の`Request`に追加するシートのオブジェクトをセットする

#### (5) スプレッドシートを取得する

```java
Spreadsheet spreadsheet = service.spreadsheets().get(spreadsheetId).setIncludeGridData(false).execute();
int sheetCount = spreadsheet.getSheets().size();
```
  - `spreadsheet`オブジェクトにある`getSheets()`にて、スプレッドシート内のシートを取得できる

#### (6) ドライブのフォルダにスプレッドシートを作成する

```java
final NetHttpTransport HTTP_TRANSPORT = GoogleNetHttpTransport.newTrustedTransport();
Drive driveService = new Drive.Builder(HTTP_TRANSPORT, AuthUtil.JSON_FACTORY, AuthUtil.getCredentials())
	.setApplicationName("/** 任意の文字列 **/")
	.build();

File file = new File().setName("新スプレッドシート")
	.setMimeType("application/vnd.google-apps.spreadsheet")
	.setParents(Arrays.asList("/** GoogleドライブのフォルダID **/"));
driveService.files().create(file).execute();
```

  - スプレッドシート自体の新規作成は`DriveAPI`で行う
  - `setMimeType()`に追加するファイルの種別を設定
  - `DriveAPI`で使用できる`Mimetype`については[Googolのガイド](https://developers.google.com/drive/api/guides/mime-types)を参照
  - `parents`はフォルダを表す

#### (7) ドライブのフォルダにあるスプレッドシート一覧を取得する

```java
final NetHttpTransport HTTP_TRANSPORT = GoogleNetHttpTransport.newTrustedTransport();

Drive driveService = new Drive.Builder(HTTP_TRANSPORT, AuthUtil.JSON_FACTORY, AuthUtil.getCredentials())
		.setApplicationName("/** 任意の文字列 **/")
		.build();

String query = String.format("'%s' in parents and mimeType = '%s' and trashed = false", 
		"/** GoogleドライブのフォルダID **/", "application/vnd.google-apps.spreadsheet");
FileList result = driveService.files().list().setQ(query).execute();
for (File file : result.getFiles()) {
    System.out.printf("Found file: %s (%s)\n",file.getName(), file.getId());
}
```

  - `setQ()`に検索条件を設定して検索を行う
  - クエリーのサンプルについては[ファイルとフォルダを検索する](https://developers.google.com/drive/api/guides/search-files)を参照

#### (8) ドライブのフォルダにあるスプレッドシートをコピーする

```java
final NetHttpTransport HTTP_TRANSPORT = GoogleNetHttpTransport.newTrustedTransport();

Drive driveService = new Drive.Builder(HTTP_TRANSPORT, AuthUtil.JSON_FACTORY, AuthUtil.getCredentials())
		.setApplicationName("/** 任意の文字列 **/")
		.build();

String query = String.format("'%s' in parents and mimeType = '%s' and trashed = false", 
		"/** GoogleドライブのフォルダID **/", "application/vnd.google-apps.spreadsheet");
FileList result = driveService.files().list().setQ(query).execute();
for (File file : result.getFiles()) {
	File metadata = new File().setName(file.getName() + "複製")
    		.setMimeType("application/vnd.google-apps.spreadsheet")
    		.setParents(Arrays.asList("/** GoogleドライブのフォルダID **/"));

    driveService.files().copy(file.getId(), metadata).execute();
}
```
  - `copy()`の引数にコピーしたいスプレッドシートのIDとコピー後のスプレッドシートの情報を指定してexecuteを実行する

#### (9) スプレッドシートIDからフォルダIDを取得する

```java
Drive driveService = new Drive.Builder(HTTP_TRANSPORT, AuthUtil.JSON_FACTORY, AuthUtil.getCredentials())
		.setApplicationName("/** 任意の文字列 **/")
		.build();

File file = driveService.files().get("/** スプレッドシートID **/").setFields("id, name, parents").execute();
System.out.println("Parents: " + file.getParents());
```

  - `setFields()`で検索時に取得する項目を指定
  - フォルダを示す項目名は`Parents`



以上