http://qiita.com/pilot/items/85bae4e5d5e35ffa7019 の続き
やったこと
- Java側
以下、Windows上で
後でeclipseにインポートするためのプロジェクトディレクトリ「mytestapp1」を作成
当該ディレクトリに以下のファイルを作成
build.gradle
apply plugin: "java"
apply plugin: "application"
apply plugin: "eclipse"
repositories {
mavenCentral()
}
dependencies {
compile "com.google.api-client:google-api-client:1.22.0"
compile "com.google.oauth-client:google-oauth-client-jetty:1.22.0"
compile "com.google.apis:google-api-services-drive:v3-rev49-1.22.0"
compile "com.google.apis:google-api-services-sheets:v4-rev34-1.22.0"
compile "org.slf4j:slf4j-api:1.7.21"
compile "org.slf4j:jul-to-slf4j:1.7.21" // Googleライブラリがjava.util.loggingを使っているため
compile "ch.qos.logback:logback-classic:1.1.7"
}
compileJava {
options.encoding = "UTF-8"
}
sourceCompatibility = "1.8"
targetCompatibility = "1.8"
mainClassName = "mytestapp1.MyTestApp1"
task wrapper(type: Wrapper) {
gradleVersion = "3.1"
}
コマンドプロンプトで当該ディレクトリに移動して以下を実行
mkdir src\main\java src\main\resources
gradle eclipse
eclipseで当該ディレクトリをインポート(「既存プロジェクトをワークスペースへ」)
「やったこと その1」の最後にダウンロードしたJSONファイルをsrc/main/resourcesに「google_client_secret.json」というファイル名で配置
eclipse上で以下の新規クラス・XMLを作成
src/main/java/mytestapp1/GoogleService.java
package mytestapp1;
import java.io.IOException;
import java.io.Reader;
import java.security.GeneralSecurityException;
import java.util.Collection;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.api.client.auth.oauth2.Credential;
import com.google.api.client.auth.oauth2.CredentialRefreshListener;
import com.google.api.client.auth.oauth2.TokenErrorResponse;
import com.google.api.client.auth.oauth2.TokenResponse;
import com.google.api.client.extensions.java6.auth.oauth2.AuthorizationCodeInstalledApp;
import com.google.api.client.extensions.jetty.auth.oauth2.LocalServerReceiver;
import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeFlow;
import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeFlow.Builder;
import com.google.api.client.googleapis.auth.oauth2.GoogleClientSecrets;
import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.json.JsonFactory;
import com.google.api.client.json.jackson2.JacksonFactory;
import com.google.api.client.util.store.FileDataStoreFactory;
import com.google.api.services.drive.Drive;
import com.google.api.services.drive.model.File;
import com.google.api.services.sheets.v4.Sheets;
public class GoogleService {
private static final Logger LOGGER = LoggerFactory.getLogger(GoogleService.class);
// Global instance of the JSON factory.
private static final JsonFactory JSON_FACTORY = JacksonFactory.getDefaultInstance();
// Global instance of the HTTP transport.
private static HttpTransport HTTP_TRANSPORT = null;
// Reader for client secret.
private Reader clientSecretReader;
// Collection of authorization scopes
private Collection<String> authorizationScopes;
// Directory to store user credentials
private String credentialStoreDirectory;
// application name
private String applicationName;
/**
* Constructor.
*
* @param clientSecretReader reader for client secret
* @param authorizationScopes collection of authorization scopes
* @param credentialStoreDirectory directory to store user credentials
* @param applicationName application name
*/
public GoogleService(Reader clientSecretReader, Collection<String> authorizationScopes, String credentialStoreDirectory, String applicationName) {
this.clientSecretReader = clientSecretReader;
this.authorizationScopes = authorizationScopes;
this.credentialStoreDirectory = credentialStoreDirectory;
this.applicationName = applicationName;
}
// Credential.
private Credential credential = null;
/**
* Creates an authorized Credential object.
*
* @throws IOException
* @throws GeneralSecurityException
*/
public void authorize() throws IOException, GeneralSecurityException {
if (credential != null) {
return;
}
// Load client secrets.
GoogleClientSecrets clientSecrets = GoogleClientSecrets.load(JSON_FACTORY, clientSecretReader);
// instance of the {@link FileDataStoreFactory}.
FileDataStoreFactory dataStoreFactory = new FileDataStoreFactory(new java.io.File(credentialStoreDirectory));
// ↑ Windowsでは「unable to change permissions~」ログが出力される
if (HTTP_TRANSPORT == null) {
HTTP_TRANSPORT = GoogleNetHttpTransport.newTrustedTransport();
}
// Build flow and trigger user authorization request.
Builder builder = new GoogleAuthorizationCodeFlow.Builder(HTTP_TRANSPORT, JSON_FACTORY, clientSecrets, authorizationScopes);
builder.setDataStoreFactory(dataStoreFactory).setAccessType("offline");
// ↑
// AccessType「offline」でRefreshTokenを得る(AccessTokenのexpire前60秒以後のAPI呼出時に自動refreshが行われるようになる)
builder.addRefreshListener(new CredentialRefreshListener() {
@Override
public void onTokenResponse(Credential credential, TokenResponse tokenResponse) throws IOException {
LOGGER.info("AccessTokenのrefreshが成功しました。(AccessToken=[{}], ExpiresInSeconds={}, RefreshToken=[{}])", credential.getAccessToken(), credential.getExpiresInSeconds(), credential.getRefreshToken());
}
@Override
public void onTokenErrorResponse(Credential credential, TokenErrorResponse tokenErrorResponse) throws IOException {
LOGGER.error("AccessTokenのrefreshが失敗しました。(Error=[{}], ErrorDescription=[{}], ErrorUri=[{}])", tokenErrorResponse.getError(), tokenErrorResponse.getErrorDescription(), tokenErrorResponse.getErrorUri());
}
});
// ↑ AccessTokenのrefresh後のListner
GoogleAuthorizationCodeFlow flow = builder.build();
credential = new AuthorizationCodeInstalledApp(flow, new LocalServerReceiver()).authorize("user");
// ↑ 初回はブラウザがGoogleの許可リクエスト画面を表示する(関連ログも出力される)
// → 「許可」押下でローカルJettyにリダイレクトされ、Credentialがファイルに保存される
// → 以後はCredentialファイルがある限りブラウザは起動しない(自動refreshのおかげ)
// → サーバで実行する場合はローカルPCで作成したCredentialファイルをサーバに配置しておく
// → 何らかのエラーでサーバ上のCredentialファイルが無効になった場合は当時のファイルを再度配置する
LOGGER.info("AccessTokenを取得しました。(AccessToken=[{}], ExpiresInSeconds={}, RefreshToken=[{}])", credential.getAccessToken(), credential.getExpiresInSeconds(), credential.getRefreshToken());
}
/**
* Build and return an authorized Drive client service.
*
* @return an authorized Drive client service
* @throws GeneralSecurityException
* @throws IOException
*/
public Drive getDriveService() throws IOException, GeneralSecurityException {
if (credential == null) {
authorize();
}
return new Drive.Builder(HTTP_TRANSPORT, JSON_FACTORY, credential).setApplicationName(applicationName).build();
}
/**
* Build and return an authorized Sheets API client service.
*
* @return an authorized Sheets API client service
* @throws GeneralSecurityException
* @throws IOException
*/
public Sheets getSheetsService() throws IOException, GeneralSecurityException {
if (credential == null) {
authorize();
}
return new Sheets.Builder(HTTP_TRANSPORT, JSON_FACTORY, credential).setApplicationName(applicationName).build();
}
SheetsWrapper getSheetsWrapperWithWorksheet(String folderName, String spreadsheetName, String worksheetName) throws IOException, GeneralSecurityException {
DriveWrapper driveWrapper = new DriveWrapper(getDriveService());
Sheets sheetsService = getSheetsService();
List<File> folderList = driveWrapper.searchFolder(folderName, DriveWrapper.ROOT_FOLDER_ID);
// ↑ AccessTokenのexpire前60秒以後、ここでAccessTokenのrefreshが実行される
String folderId = null;
if (folderList.size() == 0) {
folderId = driveWrapper.createFolder(folderName, DriveWrapper.ROOT_FOLDER_ID).getId();
LOGGER.info("フォルダ '{}' を作成しました。", folderName);
} else if (folderList.size() == 1) {
folderId = folderList.get(0).getId();
} else {
throw new IOException(String.format("フォルダ '%s' が複数存在しています。", folderName));
}
List<File> spreadsheetList = driveWrapper.searchSpreadsheet(spreadsheetName, folderId);
SheetsWrapper sheetsWrapper = null;
if (spreadsheetList.size() == 0) {
String spreadsheetId = driveWrapper.createSpreadsheet(spreadsheetName, folderId).getId();
sheetsWrapper = new SheetsWrapper(sheetsService, spreadsheetId);
LOGGER.info("スプレッドシート '{}' を作成しました。", spreadsheetName);
sheetsWrapper.renameWorksheet(0, worksheetName);
LOGGER.info("ワークシート '{}' を作成しました。", worksheetName);
} else if (spreadsheetList.size() == 1) {
String spreadsheetId = spreadsheetList.get(0).getId();
sheetsWrapper = new SheetsWrapper(sheetsService, spreadsheetId);
if (sheetsWrapper.getWorksheetProperties(worksheetName) == null) {
sheetsWrapper.addWorksheet(worksheetName);
LOGGER.info("ワークシート '{}' を作成しました。", worksheetName);
}
} else {
throw new IOException(String.format("スプレッドシート '%s' が複数存在しています。", spreadsheetName));
}
return sheetsWrapper;
}
}
src/main/java/mytestapp1/RequestUtil.java
package mytestapp1;
import java.io.IOException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.api.client.googleapis.json.GoogleJsonResponseException;
import com.google.api.client.googleapis.services.json.AbstractGoogleJsonClientRequest;
public class RequestUtil {
private static final Logger LOGGER = LoggerFactory.getLogger(RequestUtil.class);
private static final int RETRY_COUNT = 10;
private static final int RETRY_INTERVAL_SECONDS = 10;
static <R extends AbstractGoogleJsonClientRequest<T>, T> T executeWithRetry(R request) throws IOException {
for (int i = 0; i < (RETRY_COUNT - 1); i++) {
try {
return request.execute();
} catch (GoogleJsonResponseException e) {
if (e.getStatusCode() == 503) {
LOGGER.warn("Googleからリトライ可能なエラーが返却されました。", e);
LOGGER.warn("{}秒後にリトライを行います。({}/{}回目)", RETRY_INTERVAL_SECONDS * (i + 1), i + 1, RETRY_COUNT);
try {
Thread.sleep(RETRY_INTERVAL_SECONDS * (i + 1) * 1000L);
} catch (InterruptedException e2) {
// do nothing
}
}
}
}
return request.execute();
}
}
src/main/java/mytestapp1/DriveWrapper.java
package mytestapp1;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import com.google.api.services.drive.Drive;
import com.google.api.services.drive.model.File;
public class DriveWrapper {
private Drive driveService;
private static final String QUERY_FORMAT = "name = '%s' and '%s' in parents and mimeType = '%s' and trashed = false";
private static final String MIMETYPE_FOLDER = "application/vnd.google-apps.folder";
private static final String MIMETYPE_SPREADSHEET = "application/vnd.google-apps.spreadsheet";
public static final String ROOT_FOLDER_ID = "root";
public DriveWrapper(Drive driveService) {
this.driveService = driveService;
}
public List<File> searchFile(String name, String parentFolderId, String mimeType) throws IOException {
String query = String.format(QUERY_FORMAT, name, parentFolderId, mimeType);
return RequestUtil.executeWithRetry(driveService.files().list().setQ(query)).getFiles();
}
public List<File> searchFolder(String name, String parentFolderId) throws IOException {
return searchFile(name, parentFolderId, MIMETYPE_FOLDER);
}
public List<File> searchSpreadsheet(String name, String parentFolderId) throws IOException {
return searchFile(name, parentFolderId, MIMETYPE_SPREADSHEET);
}
public File createFile(String name, String parentFolderId, String mimeType) throws IOException {
File metadata = new File().setName(name).setMimeType(mimeType).setParents(Arrays.asList(parentFolderId));
return RequestUtil.executeWithRetry(driveService.files().create(metadata));
}
public File createFolder(String name, String parentFolderId) throws IOException {
return createFile(name, parentFolderId, MIMETYPE_FOLDER);
}
public File createSpreadsheet(String name, String parentFolderId) throws IOException {
return createFile(name, parentFolderId, MIMETYPE_SPREADSHEET);
}
}
src/main/java/mytestapp1/SheetsWrapper.java
package mytestapp1;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import com.google.api.services.sheets.v4.Sheets;
import com.google.api.services.sheets.v4.model.AddSheetRequest;
import com.google.api.services.sheets.v4.model.BatchUpdateSpreadsheetRequest;
import com.google.api.services.sheets.v4.model.GridProperties;
import com.google.api.services.sheets.v4.model.Request;
import com.google.api.services.sheets.v4.model.Sheet;
import com.google.api.services.sheets.v4.model.SheetProperties;
import com.google.api.services.sheets.v4.model.Spreadsheet;
import com.google.api.services.sheets.v4.model.UpdateSheetPropertiesRequest;
import com.google.api.services.sheets.v4.model.ValueRange;
public class SheetsWrapper {
private Sheets sheetsService;
private Spreadsheet spreadsheet;
public SheetsWrapper(Sheets sheetsService, String fileId) throws IOException {
this.sheetsService = sheetsService;
this.spreadsheet = RequestUtil.executeWithRetry(sheetsService.spreadsheets().get(fileId).setIncludeGridData(false));
}
public void renameWorksheet(int index, String newName) throws IOException {
SheetProperties sheetProperties = new SheetProperties().setIndex(index).setTitle(newName);
UpdateSheetPropertiesRequest updateSheetPropertiesRequest = new UpdateSheetPropertiesRequest().setProperties(sheetProperties).setFields("title");
List<Request> requests = new ArrayList<>();
requests.add(new Request().setUpdateSheetProperties(updateSheetPropertiesRequest));
RequestUtil.executeWithRetry(sheetsService.spreadsheets().batchUpdate(spreadsheet.getSpreadsheetId(), new BatchUpdateSpreadsheetRequest().setRequests(requests)));
spreadsheet = RequestUtil.executeWithRetry(sheetsService.spreadsheets().get(spreadsheet.getSpreadsheetId()).setIncludeGridData(false));
// ↑
// rename後のgetSheets().get().getProperties().getTitle()がrename前の値を返す問題の対応
}
// 見つからないときはnullを返す
public SheetProperties getWorksheetProperties(String worksheetName) {
for (int index = 0; index < spreadsheet.getSheets().size(); index++) {
Sheet ws = spreadsheet.getSheets().get(index);
if (ws.getProperties().getTitle().equals(worksheetName)) {
return ws.getProperties();
}
}
return null;
}
public void addWorksheet(String worksheetName) throws IOException {
SheetProperties sheetProperties = new SheetProperties().setTitle(worksheetName);
AddSheetRequest addSheetRequest = new AddSheetRequest().setProperties(sheetProperties);
List<Request> requests = new ArrayList<>();
requests.add(new Request().setAddSheet(addSheetRequest));
RequestUtil.executeWithRetry(sheetsService.spreadsheets().batchUpdate(spreadsheet.getSpreadsheetId(), new BatchUpdateSpreadsheetRequest().setRequests(requests)));
spreadsheet = RequestUtil.executeWithRetry(sheetsService.spreadsheets().get(spreadsheet.getSpreadsheetId()).setIncludeGridData(false));
// ↑ add後のgetSheets()がaddしたシートを返さない問題の対応
}
// 値取得
public List<List<Object>> getValues(String worksheetName, int startColNum, int startRowNum, int endColNum, int endRowNum) throws IOException {
boolean specifyColRow = startColNum > 0 || startRowNum > 0 || endColNum > 0 || endRowNum > 0;
StringBuilder rangeBuf = new StringBuilder();
rangeBuf.append(worksheetName);
rangeBuf.append(specifyColRow ? '!' : "");
rangeBuf.append((startColNum > 0) ? bijectiveBase26(startColNum) : "");
rangeBuf.append((startRowNum > 0) ? startRowNum : "");
rangeBuf.append(specifyColRow ? ':' : "");
rangeBuf.append((endColNum > 0) ? bijectiveBase26(endColNum) : "");
rangeBuf.append((endRowNum > 0) ? endRowNum : "");
return RequestUtil.executeWithRetry(sheetsService.spreadsheets().values().get(spreadsheet.getSpreadsheetId(), rangeBuf.toString())).getValues();
}
// 指定列で値の入っている最後の行の行番号を取得
// (ワークシート名のみ指定すれば「全列で」となるがメモリ節約のため列指定をできるようにしておく)
public int getLastRowNumberWithValue(String worksheetName, int colNum) throws IOException {
List<List<Object>> values = getValues(worksheetName, colNum, 0, colNum, 0);
if (values != null) {
return values.size();
}
return 0;
}
// 値書込
// 指定列・行が現在のワークシートの大きさを超える場合にはワークシートを必要なだけ拡張する
// 指定位置に既存の値があれば上書きする
public void setValues(String worksheetName, int startColNum, int startRowNum, Object[][] values) throws IOException {
List<List<Object>> valueList = new ArrayList<>();
for (Object[] row : values) {
valueList.add(Arrays.asList(row));
}
setValues(worksheetName, startColNum, startRowNum, valueList);
}
public void setValues(String worksheetName, int startColNum, int startRowNum, Object[] values) throws IOException {
setValues(worksheetName, startColNum, startRowNum, new Object[][] { values });
}
public void setValue(String worksheetName, int colNum, int rowNum, Object value) throws IOException {
setValues(worksheetName, colNum, rowNum, new Object[][] { { value } });
}
public void setValues(String worksheetName, int startColNum, int startRowNum, List<List<Object>> valueList) throws IOException {
ValueRange valueRange = new ValueRange().setValues(valueList).setMajorDimension("ROWS");
String range = worksheetName + "!" + bijectiveBase26(startColNum) + startRowNum;
// 開始位置が範囲内ならupdate、範囲外ならappend
// (updateは開始位置が範囲外だとエラー、開始位置さえ範囲内ならその後は必要に応じて列・行を追加してくれる)
// (appendは開始位置が範囲外でも必要に応じて列・行を追加してくれるが、開始位置が空がない場合に上書きしてくれない)
GridProperties gridProperties = getWorksheetProperties(worksheetName).getGridProperties();
if (startColNum <= gridProperties.getColumnCount() && startRowNum <= gridProperties.getRowCount()) {
RequestUtil.executeWithRetry(sheetsService.spreadsheets().values().update(spreadsheet.getSpreadsheetId(), range, valueRange).setValueInputOption("USER_ENTERED"));
} else {
RequestUtil.executeWithRetry(sheetsService.spreadsheets().values().append(spreadsheet.getSpreadsheetId(), range, valueRange).setValueInputOption("USER_ENTERED"));
}
}
// 列番号を列番号文字列に変換する
private String bijectiveBase26(int n) {
// https://gist.github.com/theazureshadow/4a5a032944f1c9bc0f4a より
StringBuilder buf = new StringBuilder();
while (n != 0) {
buf.append((char) ((n - 1) % 26 + 'A'));
n = (n - 1) / 26;
}
return buf.reverse().toString();
}
}
src/main/java/mytestapp1/CreateCredentialFile.java
package mytestapp1;
import java.io.IOException;
import java.security.GeneralSecurityException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.bridge.SLF4JBridgeHandler;
/**
* Google認証を行い、Credentialファイルを作成する。
*/
public class CreateCredentialFile {
private static final Logger LOGGER = LoggerFactory.getLogger(CreateCredentialFile.class);
/**
* Google認証を行い、Credentialファイルを作成する。<br>
* ブラウザが起動し、Googleの許可リクエスト画面が表示される。
*
* @param args
* @throws IOException
* @throws GeneralSecurityException
*/
public static void main(String[] args) throws IOException, GeneralSecurityException {
SLF4JBridgeHandler.removeHandlersForRootLogger();
SLF4JBridgeHandler.install();
MyTestApp1.getGoogleService().authorize();
LOGGER.info("Credentialファイルを保存しました。");
}
}
src/main/java/mytestapp1/MyTestApp1.java
package mytestapp1;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.security.GeneralSecurityException;
import java.util.Arrays;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.bridge.SLF4JBridgeHandler;
import com.google.api.services.drive.DriveScopes;
import com.google.api.services.drive.model.File;
import com.google.api.services.sheets.v4.Sheets;
import com.google.api.services.sheets.v4.SheetsScopes;
/**
* Googleスプレッドシートへの書き込みを行う。
*/
public class MyTestApp1 {
private static final Logger LOGGER = LoggerFactory.getLogger(MyTestApp1.class);
// Application name.
private static final String APPLICATION_NAME = "MyTestApp1";
// Return reader for Google client secret.
private static Reader getClientSecretReader() {
return new InputStreamReader(MyTestApp1.class.getResourceAsStream("/google_client_secret.json"));
}
// Google authorization scopes required by this application.
// If modifying these scopes, delete your previously saved credentials.
private static final List<String> AUTHORIZATION_SCOPE_LIST = Arrays.asList(DriveScopes.DRIVE, SheetsScopes.SPREADSHEETS);
// Directory to store Google user credentials for this application.
private static final String CREDENTIAL_STORE_DIRECTORY = System.getProperty("user.home") + "/.google_credentials/" + APPLICATION_NAME;
public static void main(String[] args) throws IOException, GeneralSecurityException {
SLF4JBridgeHandler.removeHandlersForRootLogger();
SLF4JBridgeHandler.install();
final String folderName = "フォルダ1";
final String spreadsheetName = "スプレッドシート1";
final String worksheetName = "ワークシート1";
// Build a new authorized API client service.
GoogleService googleService = getGoogleService();
SheetsWrapper sheetsWrapper = googleService.getSheetsWrapperWithWorksheet(folderName, spreadsheetName, worksheetName);
int lastRowNumberWithValue = sheetsWrapper.getLastRowNumberWithValue(worksheetName, 1);
// 値の入っている最後の行の次の行から書き込み
Object[][] values = { { 1, "A" }, { 2.1D, "B" }, { 2.50E-3, "C" } };
sheetsWrapper.setValues(worksheetName, 1, lastRowNumWithValue + 1, values);
LOGGER.info("書き込みました。");
}
static GoogleService getGoogleService() {
return new GoogleService(getClientSecretReader(), AUTHORIZATION_SCOPE_LIST, CREDENTIAL_STORE_DIRECTORY, APPLICATION_NAME);
}
private static Reader getClientSecretReader() {
return new InputStreamReader(MyTestApp1.class.getResourceAsStream("/google_client_secret.json"));
}
}
src/main/resources/logback.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE logback>
<configuration>
<contextListener class="ch.qos.logback.classic.jul.LevelChangePropagator">
<resetJUL>true</resetJUL>
</contextListener>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="STDOUT" />
</root>
</configuration>
- 実行
まずCreateCredentialFileクラスを実行すると、ブラウザが起動して以下の画面が表示される
(Googleログイン済のブラウザを用意しておくと良い)
「許可」押下で以下の画面が表示される
次にMyTestApp1クラスを実行すると、書き込みが行われる