diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITBackupTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITBackupTest.java index 6a2742c2d94..8de04fa3f0c 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITBackupTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITBackupTest.java @@ -245,7 +245,7 @@ public void testBackups() throws InterruptedException, ExecutionException { .build())); // Verifies that the database encryption has been properly set - testDatabaseEncryption(db1); + testDatabaseEncryption(db1, keyName); // Create two backups in parallel. String backupId1 = testHelper.getUniqueBackupId() + "_bck1"; @@ -314,7 +314,7 @@ public void testBackups() throws InterruptedException, ExecutionException { // Verifies that backup version time is the specified one testBackupVersionTime(backup1, versionTime); // Verifies that backup encryption has been properly set - testBackupEncryption(backup1); + testBackupEncryption(backup1, keyName); // Insert some more data into db2 to get a timestamp from the server. Timestamp commitTs = @@ -374,7 +374,7 @@ public void testBackups() throws InterruptedException, ExecutionException { testGetBackup(db2, backupId2, expireTime); testUpdateBackup(backup1); testCreateInvalidExpirationDate(db1); - testRestore(backup1, op1, versionTime); + testRestore(backup1, versionTime, keyName); testDelete(backupId2); testCancelBackupOperation(db1); @@ -447,17 +447,17 @@ private void testBackupVersionTime(Backup backup, Timestamp versionTime) { logger.info("Done verifying backup version time for " + backup.getId()); } - private void testDatabaseEncryption(Database database) { + private void testDatabaseEncryption(Database database, String expectedKey) { logger.info("Verifying database encryption for " + database.getId()); assertThat(database.getEncryptionConfig()).isNotNull(); - assertThat(database.getEncryptionConfig().getKmsKeyName()).isEqualTo(keyName); + assertThat(database.getEncryptionConfig().getKmsKeyName()).isEqualTo(expectedKey); logger.info("Done verifying database encryption for " + database.getId()); } - private void testBackupEncryption(Backup backup) { + private void testBackupEncryption(Backup backup, String expectedKey) { logger.info("Verifying backup encryption for " + backup.getId()); assertThat(backup.getEncryptionInfo()).isNotNull(); - assertThat(backup.getEncryptionInfo().getKmsKeyVersion()).isNotNull(); + assertThat(backup.getEncryptionInfo().getKmsKeyVersion()).contains(expectedKey); logger.info("Done verifying backup encryption for " + backup.getId()); } @@ -620,8 +620,7 @@ private void testDelete(String backupId) throws InterruptedException { logger.info("Finished delete tests"); } - private void testRestore( - Backup backup, OperationFuture backupOp, Timestamp versionTime) + private void testRestore(Backup backup, Timestamp versionTime, String expectedKey) throws InterruptedException, ExecutionException { // Restore the backup to a new database. String restoredDb = testHelper.getUniqueDatabaseId(); @@ -636,7 +635,7 @@ private void testRestore( final Restore restore = dbAdminClient .newRestoreBuilder(backup.getId(), DatabaseId.of(projectId, instanceId, restoredDb)) - .setEncryptionConfig(EncryptionConfigs.customerManagedEncryption(keyName)) + .setEncryptionConfig(EncryptionConfigs.customerManagedEncryption(expectedKey)) .build(); restoreOperation = dbAdminClient.restoreDatabase(restore); restoreOperationName = restoreOperation.getName(); @@ -687,7 +686,7 @@ private void testRestore( Timestamp.fromProto( reloadedDatabase.getProto().getRestoreInfo().getBackupInfo().getVersionTime())) .isEqualTo(versionTime); - testDatabaseEncryption(reloadedDatabase); + testDatabaseEncryption(reloadedDatabase, expectedKey); // Restoring the backup to an existing database should fail. try { diff --git a/samples/install-without-bom/pom.xml b/samples/install-without-bom/pom.xml index 0cbc50663cc..7b0b9b475b1 100644 --- a/samples/install-without-bom/pom.xml +++ b/samples/install-without-bom/pom.xml @@ -143,6 +143,9 @@ spanner-testing + us-central1 + spanner-test-keyring + spanner-test-key mysample quick-db diff --git a/samples/snapshot/pom.xml b/samples/snapshot/pom.xml index 0dec641ccf2..631718d85fd 100644 --- a/samples/snapshot/pom.xml +++ b/samples/snapshot/pom.xml @@ -142,6 +142,9 @@ spanner-testing + us-central1 + spanner-test-keyring + spanner-test-key mysample quick-db diff --git a/samples/snippets/pom.xml b/samples/snippets/pom.xml index f16cf9abc73..8ae0250e70f 100644 --- a/samples/snippets/pom.xml +++ b/samples/snippets/pom.xml @@ -147,6 +147,9 @@ spanner-testing + us-central1 + spanner-test-keyring + spanner-test-key mysample quick-db diff --git a/samples/snippets/src/main/java/com/example/spanner/CreateBackupWithEncryptionKey.java b/samples/snippets/src/main/java/com/example/spanner/CreateBackupWithEncryptionKey.java new file mode 100644 index 00000000000..b2a00ae2add --- /dev/null +++ b/samples/snippets/src/main/java/com/example/spanner/CreateBackupWithEncryptionKey.java @@ -0,0 +1,107 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.spanner; + +// [START spanner_create_backup_with_encryption_key] + +import com.google.api.gax.longrunning.OperationFuture; +import com.google.cloud.Timestamp; +import com.google.cloud.spanner.Backup; +import com.google.cloud.spanner.BackupId; +import com.google.cloud.spanner.DatabaseAdminClient; +import com.google.cloud.spanner.DatabaseId; +import com.google.cloud.spanner.Spanner; +import com.google.cloud.spanner.SpannerExceptionFactory; +import com.google.cloud.spanner.SpannerOptions; +import com.google.cloud.spanner.encryption.EncryptionConfigs; +import com.google.spanner.admin.database.v1.CreateBackupMetadata; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import org.threeten.bp.LocalDateTime; +import org.threeten.bp.OffsetDateTime; + +public class CreateBackupWithEncryptionKey { + + static void createBackupWithEncryptionKey() throws InterruptedException { + // TODO(developer): Replace these variables before running the sample. + String projectId = "my-project"; + String instanceId = "my-instance"; + String databaseId = "my-database"; + String backupId = "my-backup"; + String kmsKeyName = + "projects/" + projectId + "/locations//keyRings//cryptoKeys/"; + + try (Spanner spanner = + SpannerOptions.newBuilder().setProjectId(projectId).build().getService()) { + DatabaseAdminClient adminClient = spanner.getDatabaseAdminClient(); + createBackupWithEncryptionKey( + adminClient, + projectId, + instanceId, + databaseId, + backupId, + kmsKeyName); + } + } + + static Void createBackupWithEncryptionKey(DatabaseAdminClient adminClient, + String projectId, String instanceId, String databaseId, String backupId, String kmsKeyName) + throws InterruptedException { + // Set expire time to 14 days from now. + final Timestamp expireTime = Timestamp.ofTimeMicroseconds(TimeUnit.MICROSECONDS.convert( + System.currentTimeMillis() + TimeUnit.DAYS.toMillis(14), TimeUnit.MILLISECONDS)); + final Backup backupToCreate = adminClient + .newBackupBuilder(BackupId.of(projectId, instanceId, backupId)) + .setDatabase(DatabaseId.of(projectId, instanceId, databaseId)) + .setExpireTime(expireTime) + .setEncryptionConfig(EncryptionConfigs.customerManagedEncryption(kmsKeyName)) + .build(); + final OperationFuture operation = adminClient + .createBackup(backupToCreate); + + Backup backup; + try { + System.out.println("Waiting for operation to complete..."); + backup = operation.get(1200, TimeUnit.SECONDS); + } catch (ExecutionException e) { + // If the operation failed during execution, expose the cause. + throw SpannerExceptionFactory.asSpannerException(e.getCause()); + } catch (InterruptedException e) { + // Throw when a thread is waiting, sleeping, or otherwise occupied, + // and the thread is interrupted, either before or during the activity. + throw SpannerExceptionFactory.propagateInterrupt(e); + } catch (TimeoutException e) { + // If the operation timed out propagates the timeout + throw SpannerExceptionFactory.propagateTimeout(e); + } + + System.out.printf( + "Backup %s of size %d bytes was created at %s using encryption key %s%n", + backup.getId().getName(), + backup.getSize(), + LocalDateTime.ofEpochSecond( + backup.getProto().getCreateTime().getSeconds(), + backup.getProto().getCreateTime().getNanos(), + OffsetDateTime.now().getOffset()), + kmsKeyName + ); + + return null; + } +} +// [END spanner_create_backup_with_encryption_key] diff --git a/samples/snippets/src/main/java/com/example/spanner/CreateDatabaseWithEncryptionKey.java b/samples/snippets/src/main/java/com/example/spanner/CreateDatabaseWithEncryptionKey.java new file mode 100644 index 00000000000..ea559006c39 --- /dev/null +++ b/samples/snippets/src/main/java/com/example/spanner/CreateDatabaseWithEncryptionKey.java @@ -0,0 +1,101 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.spanner; + +// [START spanner_create_database_with_encryption_key] + +import com.google.api.gax.longrunning.OperationFuture; +import com.google.cloud.spanner.Database; +import com.google.cloud.spanner.DatabaseAdminClient; +import com.google.cloud.spanner.DatabaseId; +import com.google.cloud.spanner.Spanner; +import com.google.cloud.spanner.SpannerExceptionFactory; +import com.google.cloud.spanner.SpannerOptions; +import com.google.cloud.spanner.encryption.EncryptionConfigs; +import com.google.spanner.admin.database.v1.CreateDatabaseMetadata; +import java.util.Arrays; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +public class CreateDatabaseWithEncryptionKey { + + static void createDatabaseWithEncryptionKey() { + // TODO(developer): Replace these variables before running the sample. + String projectId = "my-project"; + String instanceId = "my-instance"; + String databaseId = "my-database"; + String kmsKeyName = + "projects/" + projectId + "/locations//keyRings//cryptoKeys/"; + + try (Spanner spanner = + SpannerOptions.newBuilder().setProjectId(projectId).build().getService()) { + DatabaseAdminClient adminClient = spanner.getDatabaseAdminClient(); + createDatabaseWithEncryptionKey( + adminClient, + projectId, + instanceId, + databaseId, + kmsKeyName); + } + } + + static Void createDatabaseWithEncryptionKey(DatabaseAdminClient adminClient, + String projectId, String instanceId, String databaseId, String kmsKeyName) { + final Database databaseToCreate = adminClient + .newDatabaseBuilder(DatabaseId.of(projectId, instanceId, databaseId)) + .setEncryptionConfig(EncryptionConfigs.customerManagedEncryption(kmsKeyName)) + .build(); + final OperationFuture operation = adminClient + .createDatabase(databaseToCreate, Arrays.asList( + "CREATE TABLE Singers (" + + " SingerId INT64 NOT NULL," + + " FirstName STRING(1024)," + + " LastName STRING(1024)," + + " SingerInfo BYTES(MAX)" + + ") PRIMARY KEY (SingerId)", + "CREATE TABLE Albums (" + + " SingerId INT64 NOT NULL," + + " AlbumId INT64 NOT NULL," + + " AlbumTitle STRING(MAX)" + + ") PRIMARY KEY (SingerId, AlbumId)," + + " INTERLEAVE IN PARENT Singers ON DELETE CASCADE" + )); + try { + System.out.println("Waiting for operation to complete..."); + Database createdDatabase = operation.get(120, TimeUnit.SECONDS); + + System.out.printf( + "Database %s created with encryption key %s%n", + createdDatabase.getId(), + createdDatabase.getEncryptionConfig().getKmsKeyName() + ); + } catch (ExecutionException e) { + // If the operation failed during execution, expose the cause. + throw SpannerExceptionFactory.asSpannerException(e.getCause()); + } catch (InterruptedException e) { + // Throw when a thread is waiting, sleeping, or otherwise occupied, + // and the thread is interrupted, either before or during the activity. + throw SpannerExceptionFactory.propagateInterrupt(e); + } catch (TimeoutException e) { + // If the operation timed out propagates the timeout + throw SpannerExceptionFactory.propagateTimeout(e); + } + return null; + } +} +// [END spanner_create_database_with_encryption_key] diff --git a/samples/snippets/src/main/java/com/example/spanner/RestoreBackupWithEncryptionKey.java b/samples/snippets/src/main/java/com/example/spanner/RestoreBackupWithEncryptionKey.java new file mode 100644 index 00000000000..4635031c0a2 --- /dev/null +++ b/samples/snippets/src/main/java/com/example/spanner/RestoreBackupWithEncryptionKey.java @@ -0,0 +1,97 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.spanner; + +// [START spanner_restore_backup_with_encryption_key] + +import com.google.api.gax.longrunning.OperationFuture; +import com.google.cloud.spanner.BackupId; +import com.google.cloud.spanner.Database; +import com.google.cloud.spanner.DatabaseAdminClient; +import com.google.cloud.spanner.DatabaseId; +import com.google.cloud.spanner.Restore; +import com.google.cloud.spanner.Spanner; +import com.google.cloud.spanner.SpannerExceptionFactory; +import com.google.cloud.spanner.SpannerOptions; +import com.google.cloud.spanner.encryption.EncryptionConfigs; +import com.google.spanner.admin.database.v1.RestoreDatabaseMetadata; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +public class RestoreBackupWithEncryptionKey { + + static void restoreBackupWithEncryptionKey() { + // TODO(developer): Replace these variables before running the sample. + String projectId = "my-project"; + String instanceId = "my-instance"; + String databaseId = "my-database"; + String backupId = "my-backup"; + String kmsKeyName = + "projects/" + projectId + "/locations//keyRings//cryptoKeys/"; + + try (Spanner spanner = + SpannerOptions.newBuilder().setProjectId(projectId).build().getService()) { + DatabaseAdminClient adminClient = spanner.getDatabaseAdminClient(); + restoreBackupWithEncryptionKey( + adminClient, + projectId, + instanceId, + backupId, + databaseId, + kmsKeyName); + } + } + + static Void restoreBackupWithEncryptionKey(DatabaseAdminClient adminClient, + String projectId, String instanceId, String backupId, String restoreId, String kmsKeyName) { + final Restore restore = adminClient + .newRestoreBuilder( + BackupId.of(projectId, instanceId, backupId), + DatabaseId.of(projectId, instanceId, restoreId)) + .setEncryptionConfig(EncryptionConfigs.customerManagedEncryption(kmsKeyName)) + .build(); + final OperationFuture operation = adminClient + .restoreDatabase(restore); + + Database database; + try { + System.out.println("Waiting for operation to complete..."); + database = operation.get(1600, TimeUnit.SECONDS); + } catch (ExecutionException e) { + // If the operation failed during execution, expose the cause. + throw SpannerExceptionFactory.asSpannerException(e.getCause()); + } catch (InterruptedException e) { + // Throw when a thread is waiting, sleeping, or otherwise occupied, + // and the thread is interrupted, either before or during the activity. + throw SpannerExceptionFactory.propagateInterrupt(e); + } catch (TimeoutException e) { + // If the operation timed out propagates the timeout + throw SpannerExceptionFactory.propagateTimeout(e); + } + + System.out.printf( + "Database %s restored to %s from backup %s using encryption key %s%n", + database.getRestoreInfo().getSourceDatabase(), + database.getId(), + database.getRestoreInfo().getBackup(), + database.getEncryptionConfig().getKmsKeyName() + ); + return null; + } +} +// [END spanner_restore_backup_with_encryption_key] diff --git a/samples/snippets/src/test/java/com/example/spanner/DatabaseIdGenerator.java b/samples/snippets/src/test/java/com/example/spanner/DatabaseIdGenerator.java new file mode 100644 index 00000000000..800db4d422a --- /dev/null +++ b/samples/snippets/src/test/java/com/example/spanner/DatabaseIdGenerator.java @@ -0,0 +1,36 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.spanner; + +import java.util.UUID; + +public class DatabaseIdGenerator { + + private static final int DATABASE_NAME_MAX_SIZE = 30; + private static final String BASE_DATABASE_ID = System.getProperty( + "spanner.sample.database", + "sampletest" + ); + + static String generateDatabaseId() { + return ( + BASE_DATABASE_ID + + "-" + + UUID.randomUUID().toString().replaceAll("-", "") + ).substring(0, DATABASE_NAME_MAX_SIZE); + } +} diff --git a/samples/snippets/src/test/java/com/example/spanner/EncryptionKeyIT.java b/samples/snippets/src/test/java/com/example/spanner/EncryptionKeyIT.java new file mode 100644 index 00000000000..13d84adf2b9 --- /dev/null +++ b/samples/snippets/src/test/java/com/example/spanner/EncryptionKeyIT.java @@ -0,0 +1,135 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.spanner; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.cloud.spanner.DatabaseAdminClient; +import com.google.cloud.spanner.Spanner; +import com.google.cloud.spanner.SpannerOptions; +import com.google.common.base.Preconditions; +import java.util.ArrayList; +import java.util.List; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** + * Integration tests for: {@link CreateDatabaseWithEncryptionKey}, {@link + * CreateBackupWithEncryptionKey} and {@link RestoreBackupWithEncryptionKey} + */ +@RunWith(JUnit4.class) +public class EncryptionKeyIT { + + private static String projectId; + private static final String instanceId = System.getProperty("spanner.test.instance"); + private static final List databasesToDrop = new ArrayList<>(); + private static final List backupsToDrop = new ArrayList<>(); + private static DatabaseAdminClient databaseAdminClient; + private static Spanner spanner; + private static String key; + + @BeforeClass + public static void setUp() { + final SpannerOptions options = SpannerOptions + .newBuilder() + .setAutoThrottleAdministrativeRequests() + .build(); + projectId = options.getProjectId(); + spanner = options.getService(); + databaseAdminClient = spanner.getDatabaseAdminClient(); + + String keyLocation = Preconditions + .checkNotNull(System.getProperty("spanner.test.key.location")); + String keyRing = Preconditions.checkNotNull(System.getProperty("spanner.test.key.ring")); + String keyName = Preconditions.checkNotNull(System.getProperty("spanner.test.key.name")); + key = "projects/" + projectId + "/locations/" + keyLocation + "/keyRings/" + keyRing + + "/cryptoKeys/" + keyName; + } + + @AfterClass + public static void tearDown() { + for (String databaseId : databasesToDrop) { + try { + databaseAdminClient.dropDatabase(instanceId, databaseId); + } catch (Exception e) { + System.out.println("Failed to drop database " + databaseId + ", skipping..."); + } + } + for (String backupId : backupsToDrop) { + try { + databaseAdminClient.deleteBackup(instanceId, backupId); + } catch (Exception e) { + System.out.println("Failed to drop backup " + backupId + ", skipping..."); + } + } + spanner.close(); + } + + @Test + public void testEncryptedDatabaseAndBackupAndRestore() throws Exception { + final String databaseId = DatabaseIdGenerator.generateDatabaseId(); + final String backupId = DatabaseIdGenerator.generateDatabaseId(); + final String restoreId = DatabaseIdGenerator.generateDatabaseId(); + + databasesToDrop.add(databaseId); + backupsToDrop.add(backupId); + databasesToDrop.add(restoreId); + + String out = SampleRunner.runSample(() -> + CreateDatabaseWithEncryptionKey.createDatabaseWithEncryptionKey( + databaseAdminClient, + projectId, + instanceId, + databaseId, + key + )); + assertThat(out).contains( + "Database projects/" + projectId + "/instances/" + instanceId + "/databases/" + databaseId + + " created with encryption key " + key); + + out = SampleRunner.runSample(() -> + CreateBackupWithEncryptionKey.createBackupWithEncryptionKey( + databaseAdminClient, + projectId, + instanceId, + databaseId, + backupId, + key + )); + assertThat(out).containsMatch( + "Backup projects/" + projectId + "/instances/" + instanceId + "/backups/" + backupId + + " of size \\d+ bytes was created at (.*) using encryption key " + key); + + out = SampleRunner.runSample(() -> + RestoreBackupWithEncryptionKey.restoreBackupWithEncryptionKey( + databaseAdminClient, + projectId, + instanceId, + backupId, + restoreId, + key + )); + assertThat(out).contains( + "Database projects/" + projectId + "/instances/" + instanceId + "/databases/" + databaseId + + " restored to projects/" + projectId + "/instances/" + instanceId + "/databases/" + + restoreId + " from backup projects/" + projectId + "/instances/" + instanceId + + "/backups/" + backupId + " using encryption key " + key); + } +} diff --git a/samples/snippets/src/test/java/com/example/spanner/SampleRunner.java b/samples/snippets/src/test/java/com/example/spanner/SampleRunner.java new file mode 100644 index 00000000000..13adf0f66e0 --- /dev/null +++ b/samples/snippets/src/test/java/com/example/spanner/SampleRunner.java @@ -0,0 +1,36 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.spanner; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.util.concurrent.Callable; + +/** + * Runs a sample and captures the output as a String. + */ +public class SampleRunner { + public static String runSample(Callable sample) throws Exception { + final PrintStream stdOut = System.out; + final ByteArrayOutputStream bout = new ByteArrayOutputStream(); + final PrintStream out = new PrintStream(bout); + System.setOut(out); + sample.call(); + System.setOut(stdOut); + return bout.toString(); + } +}