diff --git a/appengine-java8/pom.xml b/appengine-java8/pom.xml
index 1337c87abd4..f1759dc398d 100644
--- a/appengine-java8/pom.xml
+++ b/appengine-java8/pom.xml
@@ -64,12 +64,16 @@
multitenancy
oauth2
requests
- search
- sendgrid
remote-client
remote-server
+ search
+
+ sendgrid
+
+ spanner
+
static-files
taskqueues-deferred
diff --git a/appengine-java8/spanner/README.md b/appengine-java8/spanner/README.md
new file mode 100644
index 00000000000..4f62a04b072
--- /dev/null
+++ b/appengine-java8/spanner/README.md
@@ -0,0 +1,54 @@
+# Google Cloud Spanner Sample
+
+This sample demonstrates how to use [Google Cloud Spanner][spanner-docs]
+from [Google App Engine standard environment][ae-docs].
+
+[spanner-docs]: https://cloud.google.com/spanner/docs/
+[ae-docs]: https://cloud.google.com/appengine/docs/java/
+
+
+## Setup
+- Install the [Google Cloud SDK](https://cloud.google.com/sdk/) and run:
+```
+ gcloud init
+```
+If this is your first time creating an App engine application:
+```
+ gcloud app create
+```
+- [Create a Spanner instance](https://cloud.google.com/spanner/docs/quickstart-console#create_an_instance).
+
+- Update `SPANNER_INSTANCE` value in `[appengine-web.xml](src/main/webapp/WEB-INF/appengine-web.xml).
+
+## Endpoints
+- `/spanner` : will run sample operations against the spanner instance in order. Individual tasks can be run
+using the `task` query parameter. See [SpannerTasks](src/main/java/com/example/appengine/spanner/SpannerTasks.java)
+for supported set of tasks.
+Note : by default all the spanner example operations run in order, this operation may take a while to return.
+
+## Running locally
+- Authorize the local application:
+```
+ gcloud auth application-default login
+```
+You may also [create and use service account credentials](https://cloud.google.com/docs/authentication/getting-started#creating_the_service_account).
+
+- App Engine Maven plugins do not work correctly for this sample for local testing.
+ Here is the [tracking issue](https://github.com/GoogleCloudPlatform/google-cloud-java/issues/2155).
+ As a workaround to run locally, this sample uses the [Maven Jetty plugin](http://www.eclipse.org/jetty/documentation/9.4.x/jetty-maven-plugin.html).
+```
+ mvn -DSPANNER_INSTANCE=my-spanner-instance jetty:run
+```
+
+To see the results of the local application, open
+[http://localhost:8080/run](http://localhost:8080/run) in a web browser.
+Note : by default all the spanner example operations run in order, this operation may take a while to show results.
+
+## Deploying
+
+ $ mvn clean appengine:deploy
+
+To see the results of the deployed sample application, open
+`https://spanner-dot-PROJECTID.appspot.com/run` in a web browser.
+Note : by default all the spanner example operations run in order, this operation may take a while to show results.
+
diff --git a/appengine-java8/spanner/pom.xml b/appengine-java8/spanner/pom.xml
new file mode 100644
index 00000000000..25e842239a5
--- /dev/null
+++ b/appengine-java8/spanner/pom.xml
@@ -0,0 +1,92 @@
+
+
+
+ com.example.appengine
+ 4.0.0
+ appengine-spanner-j8
+ 1.0-SNAPSHOT
+ war
+
+ false
+
+
+
+ appengine-java8-samples
+ com.google.cloud
+ 1.0.0
+ ..
+
+
+
+
+ com.google.cloud
+ google-cloud-spanner
+ 0.19.0-beta
+
+
+ javax.servlet
+ javax.servlet-api
+ 3.1.0
+ provided
+
+
+ com.google.appengine
+ appengine-api-1.0-sdk
+ 1.9.53
+
+
+
+
+
+ ${project.build.directory}/${project.build.finalName}/WEB-INF/classes
+
+
+
+
+
+ org.eclipse.jetty
+ jetty-maven-plugin
+ 9.4.6.v20170531
+
+
+
+
+ org.apache.maven.plugins
+ 3.5.1
+ maven-compiler-plugin
+
+ 1.8
+ 1.8
+
+
+
+ com.google.cloud.tools
+ appengine-maven-plugin
+ 1.3.1
+
+ true
+ true
+
+
+
+
+
diff --git a/appengine-java8/spanner/src/main/java/com/example/appengine/spanner/SpannerClient.java b/appengine-java8/spanner/src/main/java/com/example/appengine/spanner/SpannerClient.java
new file mode 100644
index 00000000000..a0598a70d93
--- /dev/null
+++ b/appengine-java8/spanner/src/main/java/com/example/appengine/spanner/SpannerClient.java
@@ -0,0 +1,131 @@
+/**
+ * Copyright 2017 Google Inc.
+ *
+ *
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.appengine.spanner;
+
+import com.google.cloud.spanner.DatabaseAdminClient;
+import com.google.cloud.spanner.DatabaseClient;
+import com.google.cloud.spanner.DatabaseId;
+import com.google.cloud.spanner.Spanner;
+import com.google.cloud.spanner.SpannerOptions;
+import java.io.IOException;
+import java.util.UUID;
+import javax.servlet.ServletContext;
+import javax.servlet.ServletContextEvent;
+import javax.servlet.ServletContextListener;
+import javax.servlet.annotation.WebListener;
+
+// With @WebListener annotation the webapp/WEB-INF/web.xml is no longer required.
+@WebListener
+public class SpannerClient implements ServletContextListener {
+
+ private static String PROJECT_ID;
+ private static String INSTANCE_ID;
+ private static String DATABASE_ID;
+
+ // The initial connection can be an expensive operation -- We cache this Connection
+ // to speed things up. For this sample, keeping them here is a good idea, for
+ // your application, you may wish to keep this somewhere else.
+ private static Spanner spanner = null;
+ private static DatabaseAdminClient databaseAdminClient = null;
+ private static DatabaseClient databaseClient = null;
+
+ private static ServletContext sc;
+
+ private static void connect() throws IOException {
+ if (INSTANCE_ID == null) {
+ if (sc != null) {
+ sc.log("environment variable SPANNER_INSTANCE need to be defined.");
+ }
+ return;
+ }
+ SpannerOptions options = SpannerOptions.newBuilder().build();
+ PROJECT_ID = options.getProjectId();
+ spanner = options.getService();
+ databaseAdminClient = spanner.getDatabaseAdminClient();
+ }
+
+ static DatabaseAdminClient getDatabaseAdminClient() {
+ if (databaseAdminClient == null) {
+ try {
+ connect();
+ } catch (IOException e) {
+ if (sc != null) {
+ sc.log("getDatabaseAdminClient ", e);
+ }
+ }
+ }
+ if (databaseAdminClient == null) {
+ if (sc != null) {
+ sc.log("Spanner : Unable to connect");
+ }
+ }
+ return databaseAdminClient;
+ }
+
+ static DatabaseClient getDatabaseClient() {
+ if (databaseClient == null) {
+ databaseClient =
+ spanner.getDatabaseClient(DatabaseId.of(PROJECT_ID, INSTANCE_ID, DATABASE_ID));
+ }
+ return databaseClient;
+ }
+
+ @Override
+ public void contextInitialized(ServletContextEvent event) {
+ if (event != null) {
+ sc = event.getServletContext();
+ if (INSTANCE_ID == null) {
+ INSTANCE_ID = sc.getInitParameter("SPANNER_INSTANCE");
+ }
+ }
+ //try system properties
+ if (INSTANCE_ID == null) {
+ INSTANCE_ID = System.getProperty("SPANNER_INSTANCE");
+ }
+
+ if (DATABASE_ID == null) {
+ DATABASE_ID = "db-" + UUID.randomUUID().toString().substring(0, 25);
+ }
+
+ try {
+ connect();
+ } catch (IOException e) {
+ if (sc != null) {
+ sc.log("SpannerConnection - connect ", e);
+ }
+ }
+ if (databaseAdminClient == null) {
+ if (sc != null) {
+ sc.log("SpannerConnection - No Connection");
+ }
+ }
+ if (sc != null) {
+ sc.log("ctx Initialized: " + INSTANCE_ID + " " + DATABASE_ID);
+ }
+ }
+
+ @Override
+ public void contextDestroyed(ServletContextEvent servletContextEvent) {
+ // App Engine does not currently invoke this method.
+ databaseAdminClient = null;
+ }
+
+ static String getInstanceId() {
+ return INSTANCE_ID;
+ }
+
+ static String getDatabaseId() {
+ return DATABASE_ID;
+ }
+}
diff --git a/appengine-java8/spanner/src/main/java/com/example/appengine/spanner/SpannerTasks.java b/appengine-java8/spanner/src/main/java/com/example/appengine/spanner/SpannerTasks.java
new file mode 100644
index 00000000000..d88938b6e11
--- /dev/null
+++ b/appengine-java8/spanner/src/main/java/com/example/appengine/spanner/SpannerTasks.java
@@ -0,0 +1,436 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * 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.appengine.spanner;
+
+import com.google.cloud.spanner.Database;
+import com.google.cloud.spanner.DatabaseClient;
+import com.google.cloud.spanner.Key;
+import com.google.cloud.spanner.KeySet;
+import com.google.cloud.spanner.Mutation;
+import com.google.cloud.spanner.Operation;
+import com.google.cloud.spanner.ReadOnlyTransaction;
+import com.google.cloud.spanner.ResultSet;
+import com.google.cloud.spanner.Statement;
+import com.google.cloud.spanner.Struct;
+import com.google.common.base.Stopwatch;
+import com.google.spanner.admin.database.v1.UpdateDatabaseDdlMetadata;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+class SpannerTasks {
+
+ enum Task {
+ createDatabase,
+ writeExampleData,
+ query,
+ read,
+ addMarketingBudget,
+ updateMarketingBudget,
+ queryMarketingBudget,
+ addIndex,
+ readUsingIndex,
+ queryUsingIndex,
+ addStoringIndex,
+ readStoringIndex,
+ writeTransaction,
+ readOnlyTransaction
+ }
+
+ /** Class to contain singer sample data. */
+ static class Singer {
+
+ final long singerId;
+ final String firstName;
+ final String lastName;
+
+ Singer(long singerId, String firstName, String lastName) {
+ this.singerId = singerId;
+ this.firstName = firstName;
+ this.lastName = lastName;
+ }
+ }
+
+ /** Class to contain album sample data. */
+ static class Album {
+
+ final long singerId;
+ final long albumId;
+ final String albumTitle;
+
+ Album(long singerId, long albumId, String albumTitle) {
+ this.singerId = singerId;
+ this.albumId = albumId;
+ this.albumTitle = albumTitle;
+ }
+ }
+
+ private static final List SINGERS =
+ Arrays.asList(
+ new Singer(1, "Marc", "Richards"),
+ new Singer(2, "Catalina", "Smith"),
+ new Singer(3, "Alice", "Trentor"),
+ new Singer(4, "Lea", "Martin"),
+ new Singer(5, "David", "Lomond"));
+
+ private static final List ALBUMS =
+ Arrays.asList(
+ new Album(1, 1, "Total Junk"),
+ new Album(1, 2, "Go, Go, Go"),
+ new Album(2, 1, "Green"),
+ new Album(2, 2, "Forever Hold Your Peace"),
+ new Album(2, 3, "Terrified"));
+
+ private static DatabaseClient databaseClient = null;
+
+ private static void createDatabase(PrintWriter pw) {
+ Iterable statements =
+ Arrays.asList(
+ "CREATE TABLE Singers (\n"
+ + " SingerId INT64 NOT NULL,\n"
+ + " FirstName STRING(1024),\n"
+ + " LastName STRING(1024),\n"
+ + " SingerInfo BYTES(MAX)\n"
+ + ") PRIMARY KEY (SingerId)",
+ "CREATE TABLE Albums (\n"
+ + " SingerId INT64 NOT NULL,\n"
+ + " AlbumId INT64 NOT NULL,\n"
+ + " AlbumTitle STRING(MAX)\n"
+ + ") PRIMARY KEY (SingerId, AlbumId),\n"
+ + " INTERLEAVE IN PARENT Singers ON DELETE CASCADE");
+ Database db =
+ SpannerClient.getDatabaseAdminClient()
+ .createDatabase(
+ SpannerClient.getInstanceId(), SpannerClient.getDatabaseId(), statements)
+ .waitFor()
+ .getResult();
+ pw.println("Created database [" + db.getId() + "]");
+ }
+
+ private static void writeExampleData(PrintWriter pw) {
+ List mutations = new ArrayList<>();
+ for (Singer singer : SINGERS) {
+ mutations.add(
+ Mutation.newInsertBuilder("Singers")
+ .set("SingerId")
+ .to(singer.singerId)
+ .set("FirstName")
+ .to(singer.firstName)
+ .set("LastName")
+ .to(singer.lastName)
+ .build());
+ }
+ for (Album album : ALBUMS) {
+ mutations.add(
+ Mutation.newInsertBuilder("Albums")
+ .set("SingerId")
+ .to(album.singerId)
+ .set("AlbumId")
+ .to(album.albumId)
+ .set("AlbumTitle")
+ .to(album.albumTitle)
+ .build());
+ }
+ SpannerClient.getDatabaseClient().write(mutations);
+ }
+
+ private static void query(PrintWriter pw) {
+ // singleUse() can be used to execute a single read or query against Cloud Spanner.
+ ResultSet resultSet =
+ SpannerClient.getDatabaseClient()
+ .singleUse()
+ .executeQuery(Statement.of("SELECT SingerId, AlbumId, AlbumTitle FROM Albums"));
+ while (resultSet.next()) {
+ pw.printf("%d %d %s\n", resultSet.getLong(0), resultSet.getLong(1), resultSet.getString(2));
+ }
+ }
+
+ private static void read(PrintWriter pw) {
+ ResultSet resultSet =
+ SpannerClient.getDatabaseClient()
+ .singleUse()
+ .read(
+ "Albums",
+ // KeySet.all() can be used to read all rows in a table. KeySet exposes other
+ // methods to read only a subset of the table.
+ KeySet.all(),
+ Arrays.asList("SingerId", "AlbumId", "AlbumTitle"));
+ while (resultSet.next()) {
+ pw.printf("%d %d %s\n", resultSet.getLong(0), resultSet.getLong(1), resultSet.getString(2));
+ }
+ }
+
+ private static void addMarketingBudgetColumnToAlbums(PrintWriter pw) {
+ Operation op =
+ SpannerClient.getDatabaseAdminClient()
+ .updateDatabaseDdl(
+ SpannerClient.getInstanceId(),
+ SpannerClient.getDatabaseId(),
+ Arrays.asList("ALTER TABLE Albums ADD COLUMN MarketingBudget INT64"),
+ null);
+ op.waitFor();
+ }
+
+ // Before executing this method, a new column MarketingBudget has to be added to the Albums
+ // table by applying the DDL statement "ALTER TABLE Albums ADD COLUMN MarketingBudget INT64".
+ private static void updateMarketingBudgetData() {
+ // Mutation can be used to update/insert/delete a single row in a table. Here we use
+ // newUpdateBuilder to create update mutations.
+ List mutations =
+ Arrays.asList(
+ Mutation.newUpdateBuilder("Albums")
+ .set("SingerId")
+ .to(1)
+ .set("AlbumId")
+ .to(1)
+ .set("MarketingBudget")
+ .to(100000)
+ .build(),
+ Mutation.newUpdateBuilder("Albums")
+ .set("SingerId")
+ .to(2)
+ .set("AlbumId")
+ .to(2)
+ .set("MarketingBudget")
+ .to(500000)
+ .build());
+ // This writes all the mutations to Cloud Spanner atomically.
+ SpannerClient.getDatabaseClient().write(mutations);
+ }
+
+ private static void writeWithTransaction() {
+ SpannerClient.getDatabaseClient()
+ .readWriteTransaction()
+ .run(
+ (transactionContext -> {
+ // Transfer marketing budget from one album to another. We do it in a transaction to
+ // ensure that the transfer is atomic.
+ Struct row =
+ transactionContext.readRow(
+ "Albums", Key.of(2, 2), Arrays.asList("MarketingBudget"));
+ long album2Budget = row.getLong(0);
+ // Transaction will only be committed if this condition still holds at the time of
+ // commit. Otherwise it will be aborted and the callable will be rerun by the
+ // client library.
+ if (album2Budget >= 300000) {
+ long album1Budget =
+ transactionContext
+ .readRow("Albums", Key.of(1, 1), Arrays.asList("MarketingBudget"))
+ .getLong(0);
+ long transfer = 200000;
+ album1Budget += transfer;
+ album2Budget -= transfer;
+ transactionContext.buffer(
+ Mutation.newUpdateBuilder("Albums")
+ .set("SingerId")
+ .to(1)
+ .set("AlbumId")
+ .to(1)
+ .set("MarketingBudget")
+ .to(album1Budget)
+ .build());
+ transactionContext.buffer(
+ Mutation.newUpdateBuilder("Albums")
+ .set("SingerId")
+ .to(2)
+ .set("AlbumId")
+ .to(2)
+ .set("MarketingBudget")
+ .to(album2Budget)
+ .build());
+ }
+ return null;
+ }));
+ }
+
+ private static void queryMarketingBudget(PrintWriter pw) {
+ // Rows without an explicit value for MarketingBudget will have a MarketingBudget equal to
+ // null.
+ ResultSet resultSet =
+ SpannerClient.getDatabaseClient()
+ .singleUse()
+ .executeQuery(Statement.of("SELECT SingerId, AlbumId, MarketingBudget FROM Albums"));
+ while (resultSet.next()) {
+ pw.printf(
+ "%d %d %s\n",
+ resultSet.getLong("SingerId"),
+ resultSet.getLong("AlbumId"),
+ // We check that the value is non null. ResultSet getters can only be used to retrieve
+ // non null values.
+ resultSet.isNull("MarketingBudget") ? "NULL" : resultSet.getLong("MarketingBudget"));
+ }
+ }
+
+ private static void addIndex() {
+ Operation op =
+ SpannerClient.getDatabaseAdminClient()
+ .updateDatabaseDdl(
+ SpannerClient.getInstanceId(),
+ SpannerClient.getDatabaseId(),
+ Arrays.asList("CREATE INDEX AlbumsByAlbumTitle ON Albums(AlbumTitle)"),
+ null);
+ op.waitFor();
+ }
+
+ // Before running this example, add the index AlbumsByAlbumTitle by applying the DDL statement
+ // "CREATE INDEX AlbumsByAlbumTitle ON Albums(AlbumTitle)".
+ private static void queryUsingIndex(PrintWriter pw) {
+ ResultSet resultSet =
+ SpannerClient.getDatabaseClient()
+ .singleUse()
+ .executeQuery(
+ // We use FORCE_INDEX hint to specify which index to use. For more details see
+ // https://cloud.google.com/spanner/docs/query-syntax#from-clause
+ Statement.of(
+ "SELECT AlbumId, AlbumTitle, MarketingBudget\n"
+ + "FROM Albums@{FORCE_INDEX=AlbumsByAlbumTitle}\n"
+ + "WHERE AlbumTitle >= 'Aardvark' AND AlbumTitle < 'Goo'"));
+ while (resultSet.next()) {
+ pw.printf(
+ "%d %s %s\n",
+ resultSet.getLong("AlbumId"),
+ resultSet.getString("AlbumTitle"),
+ resultSet.isNull("MarketingBudget") ? "NULL" : resultSet.getLong("MarketingBudget"));
+ }
+ }
+
+ private static void readUsingIndex(PrintWriter pw) {
+ ResultSet resultSet =
+ SpannerClient.getDatabaseClient()
+ .singleUse()
+ .readUsingIndex(
+ "Albums",
+ "AlbumsByAlbumTitle",
+ KeySet.all(),
+ Arrays.asList("AlbumId", "AlbumTitle"));
+ while (resultSet.next()) {
+ pw.printf("%d %s\n", resultSet.getLong(0), resultSet.getString(1));
+ }
+ }
+
+ private static void addStoringIndex() {
+ Operation op =
+ SpannerClient.getDatabaseAdminClient()
+ .updateDatabaseDdl(
+ SpannerClient.getInstanceId(),
+ SpannerClient.getDatabaseId(),
+ Arrays.asList(
+ "CREATE INDEX AlbumsByAlbumTitle2 ON Albums(AlbumTitle) STORING (MarketingBudget)"),
+ null);
+ op.waitFor();
+ }
+
+ // Before running this example, create a storing index AlbumsByAlbumTitle2 by applying the DDL
+ // statement "CREATE INDEX AlbumsByAlbumTitle2 ON Albums(AlbumTitle) STORING (MarketingBudget)".
+ private static void readStoringIndex(PrintWriter pw) {
+ // We can read MarketingBudget also from the index since it stores a copy of MarketingBudget.
+ ResultSet resultSet =
+ SpannerClient.getDatabaseClient()
+ .singleUse()
+ .readUsingIndex(
+ "Albums",
+ "AlbumsByAlbumTitle2",
+ KeySet.all(),
+ Arrays.asList("AlbumId", "AlbumTitle", "MarketingBudget"));
+ while (resultSet.next()) {
+ pw.printf(
+ "%d %s %s\n",
+ resultSet.getLong(0),
+ resultSet.getString(1),
+ resultSet.isNull("MarketingBudget") ? "NULL" : resultSet.getLong("MarketingBudget"));
+ }
+ }
+
+ private static void readOnlyTransaction(PrintWriter pw) {
+ // ReadOnlyTransaction must be closed by calling close() on it to release resources held by it.
+ // We use a try-with-resource block to automatically do so.
+ try (ReadOnlyTransaction transaction =
+ SpannerClient.getDatabaseClient().readOnlyTransaction()) {
+ ResultSet queryResultSet =
+ transaction.executeQuery(
+ Statement.of("SELECT SingerId, AlbumId, AlbumTitle FROM Albums"));
+ while (queryResultSet.next()) {
+ pw.printf(
+ "%d %d %s\n",
+ queryResultSet.getLong(0), queryResultSet.getLong(1), queryResultSet.getString(2));
+ }
+ ResultSet readResultSet =
+ transaction.read(
+ "Albums", KeySet.all(), Arrays.asList("SingerId", "AlbumId", "AlbumTitle"));
+ while (readResultSet.next()) {
+ pw.printf(
+ "%d %d %s\n",
+ readResultSet.getLong(0), readResultSet.getLong(1), readResultSet.getString(2));
+ }
+ }
+ }
+
+ static void runTask(Task task, PrintWriter pw) {
+ Stopwatch stopwatch = Stopwatch.createStarted();
+ switch (task) {
+ case createDatabase:
+ createDatabase(pw);
+ break;
+ case writeExampleData:
+ writeExampleData(pw);
+ break;
+ case query:
+ query(pw);
+ break;
+ case read:
+ read(pw);
+ break;
+ case addMarketingBudget:
+ addMarketingBudgetColumnToAlbums(pw);
+ break;
+ case updateMarketingBudget:
+ updateMarketingBudgetData();
+ break;
+ case queryMarketingBudget:
+ queryMarketingBudget(pw);
+ break;
+ case addIndex:
+ addIndex();
+ break;
+ case readUsingIndex:
+ readUsingIndex(pw);
+ break;
+ case queryUsingIndex:
+ queryUsingIndex(pw);
+ break;
+ case addStoringIndex:
+ addStoringIndex();
+ break;
+ case readStoringIndex:
+ readStoringIndex(pw);
+ break;
+ case readOnlyTransaction:
+ readOnlyTransaction(pw);
+ break;
+ case writeTransaction:
+ writeWithTransaction();
+ break;
+ default:
+ break;
+ }
+ stopwatch.stop();
+ pw.println(task + " in milliseconds : " + stopwatch.elapsed(TimeUnit.MILLISECONDS));
+ pw.println("====================================================================");
+ }
+}
diff --git a/appengine-java8/spanner/src/main/java/com/example/appengine/spanner/SpannerTasksServlet.java b/appengine-java8/spanner/src/main/java/com/example/appengine/spanner/SpannerTasksServlet.java
new file mode 100644
index 00000000000..55fc380fe1c
--- /dev/null
+++ b/appengine-java8/spanner/src/main/java/com/example/appengine/spanner/SpannerTasksServlet.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * 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.appengine.spanner;
+
+import com.example.appengine.spanner.SpannerTasks.Task;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+import javax.servlet.ServletException;
+import javax.servlet.annotation.WebServlet;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Example code for using the Cloud Spanner API. This example demonstrates all the common operations
+ * that can be done on Cloud Spanner. These are:
+ *
+ *
+ *
+ *
+ * Creating a Cloud Spanner database.
+ * Writing, reading and executing SQL queries.
+ * Writing data using a read-write transaction.
+ * Using an index to read and execute SQL queries over data.
+ *
+ *
+ * Individual tasks can be run using "tasks" query parameter. {@link SpannerTasks.Task} lists
+ * supported tasks. All tasks are run in order if no parameter or "tasks=all" is provided.
+ */
+// With @WebServlet annotation the webapp/WEB-INF/web.xml is no longer required.
+@WebServlet(value = "/spanner")
+public class SpannerTasksServlet extends HttpServlet {
+ @Override
+ protected void doGet(HttpServletRequest req, HttpServletResponse resp)
+ throws ServletException, IOException {
+ resp.setContentType("text");
+ PrintWriter pw = resp.getWriter();
+ try {
+ String tasksParam = req.getParameter("tasks");
+ List tasks;
+ if (tasksParam == null || tasksParam.equals("all")) {
+ // cycle through all operations in order
+ tasks = Arrays.asList(Task.values());
+ } else {
+ String[] tasksStr = tasksParam.split(",");
+ tasks = Arrays.stream(tasksStr).map(Task::valueOf).collect(Collectors.toList());
+ }
+
+ for (Task task : tasks) {
+ SpannerTasks.runTask(task, pw);
+ }
+ } catch (Exception e) {
+ e.printStackTrace(pw);
+ pw.append(e.getMessage());
+ resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+ }
+ }
+}
diff --git a/appengine-java8/spanner/src/main/webapp/WEB-INF/appengine-web.xml b/appengine-java8/spanner/src/main/webapp/WEB-INF/appengine-web.xml
new file mode 100644
index 00000000000..76377540fec
--- /dev/null
+++ b/appengine-java8/spanner/src/main/webapp/WEB-INF/appengine-web.xml
@@ -0,0 +1,28 @@
+
+
+
+
+ true
+ java8
+ spanner
+
+ 1
+
+
+
+
+
+
+
+