diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties
index 6637cedb28e..c3150437037 100644
--- a/.mvn/wrapper/maven-wrapper.properties
+++ b/.mvn/wrapper/maven-wrapper.properties
@@ -1 +1 @@
-distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.3.9/apache-maven-3.3.9-bin.zip
\ No newline at end of file
+distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.5.0/apache-maven-3.5.0-bin.zip
diff --git a/appengine-java8/README.md b/appengine-java8/README.md
new file mode 100644
index 00000000000..62f932bda7e
--- /dev/null
+++ b/appengine-java8/README.md
@@ -0,0 +1,101 @@
+# Google App Engine Standard Environment Samples for Java 8
+
+This is a repository that contains Java code samples for [Google App Engine
+standard environment][ae-docs].
+
+[ae-docs]: https://cloud.google.com/appengine/docs/java/
+
+## Prerequisites
+
+### Download Maven
+
+These samples use the [Apache Maven][maven] build system. Before getting
+started, be sure to [download][maven-download] and [install][maven-install] it.
+When you use Maven as described here, it will automatically download the needed
+client libraries.
+
+[maven]: https://maven.apache.org
+[maven-download]: https://maven.apache.org/download.cgi
+[maven-install]: https://maven.apache.org/install.html
+
+### Create a Project in the Google Cloud Platform Console
+
+If you haven't already created a project, create one now. Projects enable you to
+manage all Google Cloud Platform resources for your app, including deployment,
+access control, billing, and services.
+
+1. Open the [Cloud Platform Console][cloud-console].
+1. In the drop-down menu at the top, select **Create a project**.
+1. Give your project a name.
+1. Make a note of the project ID, which might be different from the project
+ name. The project ID is used in commands and in configurations.
+
+[cloud-console]: https://console.cloud.google.com/
+
+
+## Samples
+
+### Hello World
+
+This sample demonstrates how to deploy an application on Google App Engine.
+
+- [Documentation][ae-docs]
+- [Code](helloworld)
+
+### Sending Email
+
+#### Sending Email with Mailgun
+
+This sample demonstrates how to send email using the [Mailgun API][mailgun-api].
+
+- [Documentation][mailgun-sample-docs]
+- [Code](mailgun)
+
+[mailgun-api]: https://documentation.mailgun.com/
+[mailgun-sample-docs]: https://cloud.google.com/appengine/docs/java/mail/mailgun
+
+#### Sending Email with SendGrid
+
+This sample demonstrates how to send email using the [SendGrid][sendgrid].
+
+- [Documentation][sendgrid-sample-docs]
+- [Code](sendgrid)
+
+[sendgrid]: https://sendgrid.com/docs/User_Guide/index.html
+[sendgrid-sample-docs]: https://cloud.google.com/appengine/docs/java/mail/sendgrid
+
+### Sending SMS with Twilio
+
+This sample demonstrates how to use [Twilio](https://www.twilio.com) on [Google
+App Engine standard environment][ae-docs].
+
+- [Documentation][twilio-sample-docs]
+- [Code](twilio)
+
+[twilio-sample-docs]: https://cloud.google.com/appengine/docs/java/sms/twilio
+
+### App Identity
+
+This sample demonstrates how to use the [App Identity API][appid] to discover
+the application's ID and assert identity to Google and third-party APIs.
+
+- [Documentation][appid]
+- [Code](appidentity)
+
+[appid]: https://cloud.google.com/appengine/docs/java/appidentity/
+
+### Other Samples
+
+- [Sample Applications][sample-apps]
+
+[sample-apps]: https://cloud.google.com/appengine/docs/java/samples
+
+
+## Contributing changes
+
+See [CONTRIBUTING.md](../CONTRIBUTING.md).
+
+## Licensing
+
+See [LICENSE](../LICENSE).
+
diff --git a/appengine-java8/analytics/pom.xml b/appengine-java8/analytics/pom.xml
new file mode 100644
index 00000000000..b4080ef1d4f
--- /dev/null
+++ b/appengine-java8/analytics/pom.xml
@@ -0,0 +1,119 @@
+
+
+ 4.0.0
+ war
+ 1.0-SNAPSHOT
+ com.example.appengine
+ appengine-analytics-j8
+
+
+ appengine-java8-samples
+ com.google.cloud
+ 1.0.0
+ ..
+
+
+
+
+ 1.8
+ 1.8
+ 1.9.52
+
+
+
+
+
+ com.google.appengine
+ appengine-api-1.0-sdk
+ ${appengine.sdk.version}
+
+
+
+ jstl
+ jstl
+ 1.2
+
+
+
+ org.apache.httpcomponents
+ httpclient
+ 4.5.3
+
+
+
+ javax.servlet
+ javax.servlet-api
+ 3.1.0
+ jar
+ provided
+
+
+
+
+ com.google.appengine
+ appengine-testing
+ ${appengine.sdk.version}
+ test
+
+
+ com.google.appengine
+ appengine-api-stubs
+ ${appengine.sdk.version}
+ test
+
+
+
+ junit
+ junit
+ 4.12
+ test
+
+
+ org.mockito
+ mockito-core
+ 2.7.22
+ test
+
+
+ com.jcabi
+ jcabi-matchers
+ 1.4
+
+
+ com.google.truth
+ truth
+ 0.32
+ test
+
+
+
+
+
+ ${project.build.directory}/${project.build.finalName}/WEB-INF/classes
+
+
+ com.google.cloud.tools
+ appengine-maven-plugin
+ 1.3.1
+
+ true
+ true
+
+
+
+
+
diff --git a/appengine-java8/analytics/src/main/java/com/example/appengine/analytics/AnalyticsServlet.java b/appengine-java8/analytics/src/main/java/com/example/appengine/analytics/AnalyticsServlet.java
new file mode 100644
index 00000000000..4fd2d6186b6
--- /dev/null
+++ b/appengine-java8/analytics/src/main/java/com/example/appengine/analytics/AnalyticsServlet.java
@@ -0,0 +1,64 @@
+/**
+ * Copyright 2015 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.analytics;
+
+import com.google.appengine.api.urlfetch.URLFetchService;
+import com.google.appengine.api.urlfetch.URLFetchServiceFactory;
+
+import org.apache.http.client.utils.URIBuilder;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+// [START example]
+@SuppressWarnings("serial")
+public class AnalyticsServlet extends HttpServlet {
+
+ @Override
+ public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException,
+ ServletException {
+ String trackingId = System.getenv("GA_TRACKING_ID");
+ URIBuilder builder = new URIBuilder();
+ builder.setScheme("http").setHost("www.google-analytics.com").setPath("/collect")
+ .addParameter("v", "1") // API Version.
+ .addParameter("tid", trackingId) // Tracking ID / Property ID.
+ // Anonymous Client Identifier. Ideally, this should be a UUID that
+ // is associated with particular user, device, or browser instance.
+ .addParameter("cid", "555")
+ .addParameter("t", "event") // Event hit type.
+ .addParameter("ec", "example") // Event category.
+ .addParameter("ea", "test action"); // Event action.
+ URI uri = null;
+ try {
+ uri = builder.build();
+ } catch (URISyntaxException e) {
+ throw new ServletException("Problem building URI", e);
+ }
+ URLFetchService fetcher = URLFetchServiceFactory.getURLFetchService();
+ URL url = uri.toURL();
+ fetcher.fetch(url);
+ resp.getWriter().println("Event tracked.");
+ }
+}
+// [END example]
diff --git a/appengine-java8/analytics/src/main/webapp/WEB-INF/appengine-web.xml b/appengine-java8/analytics/src/main/webapp/WEB-INF/appengine-web.xml
new file mode 100644
index 00000000000..796dfaa0ddd
--- /dev/null
+++ b/appengine-java8/analytics/src/main/webapp/WEB-INF/appengine-web.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+ true
+ java8
+
+
+
+
+
diff --git a/appengine-java8/analytics/src/main/webapp/WEB-INF/web.xml b/appengine-java8/analytics/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 00000000000..cb3033ed531
--- /dev/null
+++ b/appengine-java8/analytics/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+ analytics
+ com.example.appengine.analytics.AnalyticsServlet
+
+
+ analytics
+ /
+
+
+
diff --git a/appengine-java8/appidentity/README.md b/appengine-java8/appidentity/README.md
new file mode 100644
index 00000000000..49c1c62a30e
--- /dev/null
+++ b/appengine-java8/appidentity/README.md
@@ -0,0 +1,18 @@
+# App Identity sample for Google App Engine
+
+This sample demonstrates how to use the [App Identity API][appid] on [Google App
+Engine][ae-docs].
+
+[appid]: https://cloud.google.com/appengine/docs/java/appidentity/
+[ae-docs]: https://cloud.google.com/appengine/docs/java/
+
+## Running locally
+This example uses the
+[Maven Cloud SDK based plugin](https://cloud.google.com/appengine/docs/java/tools/using-maven).
+To run this sample locally:
+
+ $ mvn appengine:run
+
+## Deploying
+
+ $ mvn appengine:deploy
diff --git a/appengine-java8/appidentity/pom.xml b/appengine-java8/appidentity/pom.xml
new file mode 100644
index 00000000000..2add6af8a74
--- /dev/null
+++ b/appengine-java8/appidentity/pom.xml
@@ -0,0 +1,117 @@
+
+
+ 4.0.0
+ war
+ 1.0-SNAPSHOT
+ com.example.appengine
+ appengine-appidentity-j8
+
+
+ com.google.cloud
+ appengine-java8-samples
+ 1.0.0
+ ..
+
+
+
+ 1.8
+ 1.8
+ 1.9.52
+
+
+
+
+ com.google.appengine
+ appengine-api-1.0-sdk
+ ${appengine.sdk.version}
+
+
+
+ com.google.guava
+ guava
+ 20.0
+
+
+
+ org.json
+ json
+ 20160810
+
+
+
+ javax.servlet
+ javax.servlet-api
+ 3.1.0
+ jar
+ provided
+
+
+
+
+ com.google.appengine
+ appengine-api-stubs
+ ${appengine.sdk.version}
+ test
+
+
+ com.google.appengine
+ appengine-tools-sdk
+ ${appengine.sdk.version}
+ test
+
+
+
+ junit
+ junit
+ 4.12
+ test
+
+
+ org.mockito
+ mockito-all
+ 1.10.19
+ test
+
+
+ com.google.appengine
+ appengine-testing
+ ${appengine.sdk.version}
+ test
+
+
+ com.google.truth
+ truth
+ 0.32
+ test
+
+
+
+
+ ${project.build.directory}/${project.build.finalName}/WEB-INF/classes
+
+
+ com.google.cloud.tools
+ appengine-maven-plugin
+ 1.3.1
+
+ true
+ true
+
+
+
+
+
diff --git a/appengine-java8/appidentity/src/main/java/com/example/appengine/appidentity/IdentityServlet.java b/appengine-java8/appidentity/src/main/java/com/example/appengine/appidentity/IdentityServlet.java
new file mode 100644
index 00000000000..d9d72a704c9
--- /dev/null
+++ b/appengine-java8/appidentity/src/main/java/com/example/appengine/appidentity/IdentityServlet.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2015 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.appidentity;
+
+import com.google.apphosting.api.ApiProxy;
+
+import java.io.IOException;
+
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+@SuppressWarnings("serial")
+public class IdentityServlet extends HttpServlet {
+
+ // [START versioned_hostnames]
+ @Override
+ public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+ resp.setContentType("text/plain");
+ ApiProxy.Environment env = ApiProxy.getCurrentEnvironment();
+ resp.getWriter().print("default_version_hostname: ");
+ resp.getWriter()
+ .println(env.getAttributes().get("com.google.appengine.runtime.default_version_hostname"));
+ }
+ // [END versioned_hostnames]
+}
diff --git a/appengine-java8/appidentity/src/main/java/com/example/appengine/appidentity/SignForAppServlet.java b/appengine-java8/appidentity/src/main/java/com/example/appengine/appidentity/SignForAppServlet.java
new file mode 100644
index 00000000000..3a9df0301fe
--- /dev/null
+++ b/appengine-java8/appidentity/src/main/java/com/example/appengine/appidentity/SignForAppServlet.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2016 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.appidentity;
+
+import com.google.appengine.api.appidentity.AppIdentityService;
+import com.google.appengine.api.appidentity.AppIdentityServiceFactory;
+import com.google.appengine.api.appidentity.PublicCertificate;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
+import java.security.Signature;
+import java.security.SignatureException;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.util.Arrays;
+import java.util.Collection;
+
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+@SuppressWarnings("serial")
+public class SignForAppServlet extends HttpServlet {
+ private final AppIdentityService appIdentity;
+
+ public SignForAppServlet() {
+ appIdentity = AppIdentityServiceFactory.getAppIdentityService();
+ }
+
+ // [START asserting_identity_to_other_services]
+ // Note that the algorithm used by AppIdentity.signForApp() and
+ // getPublicCertificatesForApp() is "SHA256withRSA"
+
+ private byte[] signBlob(byte[] blob) {
+ AppIdentityService.SigningResult result = appIdentity.signForApp(blob);
+ return result.getSignature();
+ }
+
+ private byte[] getPublicCertificate() throws UnsupportedEncodingException {
+ Collection certs = appIdentity.getPublicCertificatesForApp();
+ PublicCertificate publicCert = certs.iterator().next();
+ return publicCert.getX509CertificateInPemFormat().getBytes("UTF-8");
+ }
+
+ private Certificate parsePublicCertificate(byte[] publicCert)
+ throws CertificateException, NoSuchAlgorithmException {
+ InputStream stream = new ByteArrayInputStream(publicCert);
+ CertificateFactory cf = CertificateFactory.getInstance("X.509");
+ return cf.generateCertificate(stream);
+ }
+
+ private boolean verifySignature(byte[] blob, byte[] blobSignature, PublicKey pk)
+ throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
+ Signature signature = Signature.getInstance("SHA256withRSA");
+ signature.initVerify(pk);
+ signature.update(blob);
+ return signature.verify(blobSignature);
+ }
+
+ private String simulateIdentityAssertion()
+ throws CertificateException, UnsupportedEncodingException, NoSuchAlgorithmException,
+ InvalidKeyException, SignatureException {
+ // Simulate the sending app.
+ String message = "abcdefg";
+ byte[] blob = message.getBytes();
+ byte[] blobSignature = signBlob(blob);
+ byte[] publicCert = getPublicCertificate();
+
+ // Simulate the receiving app, which gets the certificate, blob, and signature.
+ Certificate cert = parsePublicCertificate(publicCert);
+ PublicKey pk = cert.getPublicKey();
+ boolean isValid = verifySignature(blob, blobSignature, pk);
+
+ return String.format(
+ "isValid=%b for message: %s\n\tsignature: %s\n\tpublic cert: %s",
+ isValid,
+ message,
+ Arrays.toString(blobSignature),
+ Arrays.toString(publicCert));
+ }
+ // [END asserting_identity_to_other_services]
+
+ @Override
+ public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+ resp.setContentType("text/plain");
+ try {
+ resp.getWriter().println(simulateIdentityAssertion());
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/appengine-java8/appidentity/src/main/java/com/example/appengine/appidentity/UrlShortener.java b/appengine-java8/appidentity/src/main/java/com/example/appengine/appidentity/UrlShortener.java
new file mode 100644
index 00000000000..4ef26aa135a
--- /dev/null
+++ b/appengine-java8/appidentity/src/main/java/com/example/appengine/appidentity/UrlShortener.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2016 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.appidentity;
+
+import com.google.appengine.api.appidentity.AppIdentityService;
+import com.google.appengine.api.appidentity.AppIdentityServiceFactory;
+import com.google.common.io.CharStreams;
+
+import org.json.JSONObject;
+import org.json.JSONTokener;
+
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+
+@SuppressWarnings("serial")
+class UrlShortener {
+ // [START asserting_identity_to_Google_APIs]
+ /**
+ * Returns a shortened URL by calling the Google URL Shortener API.
+ *
+ *
Note: Error handling elided for simplicity.
+ */
+ public String createShortUrl(String longUrl) throws Exception {
+ ArrayList scopes = new ArrayList();
+ scopes.add("https://www.googleapis.com/auth/urlshortener");
+ final AppIdentityService appIdentity = AppIdentityServiceFactory.getAppIdentityService();
+ final AppIdentityService.GetAccessTokenResult accessToken = appIdentity.getAccessToken(scopes);
+ // The token asserts the identity reported by appIdentity.getServiceAccountName()
+ JSONObject request = new JSONObject();
+ request.put("longUrl", longUrl);
+
+ URL url = new URL("https://www.googleapis.com/urlshortener/v1/url?pp=1");
+ HttpURLConnection connection = (HttpURLConnection) url.openConnection();
+ connection.setDoOutput(true);
+ connection.setRequestMethod("POST");
+ connection.addRequestProperty("Content-Type", "application/json");
+ connection.addRequestProperty("Authorization", "Bearer " + accessToken.getAccessToken());
+
+ OutputStreamWriter writer = new OutputStreamWriter(connection.getOutputStream());
+ request.write(writer);
+ writer.close();
+
+ if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) {
+ // Note: Should check the content-encoding.
+ // Any JSON parser can be used; this one is used for illustrative purposes.
+ JSONTokener responseTokens = new JSONTokener(connection.getInputStream());
+ JSONObject response = new JSONObject(responseTokens);
+ return (String) response.get("id");
+ } else {
+ try (InputStream s = connection.getErrorStream();
+ InputStreamReader r = new InputStreamReader(s, StandardCharsets.UTF_8)) {
+ throw new RuntimeException(String.format(
+ "got error (%d) response %s from %s",
+ connection.getResponseCode(),
+ CharStreams.toString(r),
+ connection.toString()));
+ }
+ }
+ }
+ // [END asserting_identity_to_Google_APIs]
+}
diff --git a/appengine-java8/appidentity/src/main/java/com/example/appengine/appidentity/UrlShortenerServlet.java b/appengine-java8/appidentity/src/main/java/com/example/appengine/appidentity/UrlShortenerServlet.java
new file mode 100644
index 00000000000..324b8dcd0a1
--- /dev/null
+++ b/appengine-java8/appidentity/src/main/java/com/example/appengine/appidentity/UrlShortenerServlet.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2016 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.appidentity;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+@SuppressWarnings("serial")
+public class UrlShortenerServlet extends HttpServlet {
+ private final UrlShortener shortener;
+
+ public UrlShortenerServlet() {
+ shortener = new UrlShortener();
+ }
+
+ @Override
+ public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+ PrintWriter writer = resp.getWriter();
+ writer.println("");
+ writer.println("");
+ writer.println(
+ "Asserting Identity to Google APIs - App Engine App Identity Example");
+ writer.println("");
+ }
+
+ @Override
+ public void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+ resp.setContentType("text/plain");
+ String longUrl = req.getParameter("longUrl");
+ if (longUrl == null) {
+ resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "missing longUrl parameter");
+ return;
+ }
+
+ String shortUrl;
+ PrintWriter writer = resp.getWriter();
+ try {
+ shortUrl = shortener.createShortUrl(longUrl);
+ } catch (Exception e) {
+ resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+ writer.println("error shortening URL: " + longUrl);
+ e.printStackTrace(writer);
+ return;
+ }
+
+ writer.print("long URL: ");
+ writer.println(longUrl);
+ writer.print("short URL: ");
+ writer.println(shortUrl);
+ }
+}
diff --git a/appengine-java8/appidentity/src/main/webapp/WEB-INF/appengine-web.xml b/appengine-java8/appidentity/src/main/webapp/WEB-INF/appengine-web.xml
new file mode 100644
index 00000000000..1ddd6f6a2c1
--- /dev/null
+++ b/appengine-java8/appidentity/src/main/webapp/WEB-INF/appengine-web.xml
@@ -0,0 +1,5 @@
+
+
+ true
+ java8
+
diff --git a/appengine-java8/appidentity/src/main/webapp/WEB-INF/web.xml b/appengine-java8/appidentity/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 00000000000..3296a0799f4
--- /dev/null
+++ b/appengine-java8/appidentity/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,30 @@
+
+
+
+ appidentity
+ com.example.appengine.appidentity.IdentityServlet
+
+
+ signforapp
+ com.example.appengine.appidentity.SignForAppServlet
+
+
+ urlshortener
+ com.example.appengine.appidentity.UrlShortenerServlet
+
+
+ appidentity
+ /
+
+
+ signforapp
+ /sign
+
+
+ urlshortener
+ /shorten
+
+
diff --git a/appengine-java8/appidentity/src/test/java/com/example/appengine/appidentity/IdentityServletTest.java b/appengine-java8/appidentity/src/test/java/com/example/appengine/appidentity/IdentityServletTest.java
new file mode 100644
index 00000000000..74041b24cd3
--- /dev/null
+++ b/appengine-java8/appidentity/src/test/java/com/example/appengine/appidentity/IdentityServletTest.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2015 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.appidentity;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.when;
+
+import com.google.appengine.tools.development.testing.LocalServiceTestHelper;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Unit tests for {@link IdentityServlet}.
+ */
+@RunWith(JUnit4.class)
+public class IdentityServletTest {
+
+ // Set up a helper so that the ApiProxy returns a valid environment for local testing.
+ private final LocalServiceTestHelper helper = new LocalServiceTestHelper();
+
+ @Mock private HttpServletRequest mockRequest;
+ @Mock private HttpServletResponse mockResponse;
+ private StringWriter responseWriter;
+ private IdentityServlet servletUnderTest;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ helper.setUp();
+
+ // Set up a fake HTTP response.
+ responseWriter = new StringWriter();
+ when(mockResponse.getWriter()).thenReturn(new PrintWriter(responseWriter));
+
+ servletUnderTest = new IdentityServlet();
+ }
+
+ @After public void tearDown() {
+ helper.tearDown();
+ }
+
+ @Test
+ public void doGet_defaultEnvironment_writesResponse() throws Exception {
+ servletUnderTest.doGet(mockRequest, mockResponse);
+
+ // We don't have any guarantee over what the local App Engine environment returns for
+ // "com.google.appengine.runtime.default_version_hostname". Only assert that the response
+ // contains part of the string we have control over.
+ assertThat(responseWriter.toString())
+ .named("IdentityServlet response")
+ .contains("default_version_hostname:");
+ }
+}
diff --git a/appengine-java8/appidentity/src/test/java/com/example/appengine/appidentity/SignForAppServletTest.java b/appengine-java8/appidentity/src/test/java/com/example/appengine/appidentity/SignForAppServletTest.java
new file mode 100644
index 00000000000..7cc1fa5411b
--- /dev/null
+++ b/appengine-java8/appidentity/src/test/java/com/example/appengine/appidentity/SignForAppServletTest.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2016 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.appidentity;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.when;
+
+import com.google.appengine.tools.development.testing.LocalServiceTestHelper;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Unit tests for {@link SignForAppServlet}.
+ */
+@RunWith(JUnit4.class)
+public class SignForAppServletTest {
+
+ private final LocalServiceTestHelper helper = new LocalServiceTestHelper();
+
+ @Mock private HttpServletRequest mockRequest;
+ @Mock private HttpServletResponse mockResponse;
+ private StringWriter responseWriter;
+ private SignForAppServlet servletUnderTest;
+
+ @Before public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ helper.setUp();
+
+ // Set up a fake HTTP response.
+ responseWriter = new StringWriter();
+ when(mockResponse.getWriter()).thenReturn(new PrintWriter(responseWriter));
+
+ servletUnderTest = new SignForAppServlet();
+ }
+
+ @After public void tearDown() {
+ helper.tearDown();
+ }
+
+ @Test public void doGet_defaultEnvironment_successfullyVerifiesSignature() throws Exception {
+ servletUnderTest.doGet(mockRequest, mockResponse);
+
+ assertThat(responseWriter.toString())
+ .named("SignForAppServlet response")
+ .contains("isValid=true for message: abcdefg");
+ }
+}
diff --git a/appengine-java8/cloudsql/README.md b/appengine-java8/cloudsql/README.md
new file mode 100644
index 00000000000..def7251a81d
--- /dev/null
+++ b/appengine-java8/cloudsql/README.md
@@ -0,0 +1,35 @@
+# Cloud SQL sample for Google App Engine
+This sample demonstrates how to use [Cloud SQL](https://cloud.google.com/sql/) on Google App Engine
+
+## Setup
+Before you can run or deploy the sample, you will need to create a [Cloud SQL instance)](https://cloud.google.com/sql/docs/create-instance)
+
+1. Create a new user and database for the application. The easiest way to do this is via the [Google
+Developers Console](https://console.cloud.google.com/sql/instances). Alternatively, you can use MySQL tools such as the command line client or workbench.
+2. Change the root password (under Access Control) and / or create a new user / password.
+3. Create a Database (under Databases) (or use MySQL with `gcloud beta sql connect --user=root`)
+4. Note the **Instance connection name** under Overview > properties
+(It will look like project:instance for 1st Generation or project:region:zone for 2nd Generation)
+
+or
+
+```bash
+gcloud sql instances describe | grep connectionName
+```
+
+## Deploying
+
+```bash
+$ mvn clean appengine:deploy -DINSTANCE_CONNECTION_NAME=instanceConnectionName -Duser=root
+-Dpassword=myPassword -Ddatabase=myDatabase
+```
+
+Or you can update the properties in `pom.xml`
+
+## Running locally
+
+```bash
+$ mvn clean appengine:run -DINSTANCE_CONNECTION_NAME=instanceConnectionName -Duser=root -Dpassword=myPassowrd -Ddatabase=myDatabase
+```
+Note - you must use a local mysql instance for the 1st Generation instance and change the local Url
+in `src/main/webapp/WEB-INF/appengine-web.xml` to use your local server.
diff --git a/appengine-java8/cloudsql/pom.xml b/appengine-java8/cloudsql/pom.xml
new file mode 100644
index 00000000000..0147051f3a3
--- /dev/null
+++ b/appengine-java8/cloudsql/pom.xml
@@ -0,0 +1,116 @@
+
+
+ 4.0.0
+ war
+ 1.0-SNAPSHOT
+ com.example.appengine
+ appengine-cloudsql-j8
+
+
+ appengine-java8-samples
+ com.google.cloud
+ 1.0.0
+ ..
+
+
+
+
+
+
+ root
+ myPassword
+ sqldemo
+
+
+ 1.3.1
+ 1.8
+ 1.8
+
+
+
+
+
+
+ com.google.appengine
+ appengine-api-1.0-sdk
+ ${appengine.sdk.version}
+
+
+
+ javax.servlet
+ javax.servlet-api
+ 3.1.0
+ jar
+ provided
+
+
+
+ com.google.api-client
+ google-api-client-appengine
+ 1.22.0
+
+
+
+
+ mysql
+ mysql-connector-java
+ 5.1.40
+
+
+ com.google.cloud.sql
+ mysql-socket-factory
+ 1.0.2
+
+
+
+
+
+ ${project.build.directory}/${project.build.finalName}/WEB-INF/classes
+
+
+ org.apache.maven.plugins
+ maven-war-plugin
+ 3.0.0
+
+
+
+
+ ${basedir}/src/main/webapp/WEB-INF
+ true
+ WEB-INF
+
+
+
+
+
+
+ com.google.cloud.tools
+ appengine-maven-plugin
+ ${appengine.maven.plugin}
+
+ true
+ true
+
+
+
+
+
+
diff --git a/appengine-java8/cloudsql/src/main/java/com/example/appengine/cloudsql/CloudSqlServlet.java b/appengine-java8/cloudsql/src/main/java/com/example/appengine/cloudsql/CloudSqlServlet.java
new file mode 100644
index 00000000000..9e09fbede8e
--- /dev/null
+++ b/appengine-java8/cloudsql/src/main/java/com/example/appengine/cloudsql/CloudSqlServlet.java
@@ -0,0 +1,106 @@
+/**
+ * Copyright 2016 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.cloudsql;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Timestamp;
+import java.util.Date;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+// [START example]
+@SuppressWarnings("serial")
+public class CloudSqlServlet extends HttpServlet {
+
+ @Override
+ public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException,
+ ServletException {
+ String path = req.getRequestURI();
+ if (path.startsWith("/favicon.ico")) {
+ return; // ignore the request for favicon.ico
+ }
+ // store only the first two octets of a users ip address
+ String userIp = req.getRemoteAddr();
+ InetAddress address = InetAddress.getByName(userIp);
+ if (address instanceof Inet6Address) {
+ // nest indexOf calls to find the second occurrence of a character in a string
+ // an alternative is to use Apache Commons Lang: StringUtils.ordinalIndexOf()
+ userIp = userIp.substring(0, userIp.indexOf(":", userIp.indexOf(":") + 1)) + ":*:*:*:*:*:*";
+ } else if (address instanceof Inet4Address) {
+ userIp = userIp.substring(0, userIp.indexOf(".", userIp.indexOf(".") + 1)) + ".*.*";
+ }
+
+ final String createTableSql = "CREATE TABLE IF NOT EXISTS visits ( visit_id INT NOT NULL "
+ + "AUTO_INCREMENT, user_ip VARCHAR(46) NOT NULL, timestamp DATETIME NOT NULL, "
+ + "PRIMARY KEY (visit_id) )";
+ final String createVisitSql = "INSERT INTO visits (user_ip, timestamp) VALUES (?, ?)";
+ final String selectSql = "SELECT user_ip, timestamp FROM visits ORDER BY timestamp DESC "
+ + "LIMIT 10";
+
+ PrintWriter out = resp.getWriter();
+ resp.setContentType("text/plain");
+ String url;
+ if (System
+ .getProperty("com.google.appengine.runtime.version").startsWith("Google App Engine/")) {
+ // Check the System properties to determine if we are running on appengine or not
+ // Google App Engine sets a few system properties that will reliably be present on a remote
+ // instance.
+ url = System.getProperty("ae-cloudsql.cloudsql-database-url");
+ try {
+ // Load the class that provides the new "jdbc:google:mysql://" prefix.
+ Class.forName("com.mysql.jdbc.GoogleDriver");
+ } catch (ClassNotFoundException e) {
+ throw new ServletException("Error loading Google JDBC Driver", e);
+ }
+ } else {
+ // Set the url with the local MySQL database connection url when running locally
+ url = System.getProperty("ae-cloudsql.local-database-url");
+ }
+ log("connecting to: " + url);
+ try (Connection conn = DriverManager.getConnection(url);
+ PreparedStatement statementCreateVisit = conn.prepareStatement(createVisitSql)) {
+ conn.createStatement().executeUpdate(createTableSql);
+ statementCreateVisit.setString(1, userIp);
+ statementCreateVisit.setTimestamp(2, new Timestamp(new Date().getTime()));
+ statementCreateVisit.executeUpdate();
+
+ try (ResultSet rs = conn.prepareStatement(selectSql).executeQuery()) {
+ out.print("Last 10 visits:\n");
+ while (rs.next()) {
+ String savedIp = rs.getString("user_ip");
+ String timeStamp = rs.getString("timestamp");
+ out.print("Time: " + timeStamp + " Addr: " + savedIp + "\n");
+ }
+ }
+ } catch (SQLException e) {
+ throw new ServletException("SQL error", e);
+ }
+ }
+}
+// [END example]
diff --git a/appengine-java8/cloudsql/src/main/webapp/WEB-INF/appengine-web.xml b/appengine-java8/cloudsql/src/main/webapp/WEB-INF/appengine-web.xml
new file mode 100644
index 00000000000..f028552e364
--- /dev/null
+++ b/appengine-java8/cloudsql/src/main/webapp/WEB-INF/appengine-web.xml
@@ -0,0 +1,25 @@
+
+
+
+
+ true
+ java8
+
+ true
+
+
+
+
+
+
diff --git a/appengine-java8/cloudsql/src/main/webapp/WEB-INF/web.xml b/appengine-java8/cloudsql/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 00000000000..2f5b0dfbd32
--- /dev/null
+++ b/appengine-java8/cloudsql/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+ cloudsql
+ com.example.appengine.cloudsql.CloudSqlServlet
+
+
+ cloudsql
+ /
+
+
diff --git a/appengine-java8/datastore/README.md b/appengine-java8/datastore/README.md
new file mode 100644
index 00000000000..4a9f625455a
--- /dev/null
+++ b/appengine-java8/datastore/README.md
@@ -0,0 +1,28 @@
+# Google Cloud Datastore Sample
+
+This sample demonstrates how to use [Google Cloud Datastore][java-datastore]
+from [Google App Engine standard environment][ae-docs].
+
+[java-datastore]: https://cloud.google.com/appengine/docs/java/datastore/
+[ae-docs]: https://cloud.google.com/appengine/docs/java/
+
+
+## Running locally
+
+This example uses the
+[Cloud SDK Maven plugin](https://cloud.google.com/appengine/docs/java/tools/using-maven).
+To run this sample locally:
+
+ $ mvn appengine:run
+
+To see the results of the sample application, open
+[localhost:8080](http://localhost:8080) in a web browser.
+
+
+## Deploying
+
+In the following command, replace YOUR-PROJECT-ID with your
+[Google Cloud Project ID](https://developers.google.com/console/help/new/#projectnumber)
+and SOME-VERSION with a valid version number.
+
+ $ mvn appengine:deploy
diff --git a/appengine-java8/datastore/indexes-exploding/pom.xml b/appengine-java8/datastore/indexes-exploding/pom.xml
new file mode 100644
index 00000000000..c0a049533bb
--- /dev/null
+++ b/appengine-java8/datastore/indexes-exploding/pom.xml
@@ -0,0 +1,98 @@
+
+
+ 4.0.0
+ war
+ 1.0-SNAPSHOT
+ com.example.appengine
+ appengine-datastore-indexes-exploding-j8
+
+
+ com.google.cloud
+ appengine-java8-samples
+ 1.0.0
+ ../..
+
+
+
+
+ com.google.appengine
+ appengine-api-1.0-sdk
+ ${appengine.sdk.version}
+
+
+ javax.servlet
+ javax.servlet-api
+ 3.1.0
+ jar
+ provided
+
+
+
+
+ junit
+ junit
+ 4.12
+ test
+
+
+ org.mockito
+ mockito-all
+ 1.10.19
+ test
+
+
+ com.google.appengine
+ appengine-testing
+ ${appengine.sdk.version}
+ test
+
+
+ com.google.appengine
+ appengine-api-stubs
+ ${appengine.sdk.version}
+ test
+
+
+ com.google.appengine
+ appengine-tools-sdk
+ ${appengine.sdk.version}
+ test
+
+
+ com.google.truth
+ truth
+ 0.32
+ test
+
+
+
+
+
+ ${project.build.directory}/${project.build.finalName}/WEB-INF/classes
+
+
+ com.google.cloud.tools
+ appengine-maven-plugin
+ 1.3.1
+
+ true
+ true
+
+
+
+
+
diff --git a/appengine-java8/datastore/indexes-exploding/src/main/java/com/example/appengine/IndexesServlet.java b/appengine-java8/datastore/indexes-exploding/src/main/java/com/example/appengine/IndexesServlet.java
new file mode 100644
index 00000000000..04d4fe018d4
--- /dev/null
+++ b/appengine-java8/datastore/indexes-exploding/src/main/java/com/example/appengine/IndexesServlet.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2016 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;
+
+import com.google.appengine.api.datastore.DatastoreService;
+import com.google.appengine.api.datastore.DatastoreServiceFactory;
+import com.google.appengine.api.datastore.Entity;
+import com.google.appengine.api.datastore.FetchOptions;
+import com.google.appengine.api.datastore.Query;
+import com.google.appengine.api.datastore.Query.CompositeFilterOperator;
+import com.google.appengine.api.datastore.Query.FilterOperator;
+import com.google.appengine.api.datastore.Query.FilterPredicate;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.List;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * A servlet to demonstrate the use of Cloud Datastore indexes.
+ */
+public class IndexesServlet extends HttpServlet {
+ private final DatastoreService datastore;
+
+ public IndexesServlet() {
+ datastore = DatastoreServiceFactory.getDatastoreService();
+ }
+
+ @Override
+ public void doGet(HttpServletRequest req, HttpServletResponse resp)
+ throws IOException, ServletException {
+ Query q =
+ new Query("Widget")
+ .setFilter(
+ CompositeFilterOperator.and(
+ new FilterPredicate("x", FilterOperator.EQUAL, 1),
+ new FilterPredicate("y", FilterOperator.EQUAL, "red")))
+ .addSort("date", Query.SortDirection.ASCENDING);
+ List results = datastore.prepare(q).asList(FetchOptions.Builder.withDefaults());
+
+ PrintWriter out = resp.getWriter();
+ out.printf("Got %d widgets.\n", results.size());
+ }
+}
diff --git a/appengine-java8/datastore/indexes-exploding/src/main/webapp/WEB-INF/appengine-web.xml b/appengine-java8/datastore/indexes-exploding/src/main/webapp/WEB-INF/appengine-web.xml
new file mode 100644
index 00000000000..b27da1dd82a
--- /dev/null
+++ b/appengine-java8/datastore/indexes-exploding/src/main/webapp/WEB-INF/appengine-web.xml
@@ -0,0 +1,20 @@
+
+
+
+ java8
+ true
+
diff --git a/appengine-java8/datastore/indexes-exploding/src/main/webapp/WEB-INF/datastore-indexes.xml b/appengine-java8/datastore/indexes-exploding/src/main/webapp/WEB-INF/datastore-indexes.xml
new file mode 100644
index 00000000000..f1f44c38f67
--- /dev/null
+++ b/appengine-java8/datastore/indexes-exploding/src/main/webapp/WEB-INF/datastore-indexes.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/appengine-java8/datastore/indexes-exploding/src/main/webapp/WEB-INF/web.xml b/appengine-java8/datastore/indexes-exploding/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 00000000000..6af27edbd87
--- /dev/null
+++ b/appengine-java8/datastore/indexes-exploding/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,39 @@
+
+
+
+
+ indexes-servlet
+ com.example.appengine.IndexesServlet
+
+
+ indexes-servlet
+ /
+
+
+
+
+ profile
+ /*
+
+
+ CONFIDENTIAL
+
+
+
diff --git a/appengine-java8/datastore/indexes-exploding/src/test/java/com/example/appengine/IndexesServletTest.java b/appengine-java8/datastore/indexes-exploding/src/test/java/com/example/appengine/IndexesServletTest.java
new file mode 100644
index 00000000000..ea86f530fef
--- /dev/null
+++ b/appengine-java8/datastore/indexes-exploding/src/test/java/com/example/appengine/IndexesServletTest.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2016 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;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.when;
+
+import com.google.appengine.api.datastore.DatastoreService;
+import com.google.appengine.api.datastore.DatastoreServiceFactory;
+import com.google.appengine.api.datastore.Entity;
+import com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig;
+import com.google.appengine.tools.development.testing.LocalServiceTestHelper;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.Arrays;
+import java.util.Date;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Unit tests for {@link IndexesServlet}.
+ */
+@RunWith(JUnit4.class)
+public class IndexesServletTest {
+
+ private final LocalServiceTestHelper helper =
+ new LocalServiceTestHelper(
+ // Set no eventual consistency, that way queries return all results.
+ // https://cloud.google.com/appengine/docs/java/tools/localunittesting#Java_Writing_High_Replication_Datastore_tests
+ new LocalDatastoreServiceTestConfig()
+ .setDefaultHighRepJobPolicyUnappliedJobPercentage(0));
+
+ @Mock private HttpServletRequest mockRequest;
+ @Mock private HttpServletResponse mockResponse;
+ private StringWriter responseWriter;
+ private IndexesServlet servletUnderTest;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ helper.setUp();
+
+ // Set up a fake HTTP response.
+ responseWriter = new StringWriter();
+ when(mockResponse.getWriter()).thenReturn(new PrintWriter(responseWriter));
+
+ servletUnderTest = new IndexesServlet();
+ }
+
+ @After
+ public void tearDown() {
+ helper.tearDown();
+ }
+
+ @Test
+ public void doGet_emptyDatastore_writesNoWidgets() throws Exception {
+ servletUnderTest.doGet(mockRequest, mockResponse);
+
+ assertThat(responseWriter.toString())
+ .named("IndexesServlet response")
+ .isEqualTo("Got 0 widgets.\n");
+ }
+
+ @Test
+ public void doGet_repeatedPropertyEntities_writesWidgets() throws Exception {
+ DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
+ // [START exploding_index_example_3]
+ Entity widget = new Entity("Widget");
+ widget.setProperty("x", Arrays.asList(1, 2, 3, 4));
+ widget.setProperty("y", Arrays.asList("red", "green", "blue"));
+ widget.setProperty("date", new Date());
+ datastore.put(widget);
+ // [END exploding_index_example_3]
+
+ servletUnderTest.doGet(mockRequest, mockResponse);
+
+ assertThat(responseWriter.toString())
+ .named("IndexesServlet response")
+ .isEqualTo("Got 1 widgets.\n");
+ }
+}
diff --git a/appengine-java8/datastore/indexes-perfect/pom.xml b/appengine-java8/datastore/indexes-perfect/pom.xml
new file mode 100644
index 00000000000..1e8359e42df
--- /dev/null
+++ b/appengine-java8/datastore/indexes-perfect/pom.xml
@@ -0,0 +1,98 @@
+
+
+ 4.0.0
+ war
+ 1.0-SNAPSHOT
+ com.example.appengine
+ appengine-datastore-indexes-perfect-j8
+
+
+ com.google.cloud
+ appengine-java8-samples
+ 1.0.0
+ ../..
+
+
+
+
+ com.google.appengine
+ appengine-api-1.0-sdk
+ ${appengine.sdk.version}
+
+
+ javax.servlet
+ javax.servlet-api
+ 3.1.0
+ jar
+ provided
+
+
+
+
+ junit
+ junit
+ 4.12
+ test
+
+
+ org.mockito
+ mockito-all
+ 1.10.19
+ test
+
+
+ com.google.appengine
+ appengine-testing
+ ${appengine.sdk.version}
+ test
+
+
+ com.google.appengine
+ appengine-api-stubs
+ ${appengine.sdk.version}
+ test
+
+
+ com.google.appengine
+ appengine-tools-sdk
+ ${appengine.sdk.version}
+ test
+
+
+ com.google.truth
+ truth
+ 0.32
+ test
+
+
+
+
+
+ ${project.build.directory}/${project.build.finalName}/WEB-INF/classes
+
+
+ com.google.cloud.tools
+ appengine-maven-plugin
+ 1.3.1
+
+ true
+ true
+
+
+
+
+
diff --git a/appengine-java8/datastore/indexes-perfect/src/main/java/com/example/appengine/IndexesServlet.java b/appengine-java8/datastore/indexes-perfect/src/main/java/com/example/appengine/IndexesServlet.java
new file mode 100644
index 00000000000..5a01f1a53e5
--- /dev/null
+++ b/appengine-java8/datastore/indexes-perfect/src/main/java/com/example/appengine/IndexesServlet.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2016 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;
+
+import com.google.appengine.api.datastore.DatastoreService;
+import com.google.appengine.api.datastore.DatastoreServiceFactory;
+import com.google.appengine.api.datastore.Entity;
+import com.google.appengine.api.datastore.FetchOptions;
+import com.google.appengine.api.datastore.Query;
+import com.google.appengine.api.datastore.Query.CompositeFilterOperator;
+import com.google.appengine.api.datastore.Query.FilterOperator;
+import com.google.appengine.api.datastore.Query.FilterPredicate;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.List;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * A servlet to demonstrate the use of Cloud Datastore indexes.
+ */
+public class IndexesServlet extends HttpServlet {
+ private final DatastoreService datastore;
+
+ public IndexesServlet() {
+ datastore = DatastoreServiceFactory.getDatastoreService();
+ }
+
+ @Override
+ public void doGet(HttpServletRequest req, HttpServletResponse resp)
+ throws IOException, ServletException {
+ PrintWriter out = resp.getWriter();
+ // These queries should all work with the same index.
+ // [START queries_and_indexes_example_1]
+ Query q1 =
+ new Query("Person")
+ .setFilter(
+ CompositeFilterOperator.and(
+ new FilterPredicate("lastName", FilterOperator.EQUAL, "Smith"),
+ new FilterPredicate("height", FilterOperator.EQUAL, 72)))
+ .addSort("height", Query.SortDirection.DESCENDING);
+ // [END queries_and_indexes_example_1]
+ List r1 = datastore.prepare(q1).asList(FetchOptions.Builder.withDefaults());
+ out.printf("Got %d results from query 1.\n", r1.size());
+
+ // [START queries_and_indexes_example_2]
+ Query q2 =
+ new Query("Person")
+ .setFilter(
+ CompositeFilterOperator.and(
+ new FilterPredicate("lastName", FilterOperator.EQUAL, "Jones"),
+ new FilterPredicate("height", FilterOperator.EQUAL, 63)))
+ .addSort("height", Query.SortDirection.DESCENDING);
+ // [END queries_and_indexes_example_2]
+ List r2 = datastore.prepare(q2).asList(FetchOptions.Builder.withDefaults());
+ out.printf("Got %d results from query 2.\n", r2.size());
+
+ // [START queries_and_indexes_example_3]
+ Query q3 =
+ new Query("Person")
+ .setFilter(
+ CompositeFilterOperator.and(
+ new FilterPredicate("lastName", FilterOperator.EQUAL, "Friedkin"),
+ new FilterPredicate("firstName", FilterOperator.EQUAL, "Damian")))
+ .addSort("height", Query.SortDirection.ASCENDING);
+ // [END queries_and_indexes_example_3]
+ List r3 = datastore.prepare(q3).asList(FetchOptions.Builder.withDefaults());
+ out.printf("Got %d results from query 3.\n", r3.size());
+
+ // [START queries_and_indexes_example_4]
+ Query q4 =
+ new Query("Person")
+ .setFilter(new FilterPredicate("lastName", FilterOperator.EQUAL, "Blair"))
+ .addSort("firstName", Query.SortDirection.ASCENDING)
+ .addSort("height", Query.SortDirection.ASCENDING);
+ // [END queries_and_indexes_example_4]
+ List r4 = datastore.prepare(q4).asList(FetchOptions.Builder.withDefaults());
+ out.printf("Got %d results from query 4.\n", r4.size());
+ }
+}
diff --git a/appengine-java8/datastore/indexes-perfect/src/main/webapp/WEB-INF/appengine-web.xml b/appengine-java8/datastore/indexes-perfect/src/main/webapp/WEB-INF/appengine-web.xml
new file mode 100644
index 00000000000..b27da1dd82a
--- /dev/null
+++ b/appengine-java8/datastore/indexes-perfect/src/main/webapp/WEB-INF/appengine-web.xml
@@ -0,0 +1,20 @@
+
+
+
+ java8
+ true
+
diff --git a/appengine-java8/datastore/indexes-perfect/src/main/webapp/WEB-INF/datastore-indexes.xml b/appengine-java8/datastore/indexes-perfect/src/main/webapp/WEB-INF/datastore-indexes.xml
new file mode 100644
index 00000000000..30d97392eaa
--- /dev/null
+++ b/appengine-java8/datastore/indexes-perfect/src/main/webapp/WEB-INF/datastore-indexes.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/appengine-java8/datastore/indexes-perfect/src/main/webapp/WEB-INF/web.xml b/appengine-java8/datastore/indexes-perfect/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 00000000000..6af27edbd87
--- /dev/null
+++ b/appengine-java8/datastore/indexes-perfect/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,39 @@
+
+
+
+
+ indexes-servlet
+ com.example.appengine.IndexesServlet
+
+
+ indexes-servlet
+ /
+
+
+
+
+ profile
+ /*
+
+
+ CONFIDENTIAL
+
+
+
diff --git a/appengine-java8/datastore/indexes-perfect/src/test/java/com/example/appengine/IndexesServletTest.java b/appengine-java8/datastore/indexes-perfect/src/test/java/com/example/appengine/IndexesServletTest.java
new file mode 100644
index 00000000000..d1816a7034d
--- /dev/null
+++ b/appengine-java8/datastore/indexes-perfect/src/test/java/com/example/appengine/IndexesServletTest.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2016 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;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.when;
+
+import com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig;
+import com.google.appengine.tools.development.testing.LocalServiceTestHelper;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Unit tests for {@link IndexesServlet}.
+ */
+@RunWith(JUnit4.class)
+public class IndexesServletTest {
+
+ private final LocalServiceTestHelper helper =
+ new LocalServiceTestHelper(
+ // Set no eventual consistency, that way queries return all results.
+ // https://cloud.google.com/appengine/docs/java/tools/localunittesting#Java_Writing_High_Replication_Datastore_tests
+ new LocalDatastoreServiceTestConfig()
+ .setDefaultHighRepJobPolicyUnappliedJobPercentage(0));
+
+ @Mock private HttpServletRequest mockRequest;
+ @Mock private HttpServletResponse mockResponse;
+ private StringWriter responseWriter;
+ private IndexesServlet servletUnderTest;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ helper.setUp();
+
+ // Set up a fake HTTP response.
+ responseWriter = new StringWriter();
+ when(mockResponse.getWriter()).thenReturn(new PrintWriter(responseWriter));
+
+ servletUnderTest = new IndexesServlet();
+ }
+
+ @After
+ public void tearDown() {
+ helper.tearDown();
+ }
+
+ @Test
+ public void doGet_emptyDatastore_writesNoWidgets() throws Exception {
+ servletUnderTest.doGet(mockRequest, mockResponse);
+
+ String response = responseWriter.toString();
+ assertThat(response).contains("Got 0 results from query 1.");
+ assertThat(response).contains("Got 0 results from query 2.");
+ assertThat(response).contains("Got 0 results from query 3.");
+ assertThat(response).contains("Got 0 results from query 4.");
+ }
+}
diff --git a/appengine-java8/datastore/indexes/pom.xml b/appengine-java8/datastore/indexes/pom.xml
new file mode 100644
index 00000000000..86d5cc7b8c2
--- /dev/null
+++ b/appengine-java8/datastore/indexes/pom.xml
@@ -0,0 +1,99 @@
+
+
+ 4.0.0
+ war
+ 1.0-SNAPSHOT
+ com.example.appengine
+ appengine-datastore-indexes-j8
+
+
+ com.google.cloud
+ appengine-java8-samples
+ 1.0.0
+ ../..
+
+
+
+
+ com.google.appengine
+ appengine-api-1.0-sdk
+ ${appengine.sdk.version}
+
+
+
+ javax.servlet
+ javax.servlet-api
+ 3.1.0
+ jar
+ provided
+
+
+
+
+ junit
+ junit
+ 4.12
+ test
+
+
+ org.mockito
+ mockito-all
+ 1.10.19
+ test
+
+
+ com.google.appengine
+ appengine-testing
+ ${appengine.sdk.version}
+ test
+
+
+ com.google.appengine
+ appengine-api-stubs
+ ${appengine.sdk.version}
+ test
+
+
+ com.google.appengine
+ appengine-tools-sdk
+ ${appengine.sdk.version}
+ test
+
+
+ com.google.truth
+ truth
+ 0.32
+ test
+
+
+
+
+
+ ${project.build.directory}/${project.build.finalName}/WEB-INF/classes
+
+
+ com.google.cloud.tools
+ appengine-maven-plugin
+ 1.3.1
+
+ true
+ true
+
+
+
+
+
diff --git a/appengine-java8/datastore/indexes/src/main/java/com/example/appengine/IndexesServlet.java b/appengine-java8/datastore/indexes/src/main/java/com/example/appengine/IndexesServlet.java
new file mode 100644
index 00000000000..5493ce1f1a9
--- /dev/null
+++ b/appengine-java8/datastore/indexes/src/main/java/com/example/appengine/IndexesServlet.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2016 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;
+
+import com.google.appengine.api.datastore.DatastoreService;
+import com.google.appengine.api.datastore.DatastoreServiceFactory;
+import com.google.appengine.api.datastore.Entity;
+import com.google.appengine.api.datastore.FetchOptions;
+import com.google.appengine.api.datastore.Query;
+import com.google.appengine.api.datastore.Query.CompositeFilterOperator;
+import com.google.appengine.api.datastore.Query.FilterOperator;
+import com.google.appengine.api.datastore.Query.FilterPredicate;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.List;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * A servlet to demonstrate the use of Cloud Datastore indexes.
+ */
+public class IndexesServlet extends HttpServlet {
+ private final DatastoreService datastore;
+
+ public IndexesServlet() {
+ datastore = DatastoreServiceFactory.getDatastoreService();
+ }
+
+ @Override
+ public void doGet(HttpServletRequest req, HttpServletResponse resp)
+ throws IOException, ServletException {
+ // [START exploding_index_example_1]
+ Query q =
+ new Query("Widget")
+ .setFilter(
+ CompositeFilterOperator.and(
+ new FilterPredicate("x", FilterOperator.EQUAL, 1),
+ new FilterPredicate("y", FilterOperator.EQUAL, 2)))
+ .addSort("date", Query.SortDirection.ASCENDING);
+ // [END exploding_index_example_1]
+ List results = datastore.prepare(q).asList(FetchOptions.Builder.withDefaults());
+
+ PrintWriter out = resp.getWriter();
+ out.printf("Got %d widgets.\n", results.size());
+ }
+}
diff --git a/appengine-java8/datastore/indexes/src/main/webapp/WEB-INF/appengine-web.xml b/appengine-java8/datastore/indexes/src/main/webapp/WEB-INF/appengine-web.xml
new file mode 100644
index 00000000000..b27da1dd82a
--- /dev/null
+++ b/appengine-java8/datastore/indexes/src/main/webapp/WEB-INF/appengine-web.xml
@@ -0,0 +1,20 @@
+
+
+
+ java8
+ true
+
diff --git a/appengine-java8/datastore/indexes/src/main/webapp/WEB-INF/datastore-indexes.xml b/appengine-java8/datastore/indexes/src/main/webapp/WEB-INF/datastore-indexes.xml
new file mode 100644
index 00000000000..28046fb9571
--- /dev/null
+++ b/appengine-java8/datastore/indexes/src/main/webapp/WEB-INF/datastore-indexes.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/appengine-java8/datastore/indexes/src/main/webapp/WEB-INF/web.xml b/appengine-java8/datastore/indexes/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 00000000000..6af27edbd87
--- /dev/null
+++ b/appengine-java8/datastore/indexes/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,39 @@
+
+
+
+
+ indexes-servlet
+ com.example.appengine.IndexesServlet
+
+
+ indexes-servlet
+ /
+
+
+
+
+ profile
+ /*
+
+
+ CONFIDENTIAL
+
+
+
diff --git a/appengine-java8/datastore/indexes/src/test/java/com/example/appengine/IndexesServletTest.java b/appengine-java8/datastore/indexes/src/test/java/com/example/appengine/IndexesServletTest.java
new file mode 100644
index 00000000000..83575239136
--- /dev/null
+++ b/appengine-java8/datastore/indexes/src/test/java/com/example/appengine/IndexesServletTest.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2016 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;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.when;
+
+import com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig;
+import com.google.appengine.tools.development.testing.LocalServiceTestHelper;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Unit tests for {@link IndexesServlet}.
+ */
+@RunWith(JUnit4.class)
+public class IndexesServletTest {
+
+ private final LocalServiceTestHelper helper =
+ new LocalServiceTestHelper(new LocalDatastoreServiceTestConfig());
+
+ @Mock private HttpServletRequest mockRequest;
+ @Mock private HttpServletResponse mockResponse;
+ private StringWriter responseWriter;
+ private IndexesServlet servletUnderTest;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ helper.setUp();
+
+ // Set up a fake HTTP response.
+ responseWriter = new StringWriter();
+ when(mockResponse.getWriter()).thenReturn(new PrintWriter(responseWriter));
+
+ servletUnderTest = new IndexesServlet();
+ }
+
+ @After
+ public void tearDown() {
+ helper.tearDown();
+ }
+
+ @Test
+ public void doGet_emptyDatastore_writesNoWidgets() throws Exception {
+ servletUnderTest.doGet(mockRequest, mockResponse);
+
+ assertThat(responseWriter.toString())
+ .named("IndexesServlet response")
+ .isEqualTo("Got 0 widgets.\n");
+ }
+}
diff --git a/appengine-java8/datastore/pom.xml b/appengine-java8/datastore/pom.xml
new file mode 100644
index 00000000000..22cb53265a0
--- /dev/null
+++ b/appengine-java8/datastore/pom.xml
@@ -0,0 +1,125 @@
+
+
+ 4.0.0
+ war
+ 1.0-SNAPSHOT
+ com.example.appengine
+ appengine-datastore-j8
+
+
+ com.google.cloud
+ appengine-java8-samples
+ 1.0.0
+ ..
+
+
+
+
+ com.google.appengine
+ appengine-api-1.0-sdk
+ ${appengine.sdk.version}
+
+
+
+ javax.servlet
+ javax.servlet-api
+ 3.1.0
+ jar
+ provided
+
+
+
+ com.google.auto.value
+ auto-value
+ 1.4.1
+ provided
+
+
+
+ com.google.code.findbugs
+ jsr305
+ 3.0.2
+
+
+
+ com.google.guava
+ guava
+ 20.0
+
+
+
+ joda-time
+ joda-time
+ 2.9.9
+
+
+
+
+ junit
+ junit
+ 4.12
+ test
+
+
+ org.mockito
+ mockito-all
+ 1.10.19
+ test
+
+
+
+ com.google.appengine
+ appengine-testing
+ ${appengine.sdk.version}
+ test
+
+
+ com.google.appengine
+ appengine-api-stubs
+ ${appengine.sdk.version}
+ test
+
+
+ com.google.appengine
+ appengine-tools-sdk
+ ${appengine.sdk.version}
+ test
+
+
+ com.google.truth
+ truth
+ 0.32
+ test
+
+
+
+
+
+ ${project.build.directory}/${project.build.finalName}/WEB-INF/classes
+
+
+ com.google.cloud.tools
+ appengine-maven-plugin
+ 1.3.1
+
+ true
+ true
+
+
+
+
+
diff --git a/appengine-java8/datastore/src/main/java/com/example/appengine/AbstractGuestbook.java b/appengine-java8/datastore/src/main/java/com/example/appengine/AbstractGuestbook.java
new file mode 100644
index 00000000000..b87edf7eeba
--- /dev/null
+++ b/appengine-java8/datastore/src/main/java/com/example/appengine/AbstractGuestbook.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2016 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;
+
+import com.example.time.Clock;
+
+import com.google.appengine.api.datastore.DatastoreService;
+import com.google.appengine.api.datastore.DatastoreServiceFactory;
+import com.google.appengine.api.datastore.Entity;
+import com.google.appengine.api.users.User;
+import com.google.appengine.api.users.UserService;
+import com.google.appengine.api.users.UserServiceFactory;
+import com.google.common.collect.ImmutableList;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * A log of notes left by users.
+ *
+ *
This is meant to be subclassed to demonstrate different storage structures in Datastore.
+ */
+abstract class AbstractGuestbook {
+ private final DatastoreService datastore;
+ private final UserService userService;
+ private final Clock clock;
+
+ AbstractGuestbook(Clock clock) {
+ this.datastore = DatastoreServiceFactory.getDatastoreService();
+ this.userService = UserServiceFactory.getUserService();
+ this.clock = clock;
+ }
+
+ /**
+ * Appends a new greeting to the guestbook and returns the {@link Entity} that was created.
+ */
+ public Greeting appendGreeting(String content) {
+ Greeting greeting =
+ Greeting.create(
+ createGreeting(
+ datastore,
+ userService.getCurrentUser(),
+ clock.now().toDate(),
+ content));
+ return greeting;
+ }
+
+ /**
+ * Write a greeting to Datastore.
+ */
+ protected abstract Entity createGreeting(
+ DatastoreService datastore, User user, Date date, String content);
+
+ /**
+ * Return a list of the most recent greetings.
+ */
+ public List listGreetings() {
+ ImmutableList.Builder greetings = ImmutableList.builder();
+ for (Entity entity : listGreetingEntities(datastore)) {
+ greetings.add(Greeting.create(entity));
+ }
+ return greetings.build();
+ }
+
+ /**
+ * Return a list of the most recent greetings.
+ */
+ protected abstract List listGreetingEntities(DatastoreService datastore);
+}
diff --git a/appengine-java8/datastore/src/main/java/com/example/appengine/AbstractGuestbookServlet.java b/appengine-java8/datastore/src/main/java/com/example/appengine/AbstractGuestbookServlet.java
new file mode 100644
index 00000000000..e77231497c6
--- /dev/null
+++ b/appengine-java8/datastore/src/main/java/com/example/appengine/AbstractGuestbookServlet.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2016 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;
+
+import java.io.IOException;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+abstract class AbstractGuestbookServlet extends HttpServlet {
+ private final AbstractGuestbook guestbook;
+
+ public AbstractGuestbookServlet(AbstractGuestbook guestbook) {
+ this.guestbook = guestbook;
+ }
+
+ private void renderGuestbook(HttpServletRequest req, HttpServletResponse resp)
+ throws IOException, ServletException {
+ resp.setContentType("text/html");
+ resp.setCharacterEncoding("UTF-8");
+ req.setAttribute("greetings", guestbook.listGreetings());
+ req.getRequestDispatcher("/guestbook.jsp").forward(req, resp);
+ }
+
+ @Override
+ public void doGet(HttpServletRequest req, HttpServletResponse resp)
+ throws IOException, ServletException {
+ renderGuestbook(req, resp);
+ }
+
+ @Override
+ public void doPost(HttpServletRequest req, HttpServletResponse resp)
+ throws IOException, ServletException {
+ String content = req.getParameter("content");
+ if (content == null || content.isEmpty()) {
+ resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "missing content");
+ return;
+ }
+ guestbook.appendGreeting(content);
+ renderGuestbook(req, resp);
+ }
+}
+
diff --git a/appengine-java8/datastore/src/main/java/com/example/appengine/Greeting.java b/appengine-java8/datastore/src/main/java/com/example/appengine/Greeting.java
new file mode 100644
index 00000000000..352aa0a1dde
--- /dev/null
+++ b/appengine-java8/datastore/src/main/java/com/example/appengine/Greeting.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2016 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;
+
+import com.google.appengine.api.datastore.Entity;
+import com.google.appengine.api.users.User;
+import com.google.auto.value.AutoValue;
+import org.joda.time.Instant;
+
+import java.util.Date;
+
+import javax.annotation.Nullable;
+
+@AutoValue
+public abstract class Greeting {
+ static Greeting create(Entity entity) {
+ User user = (User) entity.getProperty("user");
+ Instant date = new Instant((Date) entity.getProperty("date"));
+ String content = (String) entity.getProperty("content");
+ return new AutoValue_Greeting(user, date, content);
+ }
+
+ @Nullable
+ public abstract User getUser();
+
+ public abstract Instant getDate();
+
+ public abstract String getContent();
+}
diff --git a/appengine-java8/datastore/src/main/java/com/example/appengine/Guestbook.java b/appengine-java8/datastore/src/main/java/com/example/appengine/Guestbook.java
new file mode 100644
index 00000000000..83e984818c4
--- /dev/null
+++ b/appengine-java8/datastore/src/main/java/com/example/appengine/Guestbook.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2016 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;
+
+import com.example.time.Clock;
+
+import com.google.appengine.api.datastore.DatastoreService;
+import com.google.appengine.api.datastore.Entity;
+import com.google.appengine.api.datastore.FetchOptions;
+import com.google.appengine.api.datastore.Query;
+import com.google.appengine.api.users.User;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * A log of notes left by users.
+ *
+ *
This demonstrates the use of Google Cloud Datastore using the App Engine
+ * APIs. See the
+ * documentation
+ * for more information.
+ */
+class Guestbook extends AbstractGuestbook {
+ Guestbook(Clock clock) {
+ super(clock);
+ }
+
+ @Override
+ protected Entity createGreeting(
+ DatastoreService datastore, User user, Date date, String content) {
+ // No parent key specified, so Greeting is a root entity.
+ Entity greeting = new Entity("Greeting");
+ greeting.setProperty("user", user);
+ greeting.setProperty("date", date);
+ greeting.setProperty("content", content);
+
+ datastore.put(greeting);
+ return greeting;
+ }
+
+ @Override
+ protected List listGreetingEntities(DatastoreService datastore) {
+ Query query =
+ new Query("Greeting")
+ .addSort("date", Query.SortDirection.DESCENDING);
+ return datastore.prepare(query)
+ .asList(FetchOptions.Builder.withLimit(10));
+ }
+
+
+}
diff --git a/appengine-java8/datastore/src/main/java/com/example/appengine/GuestbookServlet.java b/appengine-java8/datastore/src/main/java/com/example/appengine/GuestbookServlet.java
new file mode 100644
index 00000000000..5ebab796530
--- /dev/null
+++ b/appengine-java8/datastore/src/main/java/com/example/appengine/GuestbookServlet.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2016 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;
+
+import com.example.time.SystemClock;
+
+public class GuestbookServlet extends AbstractGuestbookServlet {
+ public GuestbookServlet() {
+ super(new Guestbook(new SystemClock()));
+ }
+}
diff --git a/appengine-java8/datastore/src/main/java/com/example/appengine/GuestbookStrong.java b/appengine-java8/datastore/src/main/java/com/example/appengine/GuestbookStrong.java
new file mode 100644
index 00000000000..5f3ad23b91d
--- /dev/null
+++ b/appengine-java8/datastore/src/main/java/com/example/appengine/GuestbookStrong.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2016 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;
+
+import com.example.time.Clock;
+
+import com.google.appengine.api.datastore.DatastoreService;
+import com.google.appengine.api.datastore.Entity;
+import com.google.appengine.api.datastore.FetchOptions;
+import com.google.appengine.api.datastore.Key;
+import com.google.appengine.api.datastore.KeyFactory;
+import com.google.appengine.api.datastore.Query;
+import com.google.appengine.api.users.User;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * A log of notes left by users.
+ *
+ *
This demonstrates the use of Google Cloud Datastore using the App Engine
+ * APIs. See the
+ * documentation
+ * for more information.
+ */
+class GuestbookStrong extends AbstractGuestbook {
+ private final String guestbookName;
+
+ GuestbookStrong(String guestbookName, Clock clock) {
+ super(clock);
+ this.guestbookName = guestbookName;
+ }
+
+ @Override
+ protected Entity createGreeting(
+ DatastoreService datastore, User user, Date date, String content) {
+ // String guestbookName = "my guestbook"; -- Set elsewhere (injected to the constructor).
+ Key guestbookKey = KeyFactory.createKey("Guestbook", guestbookName);
+
+ // Place greeting in the same entity group as guestbook.
+ Entity greeting = new Entity("Greeting", guestbookKey);
+ greeting.setProperty("user", user);
+ greeting.setProperty("date", date);
+ greeting.setProperty("content", content);
+
+ datastore.put(greeting);
+ return greeting;
+ }
+
+ @Override
+ protected List listGreetingEntities(DatastoreService datastore) {
+ Key guestbookKey = KeyFactory.createKey("Guestbook", guestbookName);
+ Query query =
+ new Query("Greeting", guestbookKey)
+ .setAncestor(guestbookKey)
+ .addSort("date", Query.SortDirection.DESCENDING);
+ return datastore.prepare(query)
+ .asList(FetchOptions.Builder.withLimit(10));
+ }
+}
diff --git a/appengine-java8/datastore/src/main/java/com/example/appengine/GuestbookStrongServlet.java b/appengine-java8/datastore/src/main/java/com/example/appengine/GuestbookStrongServlet.java
new file mode 100644
index 00000000000..e73ae1e0fcc
--- /dev/null
+++ b/appengine-java8/datastore/src/main/java/com/example/appengine/GuestbookStrongServlet.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2016 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;
+
+import com.example.time.SystemClock;
+
+public class GuestbookStrongServlet extends AbstractGuestbookServlet {
+ public static final String GUESTBOOK_ID = "my guestbook";
+
+ public GuestbookStrongServlet() {
+ super(new GuestbookStrong(GUESTBOOK_ID, new SystemClock()));
+ }
+}
diff --git a/appengine-java8/datastore/src/main/java/com/example/appengine/ListPeopleServlet.java b/appengine-java8/datastore/src/main/java/com/example/appengine/ListPeopleServlet.java
new file mode 100644
index 00000000000..b59aaab3aa0
--- /dev/null
+++ b/appengine-java8/datastore/src/main/java/com/example/appengine/ListPeopleServlet.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2016 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;
+
+// [START cursors]
+import com.google.appengine.api.datastore.Cursor;
+import com.google.appengine.api.datastore.DatastoreService;
+import com.google.appengine.api.datastore.DatastoreServiceFactory;
+import com.google.appengine.api.datastore.Entity;
+import com.google.appengine.api.datastore.FetchOptions;
+import com.google.appengine.api.datastore.PreparedQuery;
+import com.google.appengine.api.datastore.Query;
+import com.google.appengine.api.datastore.Query.SortDirection;
+import com.google.appengine.api.datastore.QueryResultList;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+public class ListPeopleServlet extends HttpServlet {
+ static final int PAGE_SIZE = 15;
+ private final DatastoreService datastore;
+
+ public ListPeopleServlet() {
+ datastore = DatastoreServiceFactory.getDatastoreService();
+ }
+
+ @Override
+ protected void doGet(HttpServletRequest req, HttpServletResponse resp)
+ throws ServletException, IOException {
+ FetchOptions fetchOptions = FetchOptions.Builder.withLimit(PAGE_SIZE);
+
+ // If this servlet is passed a cursor parameter, let's use it.
+ String startCursor = req.getParameter("cursor");
+ if (startCursor != null) {
+ fetchOptions.startCursor(Cursor.fromWebSafeString(startCursor));
+ }
+
+ Query q = new Query("Person").addSort("name", SortDirection.ASCENDING);
+ PreparedQuery pq = datastore.prepare(q);
+
+ QueryResultList results;
+ try {
+ results = pq.asQueryResultList(fetchOptions);
+ } catch (IllegalArgumentException e) {
+ // IllegalArgumentException happens when an invalid cursor is used.
+ // A user could have manually entered a bad cursor in the URL or there
+ // may have been an internal implementation detail change in App Engine.
+ // Redirect to the page without the cursor parameter to show something
+ // rather than an error.
+ resp.sendRedirect("/people");
+ return;
+ }
+
+ resp.setContentType("text/html");
+ resp.setCharacterEncoding("UTF-8");
+ PrintWriter w = resp.getWriter();
+ w.println("");
+ w.println("");
+ w.println("Cloud Datastore Cursor Sample");
+ w.println("
");
+ for (Entity entity : results) {
+ w.println("
" + entity.getProperty("name") + "
");
+ }
+ w.println("
");
+
+ String cursorString = results.getCursor().toWebSafeString();
+
+ // This servlet lives at '/people'.
+ w.println("Next page");
+ }
+}
+// [END cursors]
diff --git a/appengine-java8/datastore/src/main/java/com/example/appengine/ProjectionServlet.java b/appengine-java8/datastore/src/main/java/com/example/appengine/ProjectionServlet.java
new file mode 100644
index 00000000000..81bdaf6cf9b
--- /dev/null
+++ b/appengine-java8/datastore/src/main/java/com/example/appengine/ProjectionServlet.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2016 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;
+
+import com.google.appengine.api.datastore.DatastoreService;
+import com.google.appengine.api.datastore.DatastoreServiceFactory;
+import com.google.appengine.api.datastore.Entity;
+import com.google.appengine.api.datastore.FetchOptions;
+import com.google.appengine.api.datastore.Key;
+import com.google.appengine.api.datastore.KeyFactory;
+import com.google.appengine.api.datastore.PropertyProjection;
+import com.google.appengine.api.datastore.Query;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.Date;
+import java.util.List;
+
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Servlet to demonstrate use of Datastore projection queries.
+ *
+ *
See the
+ * documentation
+ * for using Datastore projection queries from the Google App Engine standard environment.
+ */
+@SuppressWarnings("serial")
+public class ProjectionServlet extends HttpServlet {
+ private static final String GUESTBOOK_ID = GuestbookStrongServlet.GUESTBOOK_ID;
+ private final DatastoreService datastore;
+
+ public ProjectionServlet() {
+ datastore = DatastoreServiceFactory.getDatastoreService();
+ }
+
+ @Override
+ public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+ resp.setContentType("text/plain");
+ resp.setCharacterEncoding("UTF-8");
+ PrintWriter out = resp.getWriter();
+ out.printf("Latest entries from guestbook: \n");
+
+ Key guestbookKey = KeyFactory.createKey("Guestbook", GUESTBOOK_ID);
+ Query query = new Query("Greeting", guestbookKey);
+ addGuestbookProjections(query);
+ printGuestbookEntries(datastore, query, out);
+ }
+
+ private void addGuestbookProjections(Query query) {
+ query.addProjection(new PropertyProjection("content", String.class));
+ query.addProjection(new PropertyProjection("date", Date.class));
+ }
+
+ private void printGuestbookEntries(DatastoreService datastore, Query query, PrintWriter out) {
+ List guests = datastore.prepare(query).asList(FetchOptions.Builder.withLimit(5));
+ for (Entity guest : guests) {
+ String content = (String) guest.getProperty("content");
+ Date stamp = (Date) guest.getProperty("date");
+ out.printf("Message %s posted on %s.\n", content, stamp.toString());
+ }
+ }
+}
diff --git a/appengine-java8/datastore/src/main/java/com/example/appengine/StartupServlet.java b/appengine-java8/datastore/src/main/java/com/example/appengine/StartupServlet.java
new file mode 100644
index 00000000000..2c523f3db15
--- /dev/null
+++ b/appengine-java8/datastore/src/main/java/com/example/appengine/StartupServlet.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright 2016 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;
+
+import com.google.appengine.api.datastore.DatastoreService;
+import com.google.appengine.api.datastore.DatastoreServiceFactory;
+import com.google.appengine.api.datastore.Entity;
+import com.google.appengine.api.datastore.EntityNotFoundException;
+import com.google.appengine.api.datastore.Key;
+import com.google.appengine.api.datastore.KeyFactory;
+import com.google.common.collect.ImmutableList;
+
+import java.io.IOException;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * A startup handler to populate the datastore with example entities.
+ */
+public class StartupServlet extends HttpServlet {
+ static final String IS_POPULATED_ENTITY = "IsPopulated";
+ static final String IS_POPULATED_KEY_NAME = "is-populated";
+
+ private static final String PERSON_ENTITY = "Person";
+ private static final String NAME_PROPERTY = "name";
+ private static final ImmutableList US_PRESIDENTS =
+ ImmutableList.builder()
+ .add("George Washington")
+ .add("John Adams")
+ .add("Thomas Jefferson")
+ .add("James Madison")
+ .add("James Monroe")
+ .add("John Quincy Adams")
+ .add("Andrew Jackson")
+ .add("Martin Van Buren")
+ .add("William Henry Harrison")
+ .add("John Tyler")
+ .add("James K. Polk")
+ .add("Zachary Taylor")
+ .add("Millard Fillmore")
+ .add("Franklin Pierce")
+ .add("James Buchanan")
+ .add("Abraham Lincoln")
+ .add("Andrew Johnson")
+ .add("Ulysses S. Grant")
+ .add("Rutherford B. Hayes")
+ .add("James A. Garfield")
+ .add("Chester A. Arthur")
+ .add("Grover Cleveland")
+ .add("Benjamin Harrison")
+ .add("Grover Cleveland")
+ .add("William McKinley")
+ .add("Theodore Roosevelt")
+ .add("William Howard Taft")
+ .add("Woodrow Wilson")
+ .add("Warren G. Harding")
+ .add("Calvin Coolidge")
+ .add("Herbert Hoover")
+ .add("Franklin D. Roosevelt")
+ .add("Harry S. Truman")
+ .add("Dwight D. Eisenhower")
+ .add("John F. Kennedy")
+ .add("Lyndon B. Johnson")
+ .add("Richard Nixon")
+ .add("Gerald Ford")
+ .add("Jimmy Carter")
+ .add("Ronald Reagan")
+ .add("George H. W. Bush")
+ .add("Bill Clinton")
+ .add("George W. Bush")
+ .add("Barack Obama")
+ .build();
+
+ @Override
+ protected void doGet(HttpServletRequest req, HttpServletResponse resp)
+ throws ServletException, IOException {
+ resp.setContentType("text/plain");
+ DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
+
+ Key isPopulatedKey = KeyFactory.createKey(IS_POPULATED_ENTITY, IS_POPULATED_KEY_NAME);
+ boolean isAlreadyPopulated;
+ try {
+ datastore.get(isPopulatedKey);
+ isAlreadyPopulated = true;
+ } catch (EntityNotFoundException expected) {
+ isAlreadyPopulated = false;
+ }
+ if (isAlreadyPopulated) {
+ resp.getWriter().println("ok");
+ return;
+ }
+
+ ImmutableList.Builder people = ImmutableList.builder();
+ for (String name : US_PRESIDENTS) {
+ Entity person = new Entity(PERSON_ENTITY);
+ person.setProperty(NAME_PROPERTY, name);
+ people.add(person);
+ }
+ datastore.put(people.build());
+ datastore.put(new Entity(isPopulatedKey));
+ resp.getWriter().println("ok");
+ }
+}
diff --git a/appengine-java8/datastore/src/main/java/com/example/appengine/StatsServlet.java b/appengine-java8/datastore/src/main/java/com/example/appengine/StatsServlet.java
new file mode 100644
index 00000000000..b6d6925fc12
--- /dev/null
+++ b/appengine-java8/datastore/src/main/java/com/example/appengine/StatsServlet.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2016 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;
+
+import com.google.appengine.api.datastore.DatastoreService;
+import com.google.appengine.api.datastore.DatastoreServiceFactory;
+import com.google.appengine.api.datastore.Entity;
+import com.google.appengine.api.datastore.Query;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+public class StatsServlet extends HttpServlet {
+ @Override
+ protected void doGet(HttpServletRequest req, HttpServletResponse resp)
+ throws ServletException, IOException {
+ // [START stat_example]
+ DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
+ Entity globalStat = datastore.prepare(new Query("__Stat_Total__")).asSingleEntity();
+ Long totalBytes = (Long) globalStat.getProperty("bytes");
+ Long totalEntities = (Long) globalStat.getProperty("count");
+ // [END stat_example]
+
+ resp.setContentType("text/plain");
+ resp.setCharacterEncoding("UTF-8");
+ PrintWriter w = resp.getWriter();
+ w.printf("%d bytes\n%d entities\n", totalBytes, totalEntities);
+ }
+}
+// [END cursors]
diff --git a/appengine-java8/datastore/src/main/java/com/example/time/Clock.java b/appengine-java8/datastore/src/main/java/com/example/time/Clock.java
new file mode 100644
index 00000000000..7d75e2b62d4
--- /dev/null
+++ b/appengine-java8/datastore/src/main/java/com/example/time/Clock.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2016 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.time;
+
+import org.joda.time.Instant;
+
+/**
+ * Provides the current value of "now." To preserve testability, avoid all other libraries that
+ * access the system clock (whether {@linkplain System#currentTimeMillis directly} or {@linkplain
+ * org.joda.time.DateTime#DateTime() indirectly}).
+ *
+ *
In production, use the {@link SystemClock} implementation to return the "real" system time. In
+ * tests, either use {@link com.example.time.testing.FakeClock}, or get an instance from a mocking
+ * framework such as Mockito.
+ */
+public interface Clock {
+ /**
+ * Returns the current, absolute time according to this clock.
+ */
+ Instant now();
+}
diff --git a/appengine-java8/datastore/src/main/java/com/example/time/SystemClock.java b/appengine-java8/datastore/src/main/java/com/example/time/SystemClock.java
new file mode 100644
index 00000000000..032230bd0c4
--- /dev/null
+++ b/appengine-java8/datastore/src/main/java/com/example/time/SystemClock.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2016 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.time;
+
+import org.joda.time.Instant;
+
+/**
+ * Clock implementation that returns the "real" system time.
+ *
+ *
This class exists so that we can use a fake implementation for unit
+ * testing classes that need the current time value. See {@link Clock} for
+ * general information about clocks.
+ */
+public class SystemClock implements Clock {
+ /**
+ * Creates a new instance. All {@code SystemClock} instances function identically.
+ */
+ public SystemClock() {}
+
+ @Override
+ public Instant now() {
+ return new Instant();
+ }
+}
diff --git a/appengine-java8/datastore/src/main/java/com/example/time/testing/FakeClock.java b/appengine-java8/datastore/src/main/java/com/example/time/testing/FakeClock.java
new file mode 100644
index 00000000000..7ede635d762
--- /dev/null
+++ b/appengine-java8/datastore/src/main/java/com/example/time/testing/FakeClock.java
@@ -0,0 +1,186 @@
+/*
+ * Copyright 2016 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.time.testing;
+
+import com.example.time.Clock;
+
+import org.joda.time.Instant;
+import org.joda.time.ReadableDuration;
+import org.joda.time.ReadableInstant;
+
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * A Clock that returns a fixed Instant value as the current clock time. The
+ * fixed Instant is settable for testing. Test code should hold a reference to
+ * the FakeClock, while code under test should hold a Clock reference.
+ *
+ *
The clock time can be incremented/decremented manually, with
+ * {@link #incrementTime} and {@link #decrementTime} respectively.
+ *
+ *
The clock can also be configured so that the time is incremented whenever
+ * {@link #now()} is called: see {@link #setAutoIncrementStep}.
+ */
+public class FakeClock implements Clock {
+ private static final Instant DEFAULT_TIME = new Instant(1000000000L);
+ private final long baseTimeMs;
+ private final AtomicLong fakeNowMs;
+ private volatile long autoIncrementStepMs;
+
+ /**
+ * Creates a FakeClock instance initialized to an arbitrary constant.
+ */
+ public FakeClock() {
+ this(DEFAULT_TIME);
+ }
+
+ /**
+ * Creates a FakeClock instance initialized to the given time.
+ */
+ public FakeClock(ReadableInstant now) {
+ baseTimeMs = now.getMillis();
+ fakeNowMs = new AtomicLong(baseTimeMs);
+ }
+
+ /**
+ * Sets the value of the underlying instance for testing purposes.
+ *
+ * @return this
+ */
+ public FakeClock setNow(ReadableInstant now) {
+ fakeNowMs.set(now.getMillis());
+ return this;
+ }
+
+ @Override
+ public Instant now() {
+ return getAndAdd(autoIncrementStepMs);
+ }
+
+ /**
+ * Returns the current time without applying an auto increment, if configured.
+ * The default behavior of {@link #now()} is the same as this method.
+ */
+ public Instant peek() {
+ return new Instant(fakeNowMs.get());
+ }
+
+ /**
+ * Reset the given clock back to the base time with which the FakeClock was
+ * initially constructed.
+ *
+ * @return this
+ */
+ public FakeClock resetTime() {
+ fakeNowMs.set(baseTimeMs);
+ return this;
+ }
+
+ /**
+ * Increments the clock time by the given duration.
+ *
+ * @param duration the duration to increment the clock time by
+ * @return this
+ */
+ public FakeClock incrementTime(ReadableDuration duration) {
+ incrementTime(duration.getMillis());
+ return this;
+ }
+
+ /**
+ * Increments the clock time by the given duration.
+ *
+ * @param durationMs the duration to increment the clock time by,
+ * in milliseconds
+ * @return this
+ */
+ public FakeClock incrementTime(long durationMs) {
+ fakeNowMs.addAndGet(durationMs);
+ return this;
+ }
+
+ /**
+ * Decrements the clock time by the given duration.
+ *
+ * @param duration the duration to decrement the clock time by
+ * @return this
+ */
+ public FakeClock decrementTime(ReadableDuration duration) {
+ incrementTime(-duration.getMillis());
+ return this;
+ }
+
+ /**
+ * Decrements the clock time by the given duration.
+ *
+ * @param durationMs the duration to decrement the clock time by,
+ * in milliseconds
+ * @return this
+ */
+ public FakeClock decrementTime(long durationMs) {
+ incrementTime(-durationMs);
+ return this;
+ }
+
+ /**
+ * Sets the increment applied to the clock whenever it is queried.
+ * The increment is zero by default: the clock is left unchanged when queried.
+ *
+ * @param autoIncrementStep the new auto increment duration
+ * @return this
+ */
+ public FakeClock setAutoIncrementStep(ReadableDuration autoIncrementStep) {
+ setAutoIncrementStep(autoIncrementStep.getMillis());
+ return this;
+ }
+
+ /**
+ * Sets the increment applied to the clock whenever it is queried.
+ * The increment is zero by default: the clock is left unchanged when queried.
+ *
+ * @param autoIncrementStepMs the new auto increment duration, in milliseconds
+ * @return this
+ */
+ public FakeClock setAutoIncrementStep(long autoIncrementStepMs) {
+ this.autoIncrementStepMs = autoIncrementStepMs;
+ return this;
+ }
+
+ /**
+ * Atomically adds the given value to the current time.
+ *
+ * @see AtomicLong#addAndGet
+ *
+ * @param durationMs the duration to add, in milliseconds
+ * @return the updated current time
+ */
+ protected final Instant addAndGet(long durationMs) {
+ return new Instant(fakeNowMs.addAndGet(durationMs));
+ }
+
+ /**
+ * Atomically adds the given value to the current time.
+ *
+ * @see AtomicLong#getAndAdd
+ *
+ * @param durationMs the duration to add, in milliseconds
+ * @return the previous time
+ */
+ protected final Instant getAndAdd(long durationMs) {
+ return new Instant(fakeNowMs.getAndAdd(durationMs));
+ }
+}
diff --git a/appengine-java8/datastore/src/main/webapp/WEB-INF/appengine-web.xml b/appengine-java8/datastore/src/main/webapp/WEB-INF/appengine-web.xml
new file mode 100644
index 00000000000..5d2d5ad2429
--- /dev/null
+++ b/appengine-java8/datastore/src/main/webapp/WEB-INF/appengine-web.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+ true
+ java8
+
diff --git a/appengine-java8/datastore/src/main/webapp/WEB-INF/datastore-indexes.xml b/appengine-java8/datastore/src/main/webapp/WEB-INF/datastore-indexes.xml
new file mode 100644
index 00000000000..93f9a3d76cc
--- /dev/null
+++ b/appengine-java8/datastore/src/main/webapp/WEB-INF/datastore-indexes.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/appengine-java8/datastore/src/main/webapp/WEB-INF/web.xml b/appengine-java8/datastore/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 00000000000..cf06f60d61e
--- /dev/null
+++ b/appengine-java8/datastore/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,97 @@
+
+
+
+
+ guestbook-strong
+ com.example.appengine.GuestbookStrongServlet
+
+
+ guestbook-strong
+ /
+
+
+ guestbook
+ com.example.appengine.GuestbookServlet
+
+
+ guestbook
+ /guestbook
+
+
+ people
+ com.example.appengine.ListPeopleServlet
+
+
+ people
+ /people
+
+
+ projection
+ com.example.appengine.ProjectionServlet
+
+
+ projection
+ /projection
+
+
+ stats
+ com.example.appengine.StatsServlet
+
+
+ stats
+ /stats
+
+
+
+
+ startup
+ com.example.appengine.StartupServlet
+
+
+ startup
+ /_ah/start
+
+
+
+
+ profile
+ /*
+
+
+ CONFIDENTIAL
+
+
+ *
+
+
+
+
+
+ profile
+ /stats
+
+
+ CONFIDENTIAL
+
+
+ admin
+
+
+
diff --git a/appengine-java8/datastore/src/main/webapp/guestbook.jsp b/appengine-java8/datastore/src/main/webapp/guestbook.jsp
new file mode 100644
index 00000000000..4b65c8a308e
--- /dev/null
+++ b/appengine-java8/datastore/src/main/webapp/guestbook.jsp
@@ -0,0 +1,45 @@
+
+
+<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
+<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn" %>
+
+
+
+
diff --git a/appengine-java8/endpoints-frameworks-v2/migration-example/src/main/webapp/js/base.js b/appengine-java8/endpoints-frameworks-v2/migration-example/src/main/webapp/js/base.js
new file mode 100644
index 00000000000..8b74d85653f
--- /dev/null
+++ b/appengine-java8/endpoints-frameworks-v2/migration-example/src/main/webapp/js/base.js
@@ -0,0 +1,208 @@
+/*
+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.
+*/
+
+/**
+ * @fileoverview
+ * Provides methods for the Hello Endpoints sample UI and interaction with the
+ * Hello Endpoints API.
+ *
+ * @author danielholevoet@google.com (Dan Holevoet)
+ */
+
+/** google global namespace for Google projects. */
+var google = google || {};
+
+/** devrel namespace for Google Developer Relations projects. */
+google.devrel = google.devrel || {};
+
+/** samples namespace for DevRel sample code. */
+google.devrel.samples = google.devrel.samples || {};
+
+/** hello namespace for this sample. */
+google.devrel.samples.hello = google.devrel.samples.hello || {};
+
+/**
+ * Client ID of the application (from the APIs Console).
+ * @type {string}
+ */
+google.devrel.samples.hello.CLIENT_ID =
+ 'replace this with your web application client ID';
+
+/**
+ * Scopes used by the application.
+ * @type {string}
+ */
+google.devrel.samples.hello.SCOPES =
+ 'https://www.googleapis.com/auth/userinfo.email';
+
+/**
+ * Whether or not the user is signed in.
+ * @type {boolean}
+ */
+google.devrel.samples.hello.signedIn = false;
+
+/**
+ * Loads the application UI after the user has completed auth.
+ */
+google.devrel.samples.hello.userAuthed = function() {
+ var request = gapi.client.oauth2.userinfo.get().execute(function(resp) {
+ if (!resp.code) {
+ google.devrel.samples.hello.signedIn = true;
+ document.getElementById('signinButton').innerHTML = 'Sign out';
+ document.getElementById('authedGreeting').disabled = false;
+ }
+ });
+};
+
+/**
+ * Handles the auth flow, with the given value for immediate mode.
+ * @param {boolean} mode Whether or not to use immediate mode.
+ * @param {Function} callback Callback to call on completion.
+ */
+google.devrel.samples.hello.signin = function(mode, callback) {
+ gapi.auth.authorize({client_id: google.devrel.samples.hello.CLIENT_ID,
+ scope: google.devrel.samples.hello.SCOPES, immediate: mode},
+ callback);
+};
+
+/**
+ * Presents the user with the authorization popup.
+ */
+google.devrel.samples.hello.auth = function() {
+ if (!google.devrel.samples.hello.signedIn) {
+ google.devrel.samples.hello.signin(false,
+ google.devrel.samples.hello.userAuthed);
+ } else {
+ google.devrel.samples.hello.signedIn = false;
+ document.getElementById('signinButton').innerHTML = 'Sign in';
+ document.getElementById('authedGreeting').disabled = true;
+ }
+};
+
+/**
+ * Prints a greeting to the greeting log.
+ * param {Object} greeting Greeting to print.
+ */
+google.devrel.samples.hello.print = function(greeting) {
+ var element = document.createElement('div');
+ element.classList.add('row');
+ element.innerHTML = greeting.message;
+ document.getElementById('outputLog').appendChild(element);
+};
+
+/**
+ * Gets a numbered greeting via the API.
+ * @param {string} id ID of the greeting.
+ */
+google.devrel.samples.hello.getGreeting = function(id) {
+ gapi.client.helloworld.greetings.getGreeting({'id': id}).execute(
+ function(resp) {
+ if (!resp.code) {
+ google.devrel.samples.hello.print(resp);
+ }
+ });
+};
+
+/**
+ * Lists greetings via the API.
+ */
+google.devrel.samples.hello.listGreeting = function() {
+ gapi.client.helloworld.greetings.listGreeting().execute(
+ function(resp) {
+ if (!resp.code) {
+ resp.items = resp.items || [];
+ for (var i = 0; i < resp.items.length; i++) {
+ google.devrel.samples.hello.print(resp.items[i]);
+ }
+ }
+ });
+};
+
+/**
+ * Gets a greeting a specified number of times.
+ * @param {string} greeting Greeting to repeat.
+ * @param {string} count Number of times to repeat it.
+ */
+google.devrel.samples.hello.multiplyGreeting = function(
+ greeting, times) {
+ gapi.client.helloworld.greetings.multiply({
+ 'message': greeting,
+ 'times': times
+ }).execute(function(resp) {
+ if (!resp.code) {
+ google.devrel.samples.hello.print(resp);
+ }
+ });
+};
+
+/**
+ * Greets the current user via the API.
+ */
+google.devrel.samples.hello.authedGreeting = function(id) {
+ gapi.client.helloworld.greetings.authed().execute(
+ function(resp) {
+ google.devrel.samples.hello.print(resp);
+ });
+};
+
+/**
+ * Enables the button callbacks in the UI.
+ */
+google.devrel.samples.hello.enableButtons = function() {
+ document.getElementById('getGreeting').onclick = function() {
+ google.devrel.samples.hello.getGreeting(
+ document.getElementById('id').value);
+ }
+
+ document.getElementById('listGreeting').onclick = function() {
+ google.devrel.samples.hello.listGreeting();
+ }
+
+ document.getElementById('multiplyGreetings').onclick = function() {
+ google.devrel.samples.hello.multiplyGreeting(
+ document.getElementById('greeting').value,
+ document.getElementById('count').value);
+ }
+
+ document.getElementById('authedGreeting').onclick = function() {
+ google.devrel.samples.hello.authedGreeting();
+ }
+
+ document.getElementById('signinButton').onclick = function() {
+ google.devrel.samples.hello.auth();
+ }
+};
+
+/**
+ * Initializes the application.
+ * @param {string} apiRoot Root of the API's path.
+ */
+google.devrel.samples.hello.init = function(apiRoot) {
+ // Loads the OAuth and helloworld APIs asynchronously, and triggers login
+ // when they have completed.
+ var apisToLoad;
+ var callback = function() {
+ if (--apisToLoad == 0) {
+ google.devrel.samples.hello.enableButtons();
+ google.devrel.samples.hello.signin(true,
+ google.devrel.samples.hello.userAuthed);
+ }
+ }
+
+ apisToLoad = 2; // must match number of calls to gapi.client.load()
+ gapi.client.load('helloworld', 'v1', callback, apiRoot);
+ gapi.client.load('oauth2', 'v2', callback);
+};
diff --git a/appengine-java8/firebase-event-proxy/README.md b/appengine-java8/firebase-event-proxy/README.md
new file mode 100644
index 00000000000..1b8c0317a11
--- /dev/null
+++ b/appengine-java8/firebase-event-proxy/README.md
@@ -0,0 +1,49 @@
+# App Engine Firebase Event Proxy
+
+An example app that illustrates how to create a Java App Engine Standard Environment
+app that proxies Firebase events to another App Engine app.
+
+# Java Firebase Event Proxy
+Illustrates how to authenticate and subscribe to Firebase from Java App Engine.
+
+# Python App Engine Listener
+Illustrates how to authenticate messages received from the proxy app.
+
+## Setup
+
+### Java Firebase Event Proxy
+Firebase Secret
+Put your Firebase secret in the file:
+gae-firebase-event-proxy/src/main/webapp/firebase-secret.properties
+```
+firebaseSecret=
+```
+
+* Billing must be enabled from Cloud console.
+* Manual scaling should turned on and configured to 1 instance in appengine-web.xml
+
+## Running locally
+### Java Firebase Event Proxy
+```
+cd gae-firebase-event-proxy
+mvn appengine:run
+```
+
+### Python App Engine Listener
+```
+cd gae-firebase-listener-python
+dev_appserver .
+```
+
+## Deploying
+
+### Java Firebase Event Proxy
+```
+cd gae-firebase-event-proxy
+mvn appengine:deploy
+```
+
+### Python App Engine Listener
+```
+appcfg.py -A -V v1 update gae-firebase-listener-python
+```
diff --git a/appengine-java8/firebase-event-proxy/gae-firebase-event-proxy/pom.xml b/appengine-java8/firebase-event-proxy/gae-firebase-event-proxy/pom.xml
new file mode 100644
index 00000000000..374ce1fa4cf
--- /dev/null
+++ b/appengine-java8/firebase-event-proxy/gae-firebase-event-proxy/pom.xml
@@ -0,0 +1,103 @@
+
+
+ 4.0.0
+ war
+ 1.0-SNAPSHOT
+
+ com.example.gaefirebaseeventproxy
+ gaefirebaseeventproxy-j8
+
+
+ com.google.cloud
+ appengine-java8-samples
+ 1.0.0
+ ../..
+
+
+
+ gae-firebase-event-proxy
+ 1
+ UTF-8
+ true
+
+
+
+
+
+ com.google.appengine
+ appengine-api-1.0-sdk
+ ${appengine.sdk.version}
+
+
+ javax.servlet
+ javax.servlet-api
+ 3.1.0
+ jar
+ provided
+
+
+
+ jstl
+ jstl
+ 1.2
+
+
+ com.google.firebase
+ firebase-server-sdk
+ 3.0.3
+
+
+ com.fasterxml.jackson.core
+ jackson-core
+ 2.9.0.pr3
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+ 2.9.0.pr3
+
+
+
+
+ com.google.appengine
+ appengine-testing
+ ${appengine.sdk.version}
+ test
+
+
+ com.google.appengine
+ appengine-api-stubs
+ ${appengine.sdk.version}
+ test
+
+
+
+
+
+ ${project.build.directory}/${project.build.finalName}/WEB-INF/classes
+
+
+ com.google.cloud.tools
+ appengine-maven-plugin
+ 1.3.1
+
+ true
+ true
+
+
+
+
+
diff --git a/appengine-java8/firebase-event-proxy/gae-firebase-event-proxy/src/main/java/com/example/GaeFirebaseEventProxy/FirebaseEventProxy.java b/appengine-java8/firebase-event-proxy/gae-firebase-event-proxy/src/main/java/com/example/GaeFirebaseEventProxy/FirebaseEventProxy.java
new file mode 100644
index 00000000000..4a115b74c5d
--- /dev/null
+++ b/appengine-java8/firebase-event-proxy/gae-firebase-event-proxy/src/main/java/com/example/GaeFirebaseEventProxy/FirebaseEventProxy.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2016 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.gaefirebaseeventproxy;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.appengine.api.utils.SystemProperty;
+import com.google.firebase.FirebaseApp;
+import com.google.firebase.FirebaseOptions;
+import com.google.firebase.database.DataSnapshot;
+import com.google.firebase.database.DatabaseError;
+import com.google.firebase.database.DatabaseReference;
+import com.google.firebase.database.FirebaseDatabase;
+import com.google.firebase.database.ValueEventListener;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.net.URLEncoder;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.logging.Logger;
+
+public class FirebaseEventProxy {
+
+ private static final Logger log = Logger.getLogger(FirebaseEventProxy.class.getName());
+
+ public FirebaseEventProxy() {
+ String firebaseLocation = "https://crackling-torch-392.firebaseio.com";
+ Map databaseAuthVariableOverride = new HashMap();
+ // uid and provider will have to match what you have in your firebase security rules
+ databaseAuthVariableOverride.put("uid", "gae-firebase-event-proxy");
+ databaseAuthVariableOverride.put("provider", "com.example");
+ try {
+ FirebaseOptions options = new FirebaseOptions.Builder()
+ .setServiceAccount(new FileInputStream("gae-firebase-secrets.json"))
+ .setDatabaseUrl(firebaseLocation)
+ .setDatabaseAuthVariableOverride(databaseAuthVariableOverride).build();
+ FirebaseApp.initializeApp(options);
+ } catch (IOException e) {
+ throw new RuntimeException(
+ "Error reading firebase secrets from file: src/main/webapp/gae-firebase-secrets.json: "
+ + e.getMessage());
+ }
+ }
+
+ public void start() {
+ DatabaseReference firebase = FirebaseDatabase.getInstance().getReference();
+
+ // Subscribe to value events. Depending on use case, you may want to subscribe to child events
+ // through childEventListener.
+ firebase.addValueEventListener(new ValueEventListener() {
+ @Override
+ public void onDataChange(DataSnapshot snapshot) {
+ if (snapshot.exists()) {
+ try {
+ // Convert value to JSON using Jackson
+ String json = new ObjectMapper().writeValueAsString(snapshot.getValue(false));
+
+ // Replace the URL with the url of your own listener app.
+ URL dest = new URL("http://gae-firebase-listener-python.appspot.com/log");
+ HttpURLConnection connection = (HttpURLConnection) dest.openConnection();
+ connection.setRequestMethod("POST");
+ connection.setDoOutput(true);
+
+ // Rely on X-Appengine-Inbound-Appid to authenticate. Turning off redirects is
+ // required to enable.
+ connection.setInstanceFollowRedirects(false);
+
+ // Fill out header if in dev environment
+ if (SystemProperty.environment.value() != SystemProperty.Environment.Value.Production) {
+ connection.setRequestProperty("X-Appengine-Inbound-Appid", "dev-instance");
+ }
+
+ // Put Firebase data into http request
+ StringBuilder stringBuilder = new StringBuilder();
+ stringBuilder.append("&fbSnapshot=");
+ stringBuilder.append(URLEncoder.encode(json, "UTF-8"));
+ connection.getOutputStream().write(stringBuilder.toString().getBytes());
+ if (connection.getResponseCode() != 200) {
+ log.severe("Forwarding failed");
+ } else {
+ log.info("Sent: " + json);
+ }
+ } catch (JsonProcessingException e) {
+ log.severe("Unable to convert Firebase response to JSON: " + e.getMessage());
+ } catch (IOException e) {
+ log.severe("Error in connecting to app engine: " + e.getMessage());
+ }
+ }
+ }
+
+ @Override
+ public void onCancelled(DatabaseError error) {
+ log.severe("Firebase connection cancelled: " + error.getMessage());
+ }
+ });
+ }
+}
diff --git a/appengine-java8/firebase-event-proxy/gae-firebase-event-proxy/src/main/java/com/example/GaeFirebaseEventProxy/ServletContextListenerImpl.java b/appengine-java8/firebase-event-proxy/gae-firebase-event-proxy/src/main/java/com/example/GaeFirebaseEventProxy/ServletContextListenerImpl.java
new file mode 100644
index 00000000000..ca11ec2ba9a
--- /dev/null
+++ b/appengine-java8/firebase-event-proxy/gae-firebase-event-proxy/src/main/java/com/example/GaeFirebaseEventProxy/ServletContextListenerImpl.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2016 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.gaefirebaseeventproxy;
+
+import java.util.logging.Logger;
+
+import javax.servlet.ServletContextEvent;
+import javax.servlet.ServletContextListener;
+
+// ServletContextListener that is called whenever your App Engine app starts up.
+public class ServletContextListenerImpl implements ServletContextListener {
+
+ private static final Logger log = Logger.getLogger(ServletContextListener.class.getName());
+
+ @Override
+ public void contextInitialized(ServletContextEvent event) {
+ log.info("Starting ....");
+ FirebaseEventProxy proxy = new FirebaseEventProxy();
+ proxy.start();
+ }
+
+ @Override
+ public void contextDestroyed(ServletContextEvent event) {
+ // App Engine does not currently invoke this method.
+ }
+}
diff --git a/appengine-java8/firebase-event-proxy/gae-firebase-event-proxy/src/main/webapp/WEB-INF/appengine-web.xml b/appengine-java8/firebase-event-proxy/gae-firebase-event-proxy/src/main/webapp/WEB-INF/appengine-web.xml
new file mode 100644
index 00000000000..5aa793d9834
--- /dev/null
+++ b/appengine-java8/firebase-event-proxy/gae-firebase-event-proxy/src/main/webapp/WEB-INF/appengine-web.xml
@@ -0,0 +1,25 @@
+
+
+
+ java8
+ true
+
+
+ 1
+
+
+
+
+
+
diff --git a/appengine-java8/firebase-event-proxy/gae-firebase-event-proxy/src/main/webapp/WEB-INF/logging.properties b/appengine-java8/firebase-event-proxy/gae-firebase-event-proxy/src/main/webapp/WEB-INF/logging.properties
new file mode 100644
index 00000000000..a2cc700aef5
--- /dev/null
+++ b/appengine-java8/firebase-event-proxy/gae-firebase-event-proxy/src/main/webapp/WEB-INF/logging.properties
@@ -0,0 +1,27 @@
+# Copyright 2016 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.
+
+# A default java.util.logging configuration.
+# (All App Engine logging is through java.util.logging by default).
+#
+# To use this configuration, copy it into your application's WEB-INF
+# folder and add the following to your appengine-web.xml:
+#
+#
+#
+#
+#
+
+# Set the default logging level for all loggers to WARNING
+.level = INFO
diff --git a/appengine-java8/firebase-event-proxy/gae-firebase-event-proxy/src/main/webapp/WEB-INF/web.xml b/appengine-java8/firebase-event-proxy/gae-firebase-event-proxy/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 00000000000..5c9f392b35e
--- /dev/null
+++ b/appengine-java8/firebase-event-proxy/gae-firebase-event-proxy/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+ com.example.gaefirebaseeventproxy.ServletContextListenerImpl
+
+
+ index.jsp
+
+
diff --git a/appengine-java8/firebase-event-proxy/gae-firebase-event-proxy/src/main/webapp/index.jsp b/appengine-java8/firebase-event-proxy/gae-firebase-event-proxy/src/main/webapp/index.jsp
new file mode 100644
index 00000000000..1caf98ad673
--- /dev/null
+++ b/appengine-java8/firebase-event-proxy/gae-firebase-event-proxy/src/main/webapp/index.jsp
@@ -0,0 +1,25 @@
+<%--
+Copyright 2015 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.
+--%>
+
+<%@ page contentType="text/html;charset=UTF-8" language="java" %>
+
+
+
+
+
+
+
+
Status: up
+
+
+
diff --git a/appengine-java8/firebase-event-proxy/gae-firebase-listener-python/.gitignore b/appengine-java8/firebase-event-proxy/gae-firebase-listener-python/.gitignore
new file mode 100644
index 00000000000..0d20b6487c6
--- /dev/null
+++ b/appengine-java8/firebase-event-proxy/gae-firebase-listener-python/.gitignore
@@ -0,0 +1 @@
+*.pyc
diff --git a/appengine-java8/firebase-event-proxy/gae-firebase-listener-python/app.yaml b/appengine-java8/firebase-event-proxy/gae-firebase-listener-python/app.yaml
new file mode 100644
index 00000000000..f041d384c05
--- /dev/null
+++ b/appengine-java8/firebase-event-proxy/gae-firebase-listener-python/app.yaml
@@ -0,0 +1,7 @@
+runtime: python27
+api_version: 1
+threadsafe: true
+
+handlers:
+- url: /.*
+ script: main.app
diff --git a/appengine-java8/firebase-event-proxy/gae-firebase-listener-python/main.py b/appengine-java8/firebase-event-proxy/gae-firebase-listener-python/main.py
new file mode 100644
index 00000000000..50990be57b0
--- /dev/null
+++ b/appengine-java8/firebase-event-proxy/gae-firebase-listener-python/main.py
@@ -0,0 +1,38 @@
+# Copyright 2016 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.
+
+import os
+import webapp2
+
+IS_DEV = os.environ["SERVER_SOFTWARE"][:3] == "Dev"
+allowed_users = set()
+if IS_DEV:
+ allowed_users.add("dev-instance")
+else:
+ # Add your Java App Engine proxy App Id here
+ allowed_users.add("your-java-appengine-proxy-app-id")
+
+class LoggingHandler(webapp2.RequestHandler):
+
+ def post(self):
+ user = self.request.headers.get('X-Appengine-Inbound-Appid', None)
+ if user and user in allowed_users:
+ firebaseSnapshot = self.request.params['fbSnapshot']
+ print firebaseSnapshot
+ else:
+ print "Got unauthenticated user: %s" % user
+
+app = webapp2.WSGIApplication([
+ webapp2.Route('/log', LoggingHandler),
+])
diff --git a/appengine-java8/firebase-tictactoe/README.md b/appengine-java8/firebase-tictactoe/README.md
new file mode 100644
index 00000000000..fc7fbf0d052
--- /dev/null
+++ b/appengine-java8/firebase-tictactoe/README.md
@@ -0,0 +1,53 @@
+# Tic Tac Toe on Google App Engine Standard using Firebase
+
+This directory contains a project that implements a realtime two-player game of
+Tic Tac Toe on Google [App Engine Standard][standard], using the [Firebase] database
+for realtime notifications when the board changes.
+
+[Firebase]: https://firebase.google.com
+[standard]: https://cloud.google.com/appengine/docs/about-the-standard-environment
+
+## Prerequisites
+
+* Install [Apache Maven][maven] 3.5.0 or later
+* Install the [Google Cloud SDK][sdk]
+* Create a project in the [Firebase Console][fb-console]
+* In the [Overview section][fb-overview] of the Firebase console, click 'Add
+ Firebase to your web app' and replace the contents of the file
+ `src/main/webapp/WEB-INF/view/firebase_config.jspf` with that code snippet.
+
+[fb-console]: https://console.firebase.google.com
+[sdk]: https://cloud.google.com/sdk
+[creds]: https://console.firebase.google.com/iam-admin/serviceaccounts/project?project=_&consoleReturnUrl=https:%2F%2Fconsole.firebase.google.com%2Fproject%2F_%2Fsettings%2Fgeneral%2F
+[fb-overview]: https://console.firebase.google.com/project/_/overview
+[maven]: https://maven.apache.org
+
+
+## Run the sample
+
+* To run the app locally using the development appserver:
+
+```sh
+mvn appengine:run
+```
+
+## Troubleshooting
+
+* If you see the error `Google Cloud SDK path was not provided ...`:
+ * Make sure you've installed the [Google Cloud SDK][sdk]
+ * Make sure the Google Cloud SDK's `bin/` directory is in your `PATH`. If
+ you prefer it not to be, you can also set the environment variable
+ `GOOGLE_CLOUD_SDK_HOME` to point to where you installed the SDK:
+
+```sh
+export GOOGLE_CLOUD_SDK_HOME=/path/to/google-cloud-sdk
+```
+
+## Contributing changes
+
+See [CONTRIBUTING.md](../../CONTRIBUTING.md).
+
+## Licensing
+
+See [LICENSE](../../LICENSE).
+
diff --git a/appengine-java8/firebase-tictactoe/pom.xml b/appengine-java8/firebase-tictactoe/pom.xml
new file mode 100644
index 00000000000..ae23e13cb6d
--- /dev/null
+++ b/appengine-java8/firebase-tictactoe/pom.xml
@@ -0,0 +1,127 @@
+
+
+ 4.0.0
+ war
+ 1.0-SNAPSHOT
+ com.example.appengine
+ appengine-firebase-tictactoe-j8
+
+ com.google.cloud
+ appengine-java8-samples
+ 1.0.0
+ ..
+
+
+
+ 5.1.17
+
+ 2.7
+ 20.0
+ 1.22.0
+ 4.12
+ 1.10.19
+ 0.32
+ 1.3.1
+
+
+
+
+ com.google.appengine
+ appengine-api-1.0-sdk
+ ${appengine.sdk.version}
+
+
+
+ javax.servlet
+ javax.servlet-api
+ 3.1.0
+ jar
+ provided
+
+
+
+ com.google.code.gson
+ gson
+ 2.8.0
+
+
+ com.googlecode.objectify
+ objectify
+ ${objectify.version}
+
+
+ com.google.guava
+ guava
+ ${guava.version}
+
+
+ com.google.api-client
+ google-api-client-appengine
+ ${google-api-client.version}
+
+
+
+
+
+ junit
+ junit
+ ${junit.version}
+ test
+
+
+ org.mockito
+ mockito-all
+ ${mockito.version}
+ test
+
+
+ com.google.appengine
+ appengine-testing
+ ${appengine.sdk.version}
+ test
+
+
+ com.google.appengine
+ appengine-api-stubs
+ ${appengine.sdk.version}
+ test
+
+
+ com.google.appengine
+ appengine-tools-sdk
+ ${appengine.sdk.version}
+ test
+
+
+ com.google.truth
+ truth
+ ${google-truth.version}
+ test
+
+
+
+
+ ${project.build.directory}/${project.build.finalName}/WEB-INF/classes
+
+
+ com.google.cloud.tools
+ appengine-maven-plugin
+ ${appengine-maven.version}
+
+
+
+
diff --git a/appengine-java8/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/DeleteServlet.java b/appengine-java8/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/DeleteServlet.java
new file mode 100644
index 00000000000..1988424fbb6
--- /dev/null
+++ b/appengine-java8/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/DeleteServlet.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2016 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.firetactoe;
+
+import com.google.appengine.api.users.UserService;
+import com.google.appengine.api.users.UserServiceFactory;
+import com.googlecode.objectify.Objectify;
+import com.googlecode.objectify.ObjectifyService;
+
+import java.io.IOException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Handler that deletes the Firebase database that serves as the realtime communication channel.
+ * This handler should be invoked after a game has finished, to clean up the channel.
+ */
+public class DeleteServlet extends HttpServlet {
+ @Override
+ public void doPost(HttpServletRequest request, HttpServletResponse response)
+ throws IOException {
+ String gameId = request.getParameter("gameKey");
+ Objectify ofy = ObjectifyService.ofy();
+ Game game = ofy.load().type(Game.class).id(gameId).safe();
+
+ UserService userService = UserServiceFactory.getUserService();
+ String currentUserId = userService.getCurrentUser().getUserId();
+
+ // TODO(you): In practice, first validate that the user has permission to delete the Game
+ game.deleteChannel(currentUserId);
+ }
+}
diff --git a/appengine-java8/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/FirebaseChannel.java b/appengine-java8/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/FirebaseChannel.java
new file mode 100644
index 00000000000..5c859d65dbe
--- /dev/null
+++ b/appengine-java8/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/FirebaseChannel.java
@@ -0,0 +1,235 @@
+/*
+ * Copyright 2016 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.firetactoe;
+
+import com.google.api.client.auth.oauth2.Credential;
+import com.google.api.client.extensions.appengine.http.UrlFetchTransport;
+import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;
+import com.google.api.client.http.ByteArrayContent;
+import com.google.api.client.http.GenericUrl;
+import com.google.api.client.http.HttpRequestFactory;
+import com.google.api.client.http.HttpResponse;
+import com.google.api.client.http.HttpTransport;
+import com.google.appengine.api.appidentity.AppIdentityService;
+import com.google.appengine.api.appidentity.AppIdentityServiceFactory;
+import com.google.common.io.BaseEncoding;
+import com.google.common.io.CharStreams;
+import com.google.gson.Gson;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Utility functions for communicating with the realtime communication channel using Firebase.
+ * In this app, we use Firebase as a communication bus to push the state of the board to all clients
+ * - that is, players of the game. This class contains the methods used to communicate with
+ * Firebase.
+ */
+public class FirebaseChannel {
+ private static final String FIREBASE_SNIPPET_PATH = "WEB-INF/view/firebase_config.jspf";
+ static InputStream firebaseConfigStream = null;
+ private static final Collection FIREBASE_SCOPES = Arrays.asList(
+ "https://www.googleapis.com/auth/firebase.database",
+ "https://www.googleapis.com/auth/userinfo.email"
+ );
+ private static final String IDENTITY_ENDPOINT =
+ "https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit";
+
+ private String firebaseDbUrl;
+ private GoogleCredential credential;
+ // Keep this a package-private member variable, so that it can be mocked for unit tests
+ HttpTransport httpTransport;
+
+ private static FirebaseChannel instance;
+
+ /**
+ * FirebaseChannel is a singleton, since it's just utility functions.
+ * The class derives auth information when first instantiated.
+ */
+ public static FirebaseChannel getInstance() {
+ if (instance == null) {
+ instance = new FirebaseChannel();
+ }
+ return instance;
+ }
+
+ /**
+ * Construct the singleton, with derived auth information. The Firebase database url is derived
+ * from the snippet that we provide to the client code, to guarantee that the client and the
+ * server are communicating with the same Firebase database. The auth credentials we'll use to
+ * communicate with Firebase is derived from App Engine's default credentials, and given
+ * Firebase's OAuth scopes.
+ */
+ private FirebaseChannel() {
+ try {
+ // This variables exist primarily so it can be stubbed out in unit tests.
+ if (null == firebaseConfigStream) {
+ firebaseConfigStream = new FileInputStream(FIREBASE_SNIPPET_PATH);
+ }
+
+ String firebaseSnippet = CharStreams.toString(new InputStreamReader(
+ firebaseConfigStream, StandardCharsets.UTF_8));
+ firebaseDbUrl = parseFirebaseUrl(firebaseSnippet);
+
+ credential = GoogleCredential.getApplicationDefault().createScoped(FIREBASE_SCOPES);
+ httpTransport = UrlFetchTransport.getDefaultInstance();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Parses out the Firebase database url from the client-side code snippet.
+ * The code snippet is a piece of javascript that defines an object with the key 'databaseURL'. So
+ * look for that key, then parse out its quote-surrounded value.
+ */
+ private static String parseFirebaseUrl(String firebaseSnippet) {
+ int idx = firebaseSnippet.indexOf("databaseURL");
+ if (-1 == idx) {
+ throw new RuntimeException(
+ "Please copy your Firebase web snippet into " + FIREBASE_SNIPPET_PATH);
+ }
+ idx = firebaseSnippet.indexOf(':', idx);
+ int openQuote = firebaseSnippet.indexOf('"', idx);
+ int closeQuote = firebaseSnippet.indexOf('"', openQuote + 1);
+ return firebaseSnippet.substring(openQuote + 1, closeQuote);
+ }
+
+ public void sendFirebaseMessage(String channelKey, Game game)
+ throws IOException {
+ // Make requests auth'ed using Application Default Credentials
+ HttpRequestFactory requestFactory = httpTransport.createRequestFactory(credential);
+ GenericUrl url = new GenericUrl(
+ String.format("%s/channels/%s.json", firebaseDbUrl, channelKey));
+ HttpResponse response = null;
+
+ try {
+ if (null == game) {
+ response = requestFactory.buildDeleteRequest(url).execute();
+ } else {
+ String gameJson = new Gson().toJson(game);
+ response = requestFactory.buildPatchRequest(
+ url, new ByteArrayContent("application/json", gameJson.getBytes())).execute();
+ }
+
+ if (response.getStatusCode() != 200) {
+ throw new RuntimeException(
+ "Error code while updating Firebase: " + response.getStatusCode());
+ }
+
+ } finally {
+ if (null != response) {
+ response.disconnect();
+ }
+ }
+ }
+
+ /**
+ * Create a secure JWT token for the given userId.
+ */
+ public String createFirebaseToken(Game game, String userId) {
+ final AppIdentityService appIdentity = AppIdentityServiceFactory.getAppIdentityService();
+ final BaseEncoding base64 = BaseEncoding.base64();
+
+ String header = base64.encode("{\"typ\":\"JWT\",\"alg\":\"RS256\"}".getBytes());
+
+ // Construct the claim
+ String channelKey = game.getChannelKey(userId);
+ String clientEmail = appIdentity.getServiceAccountName();
+ long epochTime = System.currentTimeMillis() / 1000;
+ long expire = epochTime + 60 * 60; // an hour from now
+
+ Map claims = new HashMap();
+ claims.put("iss", clientEmail);
+ claims.put("sub", clientEmail);
+ claims.put("aud", IDENTITY_ENDPOINT);
+ claims.put("uid", channelKey);
+ claims.put("iat", epochTime);
+ claims.put("exp", expire);
+
+ String payload = base64.encode(new Gson().toJson(claims).getBytes());
+ String toSign = String.format("%s.%s", header, payload);
+ AppIdentityService.SigningResult result = appIdentity.signForApp(toSign.getBytes());
+ return String.format("%s.%s", toSign, base64.encode(result.getSignature()));
+ }
+
+ // The following methods are to illustrate making various calls to Firebase from App Engine
+ // Standard
+
+ public HttpResponse firebasePut(String path, Object object) throws IOException {
+ // Make requests auth'ed using Application Default Credentials
+ Credential credential = GoogleCredential.getApplicationDefault().createScoped(FIREBASE_SCOPES);
+ HttpRequestFactory requestFactory = httpTransport.createRequestFactory(credential);
+
+ String json = new Gson().toJson(object);
+ GenericUrl url = new GenericUrl(path);
+
+ return requestFactory.buildPutRequest(
+ url, new ByteArrayContent("application/json", json.getBytes())).execute();
+ }
+
+ public HttpResponse firebasePatch(String path, Object object) throws IOException {
+ // Make requests auth'ed using Application Default Credentials
+ Credential credential = GoogleCredential.getApplicationDefault().createScoped(FIREBASE_SCOPES);
+ HttpRequestFactory requestFactory = httpTransport.createRequestFactory(credential);
+
+ String json = new Gson().toJson(object);
+ GenericUrl url = new GenericUrl(path);
+
+ return requestFactory.buildPatchRequest(
+ url, new ByteArrayContent("application/json", json.getBytes())).execute();
+ }
+
+ public HttpResponse firebasePost(String path, Object object) throws IOException {
+ // Make requests auth'ed using Application Default Credentials
+ Credential credential = GoogleCredential.getApplicationDefault().createScoped(FIREBASE_SCOPES);
+ HttpRequestFactory requestFactory = httpTransport.createRequestFactory(credential);
+
+ String json = new Gson().toJson(object);
+ GenericUrl url = new GenericUrl(path);
+
+ return requestFactory.buildPostRequest(
+ url, new ByteArrayContent("application/json", json.getBytes())).execute();
+ }
+
+ public HttpResponse firebaseGet(String path) throws IOException {
+ // Make requests auth'ed using Application Default Credentials
+ Credential credential = GoogleCredential.getApplicationDefault().createScoped(FIREBASE_SCOPES);
+ HttpRequestFactory requestFactory = httpTransport.createRequestFactory(credential);
+
+ GenericUrl url = new GenericUrl(path);
+
+ return requestFactory.buildGetRequest(url).execute();
+ }
+
+ public HttpResponse firebaseDelete(String path) throws IOException {
+ // Make requests auth'ed using Application Default Credentials
+ Credential credential = GoogleCredential.getApplicationDefault().createScoped(FIREBASE_SCOPES);
+ HttpRequestFactory requestFactory = httpTransport.createRequestFactory(credential);
+
+ GenericUrl url = new GenericUrl(path);
+
+ return requestFactory.buildDeleteRequest(url).execute();
+ }
+}
diff --git a/appengine-java8/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/Game.java b/appengine-java8/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/Game.java
new file mode 100644
index 00000000000..7f6a1e84135
--- /dev/null
+++ b/appengine-java8/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/Game.java
@@ -0,0 +1,183 @@
+/*
+ * Copyright 2016 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.firetactoe;
+
+import com.googlecode.objectify.annotation.Entity;
+import com.googlecode.objectify.annotation.Id;
+
+import java.io.IOException;
+import java.util.UUID;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.regex.Pattern;
+
+/**
+ * The datastore-persisted Game object. This holds the entire game state - from a representation of
+ * the board, to the players are and whose turn it is, and who the winner is and how they won.
+ *
+ * It also contains some convenience functions for communicating updates to the board to the
+ * clients, via Firebase.
+ */
+@Entity
+public class Game {
+ static final Pattern[] XWins =
+ {Pattern.compile("XXX......"), Pattern.compile("...XXX..."), Pattern.compile("......XXX"),
+ Pattern.compile("X..X..X.."), Pattern.compile(".X..X..X."),
+ Pattern.compile("..X..X..X"), Pattern.compile("X...X...X"),
+ Pattern.compile("..X.X.X..")};
+ static final Pattern[] OWins =
+ {Pattern.compile("OOO......"), Pattern.compile("...OOO..."), Pattern.compile("......OOO"),
+ Pattern.compile("O..O..O.."), Pattern.compile(".O..O..O."),
+ Pattern.compile("..O..O..O"), Pattern.compile("O...O...O"),
+ Pattern.compile("..O.O.O..")};
+
+ @Id
+ public String id;
+ public String userX;
+ public String userO;
+ public String board;
+ public Boolean moveX;
+ public String winner;
+ public String winningBoard;
+
+ private static final Logger LOGGER = Logger.getLogger(Game.class.getName());
+
+ Game() {
+ this.id = UUID.randomUUID().toString();
+ }
+
+ Game(String userX, String userO, String board, boolean moveX) {
+ this.id = UUID.randomUUID().toString();
+ this.userX = userX;
+ this.userO = userO;
+ this.board = board;
+ this.moveX = moveX;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getUserX() {
+ return userX;
+ }
+
+ public String getUserO() {
+ return userO;
+ }
+
+ public void setUserO(String userO) {
+ this.userO = userO;
+ }
+
+ public String getBoard() {
+ return board;
+ }
+
+ public void setBoard(String board) {
+ this.board = board;
+ }
+
+ public boolean getMoveX() {
+ return moveX;
+ }
+
+ public void setMoveX(boolean moveX) {
+ this.moveX = moveX;
+ }
+
+ // [START send_updates]
+ public String getChannelKey(String userId) {
+ return userId + id;
+ }
+
+ public void deleteChannel(String userId)
+ throws IOException {
+ if (userId != null) {
+ String channelKey = getChannelKey(userId);
+ FirebaseChannel.getInstance().sendFirebaseMessage(channelKey, null);
+ }
+ }
+
+ private void sendUpdateToUser(String userId)
+ throws IOException {
+ if (userId != null) {
+ String channelKey = getChannelKey(userId);
+ FirebaseChannel.getInstance().sendFirebaseMessage(channelKey, this);
+ }
+ }
+
+ public void sendUpdateToClients()
+ throws IOException {
+ sendUpdateToUser(userX);
+ sendUpdateToUser(userO);
+ }
+ // [END send_updates]
+
+ public void checkWin() {
+ final Pattern[] wins;
+ if (moveX) {
+ wins = XWins;
+ } else {
+ wins = OWins;
+ }
+
+ for (Pattern winPattern : wins) {
+ if (winPattern.matcher(board).matches()) {
+ if (moveX) {
+ winner = userX;
+ } else {
+ winner = userO;
+ }
+ winningBoard = winPattern.toString();
+ }
+ }
+ }
+
+ public boolean makeMove(int position, String userId) {
+ String currentMovePlayer;
+ char value;
+ if (getMoveX()) {
+ value = 'X';
+ currentMovePlayer = getUserX();
+ } else {
+ value = 'O';
+ currentMovePlayer = getUserO();
+ }
+
+ if (currentMovePlayer.equals(userId)) {
+ char[] boardBytes = getBoard().toCharArray();
+ boardBytes[position] = value;
+ setBoard(new String(boardBytes));
+ checkWin();
+ setMoveX(!getMoveX());
+ try {
+ sendUpdateToClients();
+ } catch (IOException e) {
+ LOGGER.log(Level.SEVERE, "Error sending Game update to Firebase", e);
+ throw new RuntimeException(e);
+ }
+ return true;
+ }
+
+ return false;
+ }
+}
diff --git a/appengine-java8/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/MoveServlet.java b/appengine-java8/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/MoveServlet.java
new file mode 100644
index 00000000000..4911d22bc37
--- /dev/null
+++ b/appengine-java8/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/MoveServlet.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2016 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.firetactoe;
+
+import com.google.appengine.api.users.UserService;
+import com.google.appengine.api.users.UserServiceFactory;
+import com.googlecode.objectify.Objectify;
+import com.googlecode.objectify.ObjectifyService;
+
+import java.io.IOException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Handler for a user making a move in a game.
+ * Updates the game board with the requested move (if it's legal), and communicate the updated board
+ * to the clients.
+ */
+public class MoveServlet extends HttpServlet {
+ @Override
+ public void doPost(HttpServletRequest request, HttpServletResponse response)
+ throws IOException {
+ String gameId = request.getParameter("gameKey");
+ Objectify ofy = ObjectifyService.ofy();
+ Game game = ofy.load().type(Game.class).id(gameId).safe();
+
+ UserService userService = UserServiceFactory.getUserService();
+ String currentUserId = userService.getCurrentUser().getUserId();
+
+ int cell = new Integer(request.getParameter("cell"));
+ if (!game.makeMove(cell, currentUserId)) {
+ response.sendError(HttpServletResponse.SC_FORBIDDEN);
+ } else {
+ ofy.save().entity(game).now();
+ }
+ }
+}
diff --git a/appengine-java8/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/ObjectifyHelper.java b/appengine-java8/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/ObjectifyHelper.java
new file mode 100644
index 00000000000..abc1a28e33c
--- /dev/null
+++ b/appengine-java8/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/ObjectifyHelper.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2016 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.firetactoe;
+
+import com.googlecode.objectify.ObjectifyService;
+
+import javax.servlet.ServletContextEvent;
+import javax.servlet.ServletContextListener;
+
+/**
+ * ObjectifyHelper, a ServletContextListener, is setup in web.xml to run before a JSP is run. This
+ * is required to let JSP's access Ofy.
+ **/
+public class ObjectifyHelper implements ServletContextListener {
+ public void contextInitialized(ServletContextEvent event) {
+ // This will be invoked as part of a warmup request, or the first user request if no warmup
+ // request.
+ ObjectifyService.register(Game.class);
+ }
+
+ public void contextDestroyed(ServletContextEvent event) {
+ // App Engine does not currently invoke this method.
+ }
+}
diff --git a/appengine-java8/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/OpenedServlet.java b/appengine-java8/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/OpenedServlet.java
new file mode 100644
index 00000000000..2e71f9391cb
--- /dev/null
+++ b/appengine-java8/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/OpenedServlet.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2016 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.firetactoe;
+
+import com.googlecode.objectify.Objectify;
+import com.googlecode.objectify.ObjectifyService;
+
+import java.io.IOException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Handler that signals to all players of a game that the game has started.
+ */
+public class OpenedServlet extends HttpServlet {
+ @Override
+ public void doPost(HttpServletRequest request, HttpServletResponse response)
+ throws IOException {
+ // TODO(you): In practice, you should validate the user has permission to post to the given Game
+ String gameId = request.getParameter("gameKey");
+ Objectify ofy = ObjectifyService.ofy();
+ Game game = ofy.load().type(Game.class).id(gameId).safe();
+ game.sendUpdateToClients();
+ }
+}
diff --git a/appengine-java8/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/TicTacToeServlet.java b/appengine-java8/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/TicTacToeServlet.java
new file mode 100644
index 00000000000..8b248577971
--- /dev/null
+++ b/appengine-java8/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/TicTacToeServlet.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2016 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.firetactoe;
+
+import com.google.appengine.api.users.UserServiceFactory;
+import com.google.gson.Gson;
+import com.googlecode.objectify.Objectify;
+import com.googlecode.objectify.ObjectifyService;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Base handler for the Tic Tac Toe game.
+ * This handler serves up the initial jsp page that is the game, and also creates the persistent
+ * game in the datastore, as well as the Firebase database to serve as the communication channel to
+ * the clients.
+ */
+@SuppressWarnings("serial")
+public class TicTacToeServlet extends HttpServlet {
+
+ private String getGameUriWithGameParam(HttpServletRequest request, String gameKey) {
+ try {
+ String query = "";
+ if (gameKey != null) {
+ query = "gameKey=" + gameKey;
+ }
+ URI thisUri = new URI(request.getRequestURL().toString());
+ URI uriWithOptionalGameParam = new URI(
+ thisUri.getScheme(), thisUri.getUserInfo(), thisUri.getHost(),
+ thisUri.getPort(), thisUri.getPath(), query, "");
+ return uriWithOptionalGameParam.toString();
+ } catch (URISyntaxException e) {
+ // This should never happen, since we're constructing the URI from a valid URI.
+ // Nonetheless, wrap it in a RuntimeException to placate java.
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public void doGet(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, IOException {
+ String gameKey = request.getParameter("gameKey");
+
+ // 1. Create or fetch a Game object from the datastore
+ Objectify ofy = ObjectifyService.ofy();
+ Game game = null;
+ String userId = UserServiceFactory.getUserService().getCurrentUser().getUserId();
+ if (gameKey != null) {
+ game = ofy.load().type(Game.class).id(gameKey).now();
+ if (null == game) {
+ response.sendError(HttpServletResponse.SC_NOT_FOUND);
+ return;
+ }
+ if (game.getUserO() == null && !userId.equals(game.getUserX())) {
+ game.setUserO(userId);
+ }
+ ofy.save().entity(game).now();
+ } else {
+ // Initialize a new board. The board is represented as a String of 9 spaces, one for each
+ // blank spot on the tic-tac-toe board.
+ game = new Game(userId, null, " ", true);
+ ofy.save().entity(game).now();
+ gameKey = game.getId();
+ }
+
+ // 2. Create this Game in the firebase db
+ game.sendUpdateToClients();
+
+ // 3. Inject a secure token into the client, so it can get game updates
+
+ // [START pass_token]
+ // The 'Game' object exposes a method which creates a unique string based on the game's key
+ // and the user's id.
+ String token = FirebaseChannel.getInstance().createFirebaseToken(game, userId);
+ request.setAttribute("token", token);
+
+ // 4. More general template values
+ request.setAttribute("game_key", gameKey);
+ request.setAttribute("me", userId);
+ request.setAttribute("channel_id", game.getChannelKey(userId));
+ request.setAttribute("initial_message", new Gson().toJson(game));
+ request.setAttribute("game_link", getGameUriWithGameParam(request, gameKey));
+ request.getRequestDispatcher("/WEB-INF/view/index.jsp").forward(request, response);
+ // [END pass_token]
+ }
+}
diff --git a/appengine-java8/firebase-tictactoe/src/main/webapp/WEB-INF/appengine-web.xml b/appengine-java8/firebase-tictactoe/src/main/webapp/WEB-INF/appengine-web.xml
new file mode 100644
index 00000000000..4697140720f
--- /dev/null
+++ b/appengine-java8/firebase-tictactoe/src/main/webapp/WEB-INF/appengine-web.xml
@@ -0,0 +1,34 @@
+
+
+
+ java8
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/appengine-java8/firebase-tictactoe/src/main/webapp/WEB-INF/logging.properties b/appengine-java8/firebase-tictactoe/src/main/webapp/WEB-INF/logging.properties
new file mode 100644
index 00000000000..9b29028efa7
--- /dev/null
+++ b/appengine-java8/firebase-tictactoe/src/main/webapp/WEB-INF/logging.properties
@@ -0,0 +1,27 @@
+#
+# Copyright 2016 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.
+#
+# A default java.util.logging configuration.
+# (All App Engine logging is through java.util.logging by default).
+#
+# To use this configuration, copy it into your application's WEB-INF
+# folder and add the following to your appengine-web.xml:
+#
+#
+#
+#
+#
+# Set the default logging level for all loggers to WARNING
+.level=WARNING
diff --git a/appengine-java8/firebase-tictactoe/src/main/webapp/WEB-INF/view/firebase_config.jspf b/appengine-java8/firebase-tictactoe/src/main/webapp/WEB-INF/view/firebase_config.jspf
new file mode 100644
index 00000000000..25898c985af
--- /dev/null
+++ b/appengine-java8/firebase-tictactoe/src/main/webapp/WEB-INF/view/firebase_config.jspf
@@ -0,0 +1,3 @@
+REPLACE ME WITH YOUR FIREBASE WEBAPP CODE SNIPPET:
+
+https://console.firebase.google.com/project/_/overview
diff --git a/appengine-java8/firebase-tictactoe/src/main/webapp/WEB-INF/view/index.jsp b/appengine-java8/firebase-tictactoe/src/main/webapp/WEB-INF/view/index.jsp
new file mode 100644
index 00000000000..fab4ac6f0ec
--- /dev/null
+++ b/appengine-java8/firebase-tictactoe/src/main/webapp/WEB-INF/view/index.jsp
@@ -0,0 +1,59 @@
+<%@ page contentType="text/html;charset=UTF-8" language="java" %>
+<%--
+ Copyright 2016 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.
+--%>
+
+
+
+
+ <%@ include file="firebase_config.jspf" %>
+
+
+
+
+
+
+
+
Firebase-enabled Tic Tac Toe
+
+ Waiting for another player to join.
+ Send them this link to play:
+
+
+
diff --git a/appengine-java8/firebase-tictactoe/src/main/webapp/WEB-INF/web.xml b/appengine-java8/firebase-tictactoe/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 00000000000..772f32fc601
--- /dev/null
+++ b/appengine-java8/firebase-tictactoe/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,77 @@
+
+
+
+
+ index
+
+
+
+ entire-app
+ /*
+
+
+ *
+
+
+
+ TicTacToeServlet
+ com.example.appengine.firetactoe.TicTacToeServlet
+
+
+ TicTacToeServlet
+ /index
+
+
+ OpenedServlet
+ com.example.appengine.firetactoe.OpenedServlet
+
+
+ OpenedServlet
+ /opened
+
+
+ MoveServlet
+ com.example.appengine.firetactoe.MoveServlet
+
+
+ MoveServlet
+ /move
+
+
+ DeleteServlet
+ com.example.appengine.firetactoe.DeleteServlet
+
+
+ DeleteServlet
+ /delete
+
+
+
+ ObjectifyFilter
+ com.googlecode.objectify.ObjectifyFilter
+
+
+ ObjectifyFilter
+ /*
+
+
+ com.example.appengine.firetactoe.ObjectifyHelper
+
+
diff --git a/appengine-java8/firebase-tictactoe/src/main/webapp/static/main.css b/appengine-java8/firebase-tictactoe/src/main/webapp/static/main.css
new file mode 100644
index 00000000000..f314eab5b37
--- /dev/null
+++ b/appengine-java8/firebase-tictactoe/src/main/webapp/static/main.css
@@ -0,0 +1,82 @@
+body {
+ font-family: 'Helvetica';
+}
+
+#board {
+ width:152px;
+ height: 152px;
+ margin: 20px auto;
+}
+
+#display-area {
+ text-align: center;
+}
+
+#other-player, #your-move, #their-move, #you-won, #you-lost {
+ display: none;
+}
+
+#display-area.waiting #other-player {
+ display: block;
+}
+
+#display-area.waiting #board, #display-area.waiting #this-game {
+ display: none;
+}
+#display-area.won #you-won {
+ display: block;
+}
+#display-area.lost #you-lost {
+ display: block;
+}
+#display-area.your-move #your-move {
+ display: block;
+}
+#display-area.their-move #their-move {
+ display: block;
+}
+
+
+#this-game {
+ font-size: 9pt;
+}
+
+div.cell {
+ float: left;
+ width: 50px;
+ height: 50px;
+ border: none;
+ margin: 0px;
+ padding: 0px;
+ box-sizing: border-box;
+
+ line-height: 50px;
+ font-family: "Helvetica";
+ font-size: 16pt;
+ text-align: center;
+}
+
+.your-move div.cell:hover {
+ background: lightgrey;
+}
+
+.your-move div.cell:empty:hover {
+ background: lightblue;
+ cursor: pointer;
+}
+
+div.l {
+ border-right: 1pt solid black;
+}
+
+div.r {
+ border-left: 1pt solid black;
+}
+
+div.t {
+ border-bottom: 1pt solid black;
+}
+
+div.b {
+ border-top: 1pt solid black;
+}
diff --git a/appengine-java8/firebase-tictactoe/src/main/webapp/static/main.js b/appengine-java8/firebase-tictactoe/src/main/webapp/static/main.js
new file mode 100644
index 00000000000..6ae24b0247d
--- /dev/null
+++ b/appengine-java8/firebase-tictactoe/src/main/webapp/static/main.js
@@ -0,0 +1,178 @@
+/**
+ * Copyright 2016 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.
+ */
+
+'use strict';
+
+/**
+ * @fileoverview Tic-Tac-Toe, using the Firebase API
+ */
+
+/**
+ * @param gameKey - a unique key for this game.
+ * @param me - my user id.
+ * @param token - secure token passed from the server
+ * @param channelId - id of the 'channel' we'll be listening to
+ */
+function initGame(gameKey, me, token, channelId, initialMessage) {
+ var state = {
+ gameKey: gameKey,
+ me: me
+ };
+
+ // This is our Firebase realtime DB path that we'll listen to for updates
+ // We'll initialize this later in openChannel()
+ var channel = null;
+
+ /**
+ * Updates the displayed game board.
+ */
+ function updateGame(newState) {
+ $.extend(state, newState);
+
+ $('.cell').each(function(i) {
+ var square = $(this);
+ var value = state.board[i];
+ square.html(' ' === value ? '' : value);
+
+ if (state.winner && state.winningBoard) {
+ if (state.winningBoard[i] === value) {
+ if (state.winner === state.me) {
+ square.css('background', 'green');
+ } else {
+ square.css('background', 'red');
+ }
+ } else {
+ square.css('background', '');
+ }
+ }
+ });
+
+ var displayArea = $('#display-area');
+
+ if (!state.userO) {
+ displayArea[0].className = 'waiting';
+ } else if (state.winner === state.me) {
+ displayArea[0].className = 'won';
+ } else if (state.winner) {
+ displayArea[0].className = 'lost';
+ } else if (isMyMove()) {
+ displayArea[0].className = 'your-move';
+ } else {
+ displayArea[0].className = 'their-move';
+ }
+ }
+
+ function isMyMove() {
+ return !state.winner && (state.moveX === (state.userX === state.me));
+ }
+
+ function myPiece() {
+ return state.userX === state.me ? 'X' : 'O';
+ }
+
+ /**
+ * Send the user's latest move back to the server
+ */
+ function moveInSquare(e) {
+ var id = $(e.currentTarget).index();
+ if (isMyMove() && state.board[id] === ' ') {
+ $.post('/move', {cell: id});
+ }
+ }
+
+ /**
+ * This method lets the server know that the user has opened the channel
+ * After this method is called, the server may begin to send updates
+ */
+ function onOpened() {
+ $.post('/opened');
+ }
+
+ /**
+ * This deletes the data associated with the Firebase path
+ * it is critical that this data be deleted since it costs money
+ */
+ function deleteChannel() {
+ $.post('/delete');
+ }
+
+ /**
+ * This method is called every time an event is fired from Firebase
+ * it updates the entire game state and checks for a winner
+ * if a player has won the game, this function calls the server to delete
+ * the data stored in Firebase
+ */
+ function onMessage(newState) {
+ updateGame(newState);
+
+ // now check to see if there is a winner
+ if (channel && state.winner && state.winningBoard) {
+ channel.off(); //stop listening on this path
+ deleteChannel(); //delete the data we wrote
+ }
+ }
+
+ /**
+ * This function opens a realtime communication channel with Firebase
+ * It logs in securely using the client token passed from the server
+ * then it sets up a listener on the proper database path (also passed by server)
+ * finally, it calls onOpened() to let the server know it is ready to receive messages
+ */
+ function openChannel() {
+ // [START auth_login]
+ // sign into Firebase with the token passed from the server
+ firebase.auth().signInWithCustomToken(token).catch(function(error) {
+ console.log('Login Failed!', error.code);
+ console.log('Error message: ', error.message);
+ });
+ // [END auth_login]
+
+ // [START add_listener]
+ // setup a database reference at path /channels/channelId
+ channel = firebase.database().ref('channels/' + channelId);
+ // add a listener to the path that fires any time the value of the data changes
+ channel.on('value', function(data) {
+ onMessage(data.val());
+ });
+ // [END add_listener]
+ onOpened();
+ // let the server know that the channel is open
+ }
+
+ /**
+ * This function opens a communication channel with the server
+ * then it adds listeners to all the squares on the board
+ * next it pulls down the initial game state from template values
+ * finally it updates the game state with those values by calling onMessage()
+ */
+ function initialize() {
+ // Always include the gamekey in our requests
+ $.ajaxPrefilter(function(opts) {
+ if (opts.url.indexOf('?') > 0)
+ opts.url += '&gameKey=' + state.gameKey;
+ else
+ opts.url += '?gameKey=' + state.gameKey;
+ });
+
+ $('#board').on('click', '.cell', moveInSquare);
+
+ openChannel();
+
+ onMessage(initialMessage);
+ }
+
+ setTimeout(initialize, 100);
+}
diff --git a/appengine-java8/firebase-tictactoe/src/test/java/com/example/appengine/firetactoe/DeleteServletTest.java b/appengine-java8/firebase-tictactoe/src/test/java/com/example/appengine/firetactoe/DeleteServletTest.java
new file mode 100644
index 00000000000..0df68aaaefc
--- /dev/null
+++ b/appengine-java8/firebase-tictactoe/src/test/java/com/example/appengine/firetactoe/DeleteServletTest.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright 2016 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.firetactoe;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.google.api.client.http.LowLevelHttpRequest;
+import com.google.api.client.http.LowLevelHttpResponse;
+import com.google.api.client.testing.http.MockHttpTransport;
+import com.google.api.client.testing.http.MockLowLevelHttpRequest;
+import com.google.api.client.testing.http.MockLowLevelHttpResponse;
+import com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig;
+import com.google.appengine.tools.development.testing.LocalServiceTestHelper;
+import com.google.appengine.tools.development.testing.LocalURLFetchServiceTestConfig;
+import com.google.appengine.tools.development.testing.LocalUserServiceTestConfig;
+import com.google.common.collect.ImmutableMap;
+import com.googlecode.objectify.Objectify;
+import com.googlecode.objectify.ObjectifyFactory;
+import com.googlecode.objectify.ObjectifyService;
+import com.googlecode.objectify.util.Closeable;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Matchers;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.util.HashMap;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Unit tests for {@link DeleteServlet}.
+ */
+@RunWith(JUnit4.class)
+public class DeleteServletTest {
+ private static final String USER_EMAIL = "whisky@tangofoxtr.ot";
+ private static final String USER_ID = "whiskytangofoxtrot";
+ private static final String FIREBASE_DB_URL = "http://firebase.com/dburl";
+
+ private final LocalServiceTestHelper helper =
+ new LocalServiceTestHelper(
+ // Set no eventual consistency, that way queries return all results.
+ // http://g.co/cloud/appengine/docs/java/tools/localunittesting#Java_Writing_High_Replication_Datastore_tests
+ new LocalDatastoreServiceTestConfig().setDefaultHighRepJobPolicyUnappliedJobPercentage(0),
+ new LocalUserServiceTestConfig(),
+ new LocalURLFetchServiceTestConfig()
+ )
+ .setEnvEmail(USER_EMAIL)
+ .setEnvAuthDomain("gmail.com")
+ .setEnvAttributes(new HashMap(
+ ImmutableMap.of("com.google.appengine.api.users.UserService.user_id_key", USER_ID)));
+
+ @Mock private HttpServletRequest mockRequest;
+ @Mock private HttpServletResponse mockResponse;
+ protected Closeable dbSession;
+
+ private DeleteServlet servletUnderTest;
+
+ @BeforeClass
+ public static void setUpBeforeClass() {
+ // Reset the Factory so that all translators work properly.
+ ObjectifyService.setFactory(new ObjectifyFactory());
+ ObjectifyService.register(Game.class);
+ // Mock out the firebase config
+ FirebaseChannel.firebaseConfigStream = new ByteArrayInputStream(
+ String.format("databaseURL: \"%s\"", FIREBASE_DB_URL).getBytes());
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ helper.setUp();
+ dbSession = ObjectifyService.begin();
+
+ servletUnderTest = new DeleteServlet();
+
+ helper.setEnvIsLoggedIn(true);
+ // Make sure there are no firebase requests if we don't expect it
+ FirebaseChannel.getInstance().httpTransport = null;
+ }
+
+ @After
+ public void tearDown() {
+ dbSession.close();
+ helper.tearDown();
+ }
+
+ @Test
+ public void doPost_noGameKey() throws Exception {
+ try {
+ servletUnderTest.doPost(mockRequest, mockResponse);
+ fail("Should not succeed with no gameKey specified.");
+ } catch (IllegalArgumentException e) {
+ assertThat(e.getMessage()).startsWith("id 'null'");
+ }
+ }
+
+ @Test
+ public void doPost_deleteGame() throws Exception {
+ // Insert a game
+ Objectify ofy = ObjectifyService.ofy();
+ Game game = new Game(USER_ID, "my-opponent", " ", true);
+ ofy.save().entity(game).now();
+ String gameKey = game.getId();
+ when(mockRequest.getParameter("gameKey")).thenReturn(gameKey);
+
+ // Mock out the firebase response. See
+ // http://g.co/dv/api-client-library/java/google-http-java-client/unit-testing
+ MockHttpTransport mockHttpTransport = spy(new MockHttpTransport() {
+ @Override
+ public LowLevelHttpRequest buildRequest(String method, String url) throws IOException {
+ return new MockLowLevelHttpRequest() {
+ @Override
+ public LowLevelHttpResponse execute() throws IOException {
+ MockLowLevelHttpResponse response = new MockLowLevelHttpResponse();
+ response.setStatusCode(200);
+ return response;
+ }
+ };
+ }
+ });
+ FirebaseChannel.getInstance().httpTransport = mockHttpTransport;
+
+ servletUnderTest.doPost(mockRequest, mockResponse);
+
+ verify(mockHttpTransport, times(1)).buildRequest(
+ eq("DELETE"), Matchers.matches(FIREBASE_DB_URL + "/channels/[\\w-]+.json$"));
+ }
+}
diff --git a/appengine-java8/firebase-tictactoe/src/test/java/com/example/appengine/firetactoe/FirebaseChannelTest.java b/appengine-java8/firebase-tictactoe/src/test/java/com/example/appengine/firetactoe/FirebaseChannelTest.java
new file mode 100644
index 00000000000..0c92863f77e
--- /dev/null
+++ b/appengine-java8/firebase-tictactoe/src/test/java/com/example/appengine/firetactoe/FirebaseChannelTest.java
@@ -0,0 +1,254 @@
+/*
+ * Copyright 2016 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.firetactoe;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import com.google.api.client.http.LowLevelHttpRequest;
+import com.google.api.client.http.LowLevelHttpResponse;
+import com.google.api.client.testing.http.MockHttpTransport;
+import com.google.api.client.testing.http.MockLowLevelHttpRequest;
+import com.google.api.client.testing.http.MockLowLevelHttpResponse;
+import com.google.appengine.tools.development.testing.LocalAppIdentityServiceTestConfig;
+import com.google.appengine.tools.development.testing.LocalServiceTestHelper;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.MockitoAnnotations;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+
+/**
+ * Unit tests for {@link FirebaseChannel}.
+ */
+@RunWith(JUnit4.class)
+public class FirebaseChannelTest {
+ private static final String FIREBASE_DB_URL = "http://firebase.com/dburl";
+ private final LocalServiceTestHelper helper =
+ new LocalServiceTestHelper(new LocalAppIdentityServiceTestConfig());
+
+ private static FirebaseChannel firebaseChannel;
+
+ @BeforeClass
+ public static void setUpBeforeClass() {
+ // Mock out the firebase config
+ FirebaseChannel.firebaseConfigStream = new ByteArrayInputStream(
+ String.format("databaseURL: \"%s\"", FIREBASE_DB_URL).getBytes());
+
+ firebaseChannel = FirebaseChannel.getInstance();
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ helper.setUp();
+ }
+
+ @After
+ public void tearDown() {
+ helper.tearDown();
+ }
+
+ @Test
+ public void sendFirebaseMessage_create() throws Exception {
+ // Mock out the firebase response. See
+ // http://g.co/dv/api-client-library/java/google-http-java-client/unit-testing
+ MockHttpTransport mockHttpTransport = spy(new MockHttpTransport() {
+ @Override
+ public LowLevelHttpRequest buildRequest(String method, String url) throws IOException {
+ return new MockLowLevelHttpRequest() {
+ @Override
+ public LowLevelHttpResponse execute() throws IOException {
+ MockLowLevelHttpResponse response = new MockLowLevelHttpResponse();
+ response.setStatusCode(200);
+ return response;
+ }
+ };
+ }
+ });
+ FirebaseChannel.getInstance().httpTransport = mockHttpTransport;
+
+ firebaseChannel.sendFirebaseMessage("my_key", new Game());
+
+ verify(mockHttpTransport, times(1)).buildRequest(
+ "PATCH", FIREBASE_DB_URL + "/channels/my_key.json");
+ }
+
+ @Test
+ public void sendFirebaseMessage_delete() throws Exception {
+ // Mock out the firebase response. See
+ // http://g.co/dv/api-client-library/java/google-http-java-client/unit-testing
+ MockHttpTransport mockHttpTransport = spy(new MockHttpTransport() {
+ @Override
+ public LowLevelHttpRequest buildRequest(String method, String url) throws IOException {
+ return new MockLowLevelHttpRequest() {
+ @Override
+ public LowLevelHttpResponse execute() throws IOException {
+ MockLowLevelHttpResponse response = new MockLowLevelHttpResponse();
+ response.setStatusCode(200);
+ return response;
+ }
+ };
+ }
+ });
+ FirebaseChannel.getInstance().httpTransport = mockHttpTransport;
+
+ firebaseChannel.sendFirebaseMessage("my_key", null);
+
+ verify(mockHttpTransport, times(1)).buildRequest(
+ "DELETE", FIREBASE_DB_URL + "/channels/my_key.json");
+ }
+
+ @Test
+ public void createFirebaseToken() throws Exception {
+ Game game = new Game();
+
+ String jwt = firebaseChannel.createFirebaseToken(game, "userId");
+
+ assertThat(jwt).matches("^([\\w+/=-]+\\.){2}[\\w+/=-]+$");
+ }
+
+ @Test
+ public void firebasePut() throws Exception {
+ // Mock out the firebase response. See
+ // http://g.co/dv/api-client-library/java/google-http-java-client/unit-testing
+ MockHttpTransport mockHttpTransport = spy(new MockHttpTransport() {
+ @Override
+ public LowLevelHttpRequest buildRequest(String method, String url) throws IOException {
+ return new MockLowLevelHttpRequest() {
+ @Override
+ public LowLevelHttpResponse execute() throws IOException {
+ MockLowLevelHttpResponse response = new MockLowLevelHttpResponse();
+ response.setStatusCode(200);
+ return response;
+ }
+ };
+ }
+ });
+ FirebaseChannel.getInstance().httpTransport = mockHttpTransport;
+ Game game = new Game();
+
+ firebaseChannel.firebasePut(FIREBASE_DB_URL + "/my/path", game);
+
+ verify(mockHttpTransport, times(1)).buildRequest("PUT", FIREBASE_DB_URL + "/my/path");
+ }
+
+ @Test
+ public void firebasePatch() throws Exception {
+ // Mock out the firebase response. See
+ // http://g.co/dv/api-client-library/java/google-http-java-client/unit-testing
+ MockHttpTransport mockHttpTransport = spy(new MockHttpTransport() {
+ @Override
+ public LowLevelHttpRequest buildRequest(String method, String url) throws IOException {
+ return new MockLowLevelHttpRequest() {
+ @Override
+ public LowLevelHttpResponse execute() throws IOException {
+ MockLowLevelHttpResponse response = new MockLowLevelHttpResponse();
+ response.setStatusCode(200);
+ return response;
+ }
+ };
+ }
+ });
+ FirebaseChannel.getInstance().httpTransport = mockHttpTransport;
+ Game game = new Game();
+
+ firebaseChannel.firebasePatch(FIREBASE_DB_URL + "/my/path", game);
+
+ verify(mockHttpTransport, times(1)).buildRequest("PATCH", FIREBASE_DB_URL + "/my/path");
+ }
+
+ @Test
+ public void firebasePost() throws Exception {
+ // Mock out the firebase response. See
+ // http://g.co/dv/api-client-library/java/google-http-java-client/unit-testing
+ MockHttpTransport mockHttpTransport = spy(new MockHttpTransport() {
+ @Override
+ public LowLevelHttpRequest buildRequest(String method, String url) throws IOException {
+ return new MockLowLevelHttpRequest() {
+ @Override
+ public LowLevelHttpResponse execute() throws IOException {
+ MockLowLevelHttpResponse response = new MockLowLevelHttpResponse();
+ response.setStatusCode(200);
+ return response;
+ }
+ };
+ }
+ });
+ FirebaseChannel.getInstance().httpTransport = mockHttpTransport;
+ Game game = new Game();
+
+ firebaseChannel.firebasePost(FIREBASE_DB_URL + "/my/path", game);
+
+ verify(mockHttpTransport, times(1)).buildRequest("POST", FIREBASE_DB_URL + "/my/path");
+ }
+
+ @Test
+ public void firebaseGet() throws Exception {
+ // Mock out the firebase response. See
+ // http://g.co/dv/api-client-library/java/google-http-java-client/unit-testing
+ MockHttpTransport mockHttpTransport = spy(new MockHttpTransport() {
+ @Override
+ public LowLevelHttpRequest buildRequest(String method, String url) throws IOException {
+ return new MockLowLevelHttpRequest() {
+ @Override
+ public LowLevelHttpResponse execute() throws IOException {
+ MockLowLevelHttpResponse response = new MockLowLevelHttpResponse();
+ response.setStatusCode(200);
+ return response;
+ }
+ };
+ }
+ });
+ FirebaseChannel.getInstance().httpTransport = mockHttpTransport;
+
+ firebaseChannel.firebaseGet(FIREBASE_DB_URL + "/my/path");
+
+ verify(mockHttpTransport, times(1)).buildRequest("GET", FIREBASE_DB_URL + "/my/path");
+ }
+
+ @Test
+ public void firebaseDelete() throws Exception {
+ // Mock out the firebase response. See
+ // http://g.co/dv/api-client-library/java/google-http-java-client/unit-testing
+ MockHttpTransport mockHttpTransport = spy(new MockHttpTransport() {
+ @Override
+ public LowLevelHttpRequest buildRequest(String method, String url) throws IOException {
+ return new MockLowLevelHttpRequest() {
+ @Override
+ public LowLevelHttpResponse execute() throws IOException {
+ MockLowLevelHttpResponse response = new MockLowLevelHttpResponse();
+ response.setStatusCode(200);
+ return response;
+ }
+ };
+ }
+ });
+ FirebaseChannel.getInstance().httpTransport = mockHttpTransport;
+
+ firebaseChannel.firebaseDelete(FIREBASE_DB_URL + "/my/path");
+
+ verify(mockHttpTransport, times(1)).buildRequest("DELETE", FIREBASE_DB_URL + "/my/path");
+ }
+}
diff --git a/appengine-java8/firebase-tictactoe/src/test/java/com/example/appengine/firetactoe/MoveServletTest.java b/appengine-java8/firebase-tictactoe/src/test/java/com/example/appengine/firetactoe/MoveServletTest.java
new file mode 100644
index 00000000000..ac07a251028
--- /dev/null
+++ b/appengine-java8/firebase-tictactoe/src/test/java/com/example/appengine/firetactoe/MoveServletTest.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright 2016 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.firetactoe;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.google.api.client.http.LowLevelHttpRequest;
+import com.google.api.client.http.LowLevelHttpResponse;
+import com.google.api.client.testing.http.MockHttpTransport;
+import com.google.api.client.testing.http.MockLowLevelHttpRequest;
+import com.google.api.client.testing.http.MockLowLevelHttpResponse;
+import com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig;
+import com.google.appengine.tools.development.testing.LocalServiceTestHelper;
+import com.google.appengine.tools.development.testing.LocalURLFetchServiceTestConfig;
+import com.google.appengine.tools.development.testing.LocalUserServiceTestConfig;
+import com.google.common.collect.ImmutableMap;
+import com.googlecode.objectify.Objectify;
+import com.googlecode.objectify.ObjectifyFactory;
+import com.googlecode.objectify.ObjectifyService;
+import com.googlecode.objectify.util.Closeable;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Matchers;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.util.HashMap;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Unit tests for {@link MoveServlet}.
+ */
+@RunWith(JUnit4.class)
+public class MoveServletTest {
+ private static final String USER_EMAIL = "whisky@tangofoxtr.ot";
+ private static final String USER_ID = "whiskytangofoxtrot";
+ private static final String FIREBASE_DB_URL = "http://firebase.com/dburl";
+
+ private final LocalServiceTestHelper helper =
+ new LocalServiceTestHelper(
+ // Set no eventual consistency, that way queries return all results.
+ // http://g.co/cloud/appengine/docs/java/tools/localunittesting#Java_Writing_High_Replication_Datastore_tests
+ new LocalDatastoreServiceTestConfig().setDefaultHighRepJobPolicyUnappliedJobPercentage(0),
+ new LocalUserServiceTestConfig(),
+ new LocalURLFetchServiceTestConfig()
+ )
+ .setEnvEmail(USER_EMAIL)
+ .setEnvAuthDomain("gmail.com")
+ .setEnvAttributes(new HashMap(
+ ImmutableMap.of("com.google.appengine.api.users.UserService.user_id_key", USER_ID)));
+
+ @Mock private HttpServletRequest mockRequest;
+ @Mock private HttpServletResponse mockResponse;
+ protected Closeable dbSession;
+
+ private MoveServlet servletUnderTest;
+
+ @BeforeClass
+ public static void setUpBeforeClass() {
+ // Reset the Factory so that all translators work properly.
+ ObjectifyService.setFactory(new ObjectifyFactory());
+ ObjectifyService.register(Game.class);
+ // Mock out the firebase config
+ FirebaseChannel.firebaseConfigStream = new ByteArrayInputStream(
+ String.format("databaseURL: \"%s\"", FIREBASE_DB_URL).getBytes());
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ helper.setUp();
+ dbSession = ObjectifyService.begin();
+
+ servletUnderTest = new MoveServlet();
+
+ helper.setEnvIsLoggedIn(true);
+ // Make sure there are no firebase requests if we don't expect it
+ FirebaseChannel.getInstance().httpTransport = null;
+ }
+
+ @After
+ public void tearDown() {
+ dbSession.close();
+ helper.tearDown();
+ }
+
+ @Test
+ public void doPost_myTurn_move() throws Exception {
+ // Insert a game
+ Objectify ofy = ObjectifyService.ofy();
+ Game game = new Game(USER_ID, "my-opponent", " ", true);
+ ofy.save().entity(game).now();
+ String gameKey = game.getId();
+
+ when(mockRequest.getParameter("gameKey")).thenReturn(gameKey);
+ when(mockRequest.getParameter("cell")).thenReturn("1");
+
+ // Mock out the firebase response. See
+ // http://g.co/dv/api-client-library/java/google-http-java-client/unit-testing
+ MockHttpTransport mockHttpTransport = spy(new MockHttpTransport() {
+ @Override
+ public LowLevelHttpRequest buildRequest(String method, String url) throws IOException {
+ return new MockLowLevelHttpRequest() {
+ @Override
+ public LowLevelHttpResponse execute() throws IOException {
+ MockLowLevelHttpResponse response = new MockLowLevelHttpResponse();
+ response.setStatusCode(200);
+ return response;
+ }
+ };
+ }
+ });
+ FirebaseChannel.getInstance().httpTransport = mockHttpTransport;
+
+ servletUnderTest.doPost(mockRequest, mockResponse);
+
+ game = ofy.load().type(Game.class).id(gameKey).safe();
+ assertThat(game.board).isEqualTo(" X ");
+
+ verify(mockHttpTransport, times(2)).buildRequest(
+ eq("PATCH"), Matchers.matches(FIREBASE_DB_URL + "/channels/[\\w-]+.json$"));
+ }
+
+ public void doPost_notMyTurn_move() throws Exception {
+ // Insert a game
+ Objectify ofy = ObjectifyService.ofy();
+ Game game = new Game(USER_ID, "my-opponent", " ", false);
+ ofy.save().entity(game).now();
+ String gameKey = game.getId();
+
+ when(mockRequest.getParameter("gameKey")).thenReturn(gameKey);
+ when(mockRequest.getParameter("cell")).thenReturn("1");
+
+ servletUnderTest.doPost(mockRequest, mockResponse);
+
+ verify(mockResponse).sendError(401);
+ }
+}
diff --git a/appengine-java8/firebase-tictactoe/src/test/java/com/example/appengine/firetactoe/OpenedServletTest.java b/appengine-java8/firebase-tictactoe/src/test/java/com/example/appengine/firetactoe/OpenedServletTest.java
new file mode 100644
index 00000000000..9b2990196f6
--- /dev/null
+++ b/appengine-java8/firebase-tictactoe/src/test/java/com/example/appengine/firetactoe/OpenedServletTest.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright 2016 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.firetactoe;
+
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.google.api.client.http.LowLevelHttpRequest;
+import com.google.api.client.http.LowLevelHttpResponse;
+import com.google.api.client.testing.http.MockHttpTransport;
+import com.google.api.client.testing.http.MockLowLevelHttpRequest;
+import com.google.api.client.testing.http.MockLowLevelHttpResponse;
+import com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig;
+import com.google.appengine.tools.development.testing.LocalServiceTestHelper;
+import com.google.appengine.tools.development.testing.LocalURLFetchServiceTestConfig;
+import com.google.appengine.tools.development.testing.LocalUserServiceTestConfig;
+import com.google.common.collect.ImmutableMap;
+import com.googlecode.objectify.Objectify;
+import com.googlecode.objectify.ObjectifyFactory;
+import com.googlecode.objectify.ObjectifyService;
+import com.googlecode.objectify.util.Closeable;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Matchers;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.util.HashMap;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Unit tests for {@link OpenedServlet}.
+ */
+@RunWith(JUnit4.class)
+public class OpenedServletTest {
+ private static final String USER_EMAIL = "whisky@tangofoxtr.ot";
+ private static final String USER_ID = "whiskytangofoxtrot";
+ private static final String FIREBASE_DB_URL = "http://firebase.com/dburl";
+
+ private final LocalServiceTestHelper helper =
+ new LocalServiceTestHelper(
+ // Set no eventual consistency, that way queries return all results.
+ // http://g.co/cloud/appengine/docs/java/tools/localunittesting#Java_Writing_High_Replication_Datastore_tests
+ new LocalDatastoreServiceTestConfig().setDefaultHighRepJobPolicyUnappliedJobPercentage(0),
+ new LocalUserServiceTestConfig(),
+ new LocalURLFetchServiceTestConfig()
+ )
+ .setEnvEmail(USER_EMAIL)
+ .setEnvAuthDomain("gmail.com")
+ .setEnvAttributes(new HashMap(
+ ImmutableMap.of("com.google.appengine.api.users.UserService.user_id_key", USER_ID)));
+
+ @Mock private HttpServletRequest mockRequest;
+ @Mock private HttpServletResponse mockResponse;
+ protected Closeable dbSession;
+
+ private OpenedServlet servletUnderTest;
+
+ @BeforeClass
+ public static void setUpBeforeClass() {
+ // Reset the Factory so that all translators work properly.
+ ObjectifyService.setFactory(new ObjectifyFactory());
+ ObjectifyService.register(Game.class);
+ // Mock out the firebase config
+ FirebaseChannel.firebaseConfigStream = new ByteArrayInputStream(
+ String.format("databaseURL: \"%s\"", FIREBASE_DB_URL).getBytes());
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ helper.setUp();
+ dbSession = ObjectifyService.begin();
+
+ servletUnderTest = new OpenedServlet();
+
+ helper.setEnvIsLoggedIn(true);
+ // Make sure there are no firebase requests if we don't expect it
+ FirebaseChannel.getInstance().httpTransport = null;
+ }
+
+ @After
+ public void tearDown() {
+ dbSession.close();
+ helper.tearDown();
+ }
+
+ @Test
+ public void doPost_open() throws Exception {
+ // Insert a game
+ Objectify ofy = ObjectifyService.ofy();
+ Game game = new Game(USER_ID, "my-opponent", " ", true);
+ ofy.save().entity(game).now();
+ String gameKey = game.getId();
+
+ when(mockRequest.getParameter("gameKey")).thenReturn(gameKey);
+
+ // Mock out the firebase response. See
+ // http://g.co/dv/api-client-library/java/google-http-java-client/unit-testing
+ MockHttpTransport mockHttpTransport = spy(new MockHttpTransport() {
+ @Override
+ public LowLevelHttpRequest buildRequest(String method, String url) throws IOException {
+ return new MockLowLevelHttpRequest() {
+ @Override
+ public LowLevelHttpResponse execute() throws IOException {
+ MockLowLevelHttpResponse response = new MockLowLevelHttpResponse();
+ response.setStatusCode(200);
+ return response;
+ }
+ };
+ }
+ });
+ FirebaseChannel.getInstance().httpTransport = mockHttpTransport;
+
+ servletUnderTest.doPost(mockRequest, mockResponse);
+
+ verify(mockHttpTransport, times(2)).buildRequest(
+ eq("PATCH"), Matchers.matches(FIREBASE_DB_URL + "/channels/[\\w-]+.json$"));
+ }
+}
diff --git a/appengine-java8/firebase-tictactoe/src/test/java/com/example/appengine/firetactoe/TicTacToeServletTest.java b/appengine-java8/firebase-tictactoe/src/test/java/com/example/appengine/firetactoe/TicTacToeServletTest.java
new file mode 100644
index 00000000000..7f5fdf90150
--- /dev/null
+++ b/appengine-java8/firebase-tictactoe/src/test/java/com/example/appengine/firetactoe/TicTacToeServletTest.java
@@ -0,0 +1,209 @@
+/*
+ * Copyright 2016 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.firetactoe;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.anyString;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.google.api.client.http.LowLevelHttpRequest;
+import com.google.api.client.http.LowLevelHttpResponse;
+import com.google.api.client.testing.http.MockHttpTransport;
+import com.google.api.client.testing.http.MockLowLevelHttpRequest;
+import com.google.api.client.testing.http.MockLowLevelHttpResponse;
+import com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig;
+import com.google.appengine.tools.development.testing.LocalServiceTestHelper;
+import com.google.appengine.tools.development.testing.LocalURLFetchServiceTestConfig;
+import com.google.appengine.tools.development.testing.LocalUserServiceTestConfig;
+import com.google.common.collect.ImmutableMap;
+import com.googlecode.objectify.Objectify;
+import com.googlecode.objectify.ObjectifyFactory;
+import com.googlecode.objectify.ObjectifyService;
+import com.googlecode.objectify.util.Closeable;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Matchers;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.lang.StringBuffer;
+import java.util.HashMap;
+import javax.servlet.RequestDispatcher;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Unit tests for {@link TicTacToeServlet}.
+ */
+@RunWith(JUnit4.class)
+public class TicTacToeServletTest {
+ private static final String USER_EMAIL = "whisky@tangofoxtr.ot";
+ private static final String USER_ID = "whiskytangofoxtrot";
+ private static final String FIREBASE_DB_URL = "http://firebase.com/dburl";
+
+ private final LocalServiceTestHelper helper =
+ new LocalServiceTestHelper(
+ // Set no eventual consistency, that way queries return all results.
+ // http://g.co/cloud/appengine/docs/java/tools/localunittesting#Java_Writing_High_Replication_Datastore_tests
+ new LocalDatastoreServiceTestConfig().setDefaultHighRepJobPolicyUnappliedJobPercentage(0),
+ new LocalUserServiceTestConfig(),
+ new LocalURLFetchServiceTestConfig()
+ )
+ .setEnvEmail(USER_EMAIL)
+ .setEnvAuthDomain("gmail.com")
+ .setEnvAttributes(new HashMap(
+ ImmutableMap.of("com.google.appengine.api.users.UserService.user_id_key", USER_ID)));
+
+ @Mock private HttpServletRequest mockRequest;
+ @Mock private HttpServletResponse mockResponse;
+ protected Closeable dbSession;
+ @Mock RequestDispatcher requestDispatcher;
+
+ private TicTacToeServlet servletUnderTest;
+
+ @BeforeClass
+ public static void setUpBeforeClass() {
+ // Reset the Factory so that all translators work properly.
+ ObjectifyService.setFactory(new ObjectifyFactory());
+ ObjectifyService.register(Game.class);
+ // Mock out the firebase config
+ FirebaseChannel.firebaseConfigStream = new ByteArrayInputStream(
+ String.format("databaseURL: \"%s\"", FIREBASE_DB_URL).getBytes());
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ helper.setUp();
+ dbSession = ObjectifyService.begin();
+
+ // Set up a fake HTTP response.
+ when(mockRequest.getRequestURL()).thenReturn(new StringBuffer("https://timbre/"));
+ when(mockRequest.getRequestDispatcher("/WEB-INF/view/index.jsp")).thenReturn(requestDispatcher);
+
+ servletUnderTest = new TicTacToeServlet();
+
+ helper.setEnvIsLoggedIn(true);
+ }
+
+ @After
+ public void tearDown() {
+ dbSession.close();
+ helper.tearDown();
+ }
+
+ @Test
+ public void doGet_noGameKey() throws Exception {
+ // Mock out the firebase response. See
+ // http://g.co/dv/api-client-library/java/google-http-java-client/unit-testing
+ MockHttpTransport mockHttpTransport = spy(new MockHttpTransport() {
+ @Override
+ public LowLevelHttpRequest buildRequest(String method, String url) throws IOException {
+ return new MockLowLevelHttpRequest() {
+ @Override
+ public LowLevelHttpResponse execute() throws IOException {
+ MockLowLevelHttpResponse response = new MockLowLevelHttpResponse();
+ response.setStatusCode(200);
+ return response;
+ }
+ };
+ }
+ });
+ FirebaseChannel.getInstance().httpTransport = mockHttpTransport;
+
+ servletUnderTest.doGet(mockRequest, mockResponse);
+
+ // Make sure the game object was created for a new game
+ Objectify ofy = ObjectifyService.ofy();
+ Game game = ofy.load().type(Game.class).first().safe();
+ assertThat(game.userX).isEqualTo(USER_ID);
+
+ verify(mockHttpTransport, times(1)).buildRequest(
+ eq("PATCH"), Matchers.matches(FIREBASE_DB_URL + "/channels/[\\w-]+.json$"));
+ verify(requestDispatcher).forward(mockRequest, mockResponse);
+ verify(mockRequest).setAttribute(eq("token"), anyString());
+ verify(mockRequest).setAttribute("game_key", game.id);
+ verify(mockRequest).setAttribute("me", USER_ID);
+ verify(mockRequest).setAttribute("channel_id", USER_ID + game.id);
+ verify(mockRequest).setAttribute(eq("initial_message"), anyString());
+ verify(mockRequest).setAttribute(eq("game_link"), anyString());
+ }
+
+ @Test
+ public void doGet_existingGame() throws Exception {
+ // Mock out the firebase response. See
+ // http://g.co/dv/api-client-library/java/google-http-java-client/unit-testing
+ MockHttpTransport mockHttpTransport = spy(new MockHttpTransport() {
+ @Override
+ public LowLevelHttpRequest buildRequest(String method, String url) throws IOException {
+ return new MockLowLevelHttpRequest() {
+ @Override
+ public LowLevelHttpResponse execute() throws IOException {
+ MockLowLevelHttpResponse response = new MockLowLevelHttpResponse();
+ response.setStatusCode(200);
+ return response;
+ }
+ };
+ }
+ });
+ FirebaseChannel.getInstance().httpTransport = mockHttpTransport;
+
+ // Insert a game
+ Objectify ofy = ObjectifyService.ofy();
+ Game game = new Game("some-other-user-id", null, " ", true);
+ ofy.save().entity(game).now();
+ String gameKey = game.getId();
+
+ when(mockRequest.getParameter("gameKey")).thenReturn(gameKey);
+
+ servletUnderTest.doGet(mockRequest, mockResponse);
+
+ // Make sure the game object was updated with the other player
+ game = ofy.load().type(Game.class).first().safe();
+ assertThat(game.userX).isEqualTo("some-other-user-id");
+ assertThat(game.userO).isEqualTo(USER_ID);
+
+ verify(mockHttpTransport, times(2)).buildRequest(
+ eq("PATCH"), Matchers.matches(FIREBASE_DB_URL + "/channels/[\\w-]+.json$"));
+ verify(requestDispatcher).forward(mockRequest, mockResponse);
+ verify(mockRequest).setAttribute(eq("token"), anyString());
+ verify(mockRequest).setAttribute("game_key", game.id);
+ verify(mockRequest).setAttribute("me", USER_ID);
+ verify(mockRequest).setAttribute("channel_id", USER_ID + gameKey);
+ verify(mockRequest).setAttribute(eq("initial_message"), anyString());
+ verify(mockRequest).setAttribute(eq("game_link"), anyString());
+ }
+
+ @Test
+ public void doGet_nonExistentGame() throws Exception {
+ when(mockRequest.getParameter("gameKey")).thenReturn("does-not-exist");
+
+ servletUnderTest.doGet(mockRequest, mockResponse);
+
+ verify(mockResponse).sendError(404);
+ }
+}
diff --git a/appengine-java8/guestbook-cloud-datastore/README.md b/appengine-java8/guestbook-cloud-datastore/README.md
new file mode 100644
index 00000000000..feee3374a1e
--- /dev/null
+++ b/appengine-java8/guestbook-cloud-datastore/README.md
@@ -0,0 +1,26 @@
+# appengine/guestbook-cloud-datastore
+
+An App Engine guestbook using Java, Maven, and the Cloud Datastore API via
+[google-cloud-java](https://github.com/GoogleCloudPlatform/google-cloud-java).
+
+Please ask questions on [StackOverflow](http://stackoverflow.com/questions/tagged/google-app-engine).
+
+## Running Locally
+
+First, pick a project ID. You can create a project in the [Cloud Console] if you'd like, though this
+isn't necessary unless you'd like to deploy the sample.
+
+Second, modify `Persistence.java`: replace `your-project-id-here` with the project ID you picked.
+
+Then start the [Cloud Datastore Emulator](https://cloud.google.com/datastore/docs/tools/datastore-emulator):
+
+ gcloud beta emulators datastore start --project=YOUR_PROJECT_ID_HERE
+
+Finally, in a new shell, [set the Datastore Emulator environmental variables](https://cloud.google.com/datastore/docs/tools/datastore-emulator#setting_environment_variables)
+and run
+
+ mvn clean appengine:run
+
+## Deploying
+
+ mvn clean appengine:deploy
diff --git a/appengine-java8/guestbook-cloud-datastore/pom.xml b/appengine-java8/guestbook-cloud-datastore/pom.xml
new file mode 100644
index 00000000000..2a57190c0da
--- /dev/null
+++ b/appengine-java8/guestbook-cloud-datastore/pom.xml
@@ -0,0 +1,113 @@
+
+
+
+
+ 4.0.0
+ war
+ 1.0-SNAPSHOT
+
+ com.example.appengine
+ appengine-guestbook-cloud-datastore-j8
+
+ 19.0
+
+
+ com.google.cloud
+ appengine-java8-samples
+ 1.0.0
+ ..
+
+
+
+
+
+ com.google.appengine
+ appengine-api-1.0-sdk
+ ${appengine.sdk.version}
+
+
+
+ javax.servlet
+ javax.servlet-api
+ 3.1.0
+ jar
+ provided
+
+
+
+ jstl
+ jstl
+ 1.2
+
+
+
+ com.google.cloud
+ google-cloud
+ 0.17.2-alpha
+
+
+
+ com.google.guava
+ guava
+ ${guava.version}
+
+
+
+
+ junit
+ junit
+ 4.12
+ test
+
+
+ org.mockito
+ mockito-all
+ 1.10.19
+ test
+
+
+ com.google.appengine
+ appengine-testing
+ ${appengine.sdk.version}
+ test
+
+
+ com.google.appengine
+ appengine-api-stubs
+ ${appengine.sdk.version}
+ test
+
+
+ com.google.appengine
+ appengine-tools-sdk
+ ${appengine.sdk.version}
+ test
+
+
+
+
+
+
+ com.google.cloud.tools
+ appengine-maven-plugin
+ 1.3.1
+
+
+
+
diff --git a/appengine-java8/guestbook-cloud-datastore/src/main/java/com/example/guestbook/Greeting.java b/appengine-java8/guestbook-cloud-datastore/src/main/java/com/example/guestbook/Greeting.java
new file mode 100644
index 00000000000..c4ae6783399
--- /dev/null
+++ b/appengine-java8/guestbook-cloud-datastore/src/main/java/com/example/guestbook/Greeting.java
@@ -0,0 +1,124 @@
+/**
+ * Copyright 2016 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
+ *
+ *
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.
+ */
+
+//[START all]
+package com.example.guestbook;
+
+import static com.example.guestbook.Persistence.getDatastore;
+
+import com.google.cloud.Timestamp;
+import com.google.cloud.datastore.Entity;
+import com.google.cloud.datastore.FullEntity;
+import com.google.cloud.datastore.FullEntity.Builder;
+import com.google.cloud.datastore.IncompleteKey;
+import com.google.cloud.datastore.Key;
+import com.google.common.base.MoreObjects;
+
+import java.util.Date;
+import java.util.Objects;
+
+public class Greeting {
+ private Guestbook book;
+
+ public Key key;
+ public String authorEmail;
+ public String authorId;
+ public String content;
+ public Date date;
+
+ public Greeting() {
+ date = new Date();
+ }
+
+ public Greeting(String book, String content) {
+ this();
+ this.book = new Guestbook(book);
+ this.content = content;
+ }
+
+ public Greeting(String book, String content, String id, String email) {
+ this(book, content);
+ authorEmail = email;
+ authorId = id;
+ }
+
+ public Greeting(Entity entity) {
+ key = entity.hasKey() ? entity.getKey() : null;
+ authorEmail = entity.contains("authorEmail") ? entity.getString("authorEmail") : null;
+ authorId = entity.contains("authorId") ? entity.getString("authorId") : null;
+
+ date = entity.contains("date") ? entity.getTimestamp("date").toSqlTimestamp() : null;
+ content = entity.contains("content") ? entity.getString("content") : null;
+ }
+
+ public void save() {
+ if (key == null) {
+ key = getDatastore().allocateId(makeIncompleteKey()); // Give this greeting a unique ID
+ }
+
+ Builder builder = FullEntity.newBuilder(key);
+
+ if (authorEmail != null) {
+ builder.set("authorEmail", authorEmail);
+ }
+
+ if (authorId != null) {
+ builder.set("authorId", authorId);
+ }
+
+ builder.set("content", content);
+ builder.set("date", Timestamp.of(date));
+
+ getDatastore().put(builder.build());
+ }
+
+ private IncompleteKey makeIncompleteKey() {
+ // The book is our ancestor key.
+ return Key.newBuilder(book.getKey(), "Greeting").build();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ Greeting greeting = (Greeting) o;
+ return Objects.equals(key, greeting.key)
+ && Objects.equals(authorEmail, greeting.authorEmail)
+ && Objects.equals(authorId, greeting.authorId)
+ && Objects.equals(content, greeting.content)
+ && Objects.equals(date, greeting.date);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(key, authorEmail, authorId, content, date);
+ }
+
+ @Override
+ public String toString() {
+ return MoreObjects.toStringHelper(this)
+ .add("key", key)
+ .add("authorEmail", authorEmail)
+ .add("authorId", authorId)
+ .add("content", content)
+ .add("date", date)
+ .add("book", book)
+ .toString();
+ }
+}
+//[END all]
diff --git a/appengine-java8/guestbook-cloud-datastore/src/main/java/com/example/guestbook/Guestbook.java b/appengine-java8/guestbook-cloud-datastore/src/main/java/com/example/guestbook/Guestbook.java
new file mode 100644
index 00000000000..4f2aeb69a5d
--- /dev/null
+++ b/appengine-java8/guestbook-cloud-datastore/src/main/java/com/example/guestbook/Guestbook.java
@@ -0,0 +1,98 @@
+/**
+ * Copyright 2016 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
+ *
+ *
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.guestbook;
+
+import static com.example.guestbook.Persistence.getDatastore;
+import static com.example.guestbook.Persistence.getKeyFactory;
+import static com.google.cloud.datastore.StructuredQuery.OrderBy.desc;
+import static com.google.cloud.datastore.StructuredQuery.PropertyFilter.hasAncestor;
+
+import com.google.cloud.datastore.Entity;
+import com.google.cloud.datastore.EntityQuery;
+import com.google.cloud.datastore.Key;
+import com.google.cloud.datastore.KeyFactory;
+import com.google.cloud.datastore.Query;
+import com.google.cloud.datastore.QueryResults;
+import com.google.common.base.MoreObjects;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableList.Builder;
+
+import java.util.List;
+import java.util.Objects;
+
+//[START all]
+public class Guestbook {
+ private static final KeyFactory keyFactory = getKeyFactory(Guestbook.class);
+ private final Key key;
+
+ public final String book;
+
+ public Guestbook(String book) {
+ this.book = book == null ? "default" : book;
+ key =
+ keyFactory.newKey(
+ this.book); // There is a 1:1 mapping between Guestbook names and Guestbook objects
+ }
+
+ public Key getKey() {
+ return key;
+ }
+
+ public List getGreetings() {
+ // This query requires the index defined in index.yaml to work because of the orderBy on date.
+ EntityQuery query =
+ Query.newEntityQueryBuilder()
+ .setKind("Greeting")
+ .setFilter(hasAncestor(key))
+ .setOrderBy(desc("date"))
+ .setLimit(5)
+ .build();
+
+ QueryResults results = getDatastore().run(query);
+
+ Builder resultListBuilder = ImmutableList.builder();
+ while (results.hasNext()) {
+ resultListBuilder.add(new Greeting(results.next()));
+ }
+
+ return resultListBuilder.build();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ Guestbook guestbook = (Guestbook) o;
+ return Objects.equals(book, guestbook.book) && Objects.equals(key, guestbook.key);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(book, key);
+ }
+
+ @Override
+ public String toString() {
+ return MoreObjects.toStringHelper(this)
+ .add("keyFactory", keyFactory)
+ .add("book", book)
+ .add("key", key)
+ .toString();
+ }
+}
+//[END all]
diff --git a/appengine-java8/guestbook-cloud-datastore/src/main/java/com/example/guestbook/Persistence.java b/appengine-java8/guestbook-cloud-datastore/src/main/java/com/example/guestbook/Persistence.java
new file mode 100644
index 00000000000..1b68b8541e9
--- /dev/null
+++ b/appengine-java8/guestbook-cloud-datastore/src/main/java/com/example/guestbook/Persistence.java
@@ -0,0 +1,43 @@
+/**
+ * Copyright 2016 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
+ *
+ *
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.guestbook;
+
+import com.google.cloud.datastore.Datastore;
+import com.google.cloud.datastore.DatastoreOptions;
+import com.google.cloud.datastore.KeyFactory;
+
+import java.util.concurrent.atomic.AtomicReference;
+
+//[START all]
+public class Persistence {
+ private static AtomicReference datastore = new AtomicReference<>();
+
+ public static Datastore getDatastore() {
+ if (datastore.get() == null) {
+ datastore.set(DatastoreOptions.newBuilder().setProjectId("your-project-id-here")
+ .build().getService());
+ }
+
+ return datastore.get();
+ }
+
+ public static void setDatastore(Datastore datastore) {
+ Persistence.datastore.set(datastore);
+ }
+
+ public static KeyFactory getKeyFactory(Class> c) {
+ return getDatastore().newKeyFactory().setKind(c.getSimpleName());
+ }
+}
+//[END all]
diff --git a/appengine-java8/guestbook-cloud-datastore/src/main/java/com/example/guestbook/SignGuestbookServlet.java b/appengine-java8/guestbook-cloud-datastore/src/main/java/com/example/guestbook/SignGuestbookServlet.java
new file mode 100644
index 00000000000..2c95c5c3aee
--- /dev/null
+++ b/appengine-java8/guestbook-cloud-datastore/src/main/java/com/example/guestbook/SignGuestbookServlet.java
@@ -0,0 +1,50 @@
+/**
+ * Copyright 2016 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
+ *
+ *
Hello, ${fn:escapeXml(user.nickname)}! (You can
+ sign out.)
+<%
+ } else {
+%>
+
Hello!
+ Sign in
+ to include your name with greetings you post.
+<%
+ }
+%>
+
+<%-- //[START datastore]--%>
+<%
+ // Create the correct Ancestor key
+ Guestbook theBook = new Guestbook(guestbookName);
+
+ // Run an ancestor query to ensure we see the most up-to-date
+ // view of the Greetings belonging to the selected Guestbook.
+ List greetings = theBook.getGreetings();
+
+ if (greetings.isEmpty()) {
+%>
+
Guestbook '${fn:escapeXml(guestbookName)}' has no messages.
+<%
+ } else {
+%>
+
Messages in Guestbook '${fn:escapeXml(guestbookName)}'.
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
+ *
+ *
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.guestbook;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.List;
+
+@RunWith(JUnit4.class)
+public class GreetingTest {
+
+ @Before
+ public void setUp() {
+ TestUtils.startDatastore();
+ }
+
+ @Test
+ public void testSaveGreeting() throws Exception {
+ Greeting greeting = new Greeting(null, "Test!");
+ greeting.save();
+
+ Guestbook guestbook = new Guestbook(null);
+ List greetings = guestbook.getGreetings();
+ assertTrue(greetings.size() == 1);
+ assertEquals(greeting, greetings.get(0));
+ }
+
+ @After
+ public void tearDown() {
+ TestUtils.stopDatastore();
+ }
+}
diff --git a/appengine-java8/guestbook-cloud-datastore/src/test/java/com/example/guestbook/SignGuestbookServletTest.java b/appengine-java8/guestbook-cloud-datastore/src/test/java/com/example/guestbook/SignGuestbookServletTest.java
new file mode 100644
index 00000000000..4a5435eafe7
--- /dev/null
+++ b/appengine-java8/guestbook-cloud-datastore/src/test/java/com/example/guestbook/SignGuestbookServletTest.java
@@ -0,0 +1,70 @@
+/**
+ * Copyright 2016 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
+ *
+ *
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.guestbook;
+
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.when;
+
+import com.google.appengine.tools.development.testing.LocalServiceTestHelper;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.List;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+@RunWith(JUnit4.class)
+public class SignGuestbookServletTest {
+ private final LocalServiceTestHelper helper = new LocalServiceTestHelper();
+
+ @Mock private HttpServletRequest mockRequest;
+ @Mock private HttpServletResponse mockResponse;
+
+ private SignGuestbookServlet signGuestbookServlet;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ // Sets up the UserServiceFactory used in SignGuestbookServlet (but not in this test)
+ helper.setUp();
+
+ signGuestbookServlet = new SignGuestbookServlet();
+ TestUtils.startDatastore();
+ }
+
+ @Test
+ public void doPost_userNotLoggedIn() throws Exception {
+ String testBook = "default";
+ when(mockRequest.getParameter("guestbookName")).thenReturn(testBook);
+ String testGreeting = "beep!";
+ when(mockRequest.getParameter("content")).thenReturn(testGreeting);
+
+ signGuestbookServlet.doPost(mockRequest, mockResponse);
+ Guestbook guestbook = new Guestbook(testBook);
+ List greetings = guestbook.getGreetings();
+
+ assertTrue(greetings.size() == 1);
+ assertTrue(greetings.get(0).content.equals(testGreeting));
+ }
+
+ @After
+ public void tearDown() {
+ TestUtils.stopDatastore();
+ }
+}
diff --git a/appengine-java8/guestbook-cloud-datastore/src/test/java/com/example/guestbook/TestUtils.java b/appengine-java8/guestbook-cloud-datastore/src/test/java/com/example/guestbook/TestUtils.java
new file mode 100644
index 00000000000..0c4225ce96c
--- /dev/null
+++ b/appengine-java8/guestbook-cloud-datastore/src/test/java/com/example/guestbook/TestUtils.java
@@ -0,0 +1,50 @@
+package com.example.guestbook;
+
+import static com.example.guestbook.Persistence.getDatastore;
+
+import com.google.cloud.datastore.Datastore;
+import com.google.cloud.datastore.Key;
+import com.google.cloud.datastore.Query;
+import com.google.cloud.datastore.QueryResults;
+import com.google.cloud.datastore.testing.LocalDatastoreHelper;
+import com.google.common.collect.Lists;
+
+import org.threeten.bp.Duration;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.concurrent.TimeoutException;
+
+
+public class TestUtils {
+ static LocalDatastoreHelper datastore = LocalDatastoreHelper.create();
+
+ public static void startDatastore() {
+ try {
+ datastore.start();
+ Persistence.setDatastore(datastore.getOptions().getService());
+ } catch (IOException | InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static void stopDatastore() {
+ try {
+ datastore.stop(Duration.ofSeconds(20 ));
+ Persistence.setDatastore(null);
+ } catch (TimeoutException | IOException | InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static void wipeDatastore() {
+ Datastore datastore = getDatastore();
+ QueryResults guestbooks = datastore.run(Query.newKeyQueryBuilder().setKind("Greeting")
+ .build());
+ ArrayList keys = Lists.newArrayList(guestbooks);
+
+ if (!keys.isEmpty()) {
+ datastore.delete(keys.toArray(new Key[keys.size()]));
+ }
+ }
+}
diff --git a/appengine-java8/guestbook-objectify/README.md b/appengine-java8/guestbook-objectify/README.md
new file mode 100644
index 00000000000..2a7ee9cd289
--- /dev/null
+++ b/appengine-java8/guestbook-objectify/README.md
@@ -0,0 +1,17 @@
+# appengine/guestbook-objectify
+
+An App Engine guestbook using Java, Maven, and Objectify.
+
+Data access using [Objectify](https://github.com/objectify/objectify)
+
+Please ask questions on [Stackoverflow](http://stackoverflow.com/questions/tagged/google-app-engine)
+
+## Running Locally
+
+How do I, as a developer, start working on the project?
+
+1. `mvn clean appengine:devserver`
+
+## Deploying
+
+1. `mvn clean appengine:update -Dappengine.appId=PROJECT -Dappengine.version=VERSION`
diff --git a/appengine-java8/guestbook-objectify/pom.xml b/appengine-java8/guestbook-objectify/pom.xml
new file mode 100644
index 00000000000..13052937a2d
--- /dev/null
+++ b/appengine-java8/guestbook-objectify/pom.xml
@@ -0,0 +1,126 @@
+
+
+
+
+ 4.0.0
+ war
+ 1.0-SNAPSHOT
+
+ com.example.appengine
+ appengine-guestbook-objectify-j8
+
+ 5.1.17
+ 20.0
+
+
+ com.google.cloud
+ appengine-java8-samples
+ 1.0.0
+ ..
+
+
+
+
+
+ com.google.appengine
+ appengine-api-1.0-sdk
+ ${appengine.sdk.version}
+
+
+
+ javax.servlet
+ javax.servlet-api
+ 3.1.0
+ jar
+ provided
+
+
+
+ jstl
+ jstl
+ 1.2
+
+
+
+
+ com.google.guava
+ guava
+ ${guava.version}
+
+
+ com.googlecode.objectify
+ objectify
+ ${objectify.version}
+
+
+
+
+
+ junit
+ junit
+ 4.12
+ test
+
+
+ org.mockito
+ mockito-all
+ 1.10.19
+ test
+
+
+ com.google.appengine
+ appengine-testing
+ ${appengine.sdk.version}
+ test
+
+
+ com.google.appengine
+ appengine-api-stubs
+ ${appengine.sdk.version}
+ test
+
+
+ com.google.appengine
+ appengine-tools-sdk
+ ${appengine.sdk.version}
+ test
+
+
+ com.google.truth
+ truth
+ 0.32
+ test
+
+
+
+
+
+
+ ${project.build.directory}/${project.build.finalName}/WEB-INF/classes
+
+
+ com.google.cloud.tools
+ appengine-maven-plugin
+ 1.3.1
+
+ true
+ true
+
+
+
+
+
diff --git a/appengine-java8/guestbook-objectify/src/main/java/com/example/guestbook/Greeting.java b/appengine-java8/guestbook-objectify/src/main/java/com/example/guestbook/Greeting.java
new file mode 100644
index 00000000000..8067cd3fce7
--- /dev/null
+++ b/appengine-java8/guestbook-objectify/src/main/java/com/example/guestbook/Greeting.java
@@ -0,0 +1,80 @@
+/**
+ * Copyright 2014-2015 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.
+ */
+
+//[START all]
+package com.example.guestbook;
+
+import com.googlecode.objectify.Key;
+import com.googlecode.objectify.annotation.Entity;
+import com.googlecode.objectify.annotation.Id;
+import com.googlecode.objectify.annotation.Index;
+import com.googlecode.objectify.annotation.Parent;
+
+import java.lang.String;
+import java.util.Date;
+
+/**
+ * The @Entity tells Objectify about our entity. We also register it in {@link OfyHelper}
+ * Our primary key @Id is set automatically by the Google Datastore for us.
+ *
+ * We add a @Parent to tell the object about its ancestor. We are doing this to support many
+ * guestbooks. Objectify, unlike the AppEngine library requires that you specify the fields you
+ * want to index using @Index. Only indexing the fields you need can lead to substantial gains in
+ * performance -- though if not indexing your data from the start will require indexing it later.
+ *
+ * NOTE - all the properties are PUBLIC so that can keep the code simple.
+ **/
+@Entity
+public class Greeting {
+ @Parent Key theBook;
+ @Id public Long id;
+
+ public String authorEmail;
+ public String authorId;
+ public String content;
+ @Index public Date date;
+
+ /**
+ * Simple constructor just sets the date.
+ **/
+ public Greeting() {
+ date = new Date();
+ }
+
+ /**
+ * A convenience constructor.
+ **/
+ public Greeting(String book, String content) {
+ this();
+ if ( book != null ) {
+ theBook = Key.create(Guestbook.class, book); // Creating the Ancestor key
+ } else {
+ theBook = Key.create(Guestbook.class, "default");
+ }
+ this.content = content;
+ }
+
+ /**
+ * Takes all important fields.
+ **/
+ public Greeting(String book, String content, String id, String email) {
+ this(book, content);
+ authorEmail = email;
+ authorId = id;
+ }
+
+}
+//[END all]
diff --git a/appengine-java8/guestbook-objectify/src/main/java/com/example/guestbook/Guestbook.java b/appengine-java8/guestbook-objectify/src/main/java/com/example/guestbook/Guestbook.java
new file mode 100644
index 00000000000..2b490aeff79
--- /dev/null
+++ b/appengine-java8/guestbook-objectify/src/main/java/com/example/guestbook/Guestbook.java
@@ -0,0 +1,33 @@
+/**
+ * Copyright 2014-2015 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.
+ */
+
+//[START all]
+package com.example.guestbook;
+
+import com.googlecode.objectify.annotation.Entity;
+import com.googlecode.objectify.annotation.Id;
+
+/**
+ * The @Entity tells Objectify about our entity. We also register it in
+ * OfyHelper.java -- very important.
+ *
+ * This is never actually created, but gives a hint to Objectify about our Ancestor key.
+ */
+@Entity
+public class Guestbook {
+ @Id public String book;
+}
+//[END all]
diff --git a/appengine-java8/guestbook-objectify/src/main/java/com/example/guestbook/OfyHelper.java b/appengine-java8/guestbook-objectify/src/main/java/com/example/guestbook/OfyHelper.java
new file mode 100644
index 00000000000..3d34612007c
--- /dev/null
+++ b/appengine-java8/guestbook-objectify/src/main/java/com/example/guestbook/OfyHelper.java
@@ -0,0 +1,37 @@
+/**
+ * Copyright 2014-2015 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.
+ */
+//[START all]
+package com.example.guestbook;
+
+import com.googlecode.objectify.ObjectifyService;
+
+import javax.servlet.ServletContextEvent;
+import javax.servlet.ServletContextListener;
+
+/**
+ * OfyHelper, a ServletContextListener, is setup in web.xml to run before a JSP is run. This is
+ * required to let JSP's access Ofy.
+ **/
+public class OfyHelper implements ServletContextListener {
+ public void contextInitialized(ServletContextEvent event) {
+ // This will be invoked as part of a warmup request, or the first user request if no warmup
+ // request.
+ ObjectifyService.register(Guestbook.class);
+ ObjectifyService.register(Greeting.class);
+ }
+
+ public void contextDestroyed(ServletContextEvent event) {
+ // App Engine does not currently invoke this method.
+ }
+}
+//[END all]
diff --git a/appengine-java8/guestbook-objectify/src/main/java/com/example/guestbook/SignGuestbookServlet.java b/appengine-java8/guestbook-objectify/src/main/java/com/example/guestbook/SignGuestbookServlet.java
new file mode 100644
index 00000000000..e32912cb933
--- /dev/null
+++ b/appengine-java8/guestbook-objectify/src/main/java/com/example/guestbook/SignGuestbookServlet.java
@@ -0,0 +1,62 @@
+/**
+ * Copyright 2014-2015 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.
+ */
+
+//[START all]
+package com.example.guestbook;
+
+import com.google.appengine.api.users.User;
+import com.google.appengine.api.users.UserService;
+import com.google.appengine.api.users.UserServiceFactory;
+
+import com.googlecode.objectify.ObjectifyService;
+
+import java.io.IOException;
+
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Form Handling Servlet - most of the action for this sample is in webapp/guestbook.jsp,
+ * which displays the {@link Greeting}'s.
+ */
+public class SignGuestbookServlet extends HttpServlet {
+
+ // Process the http POST of the form
+ @Override
+ public void doPost(HttpServletRequest req, HttpServletResponse resp)
+ throws IOException {
+ Greeting greeting;
+
+ UserService userService = UserServiceFactory.getUserService();
+ User user = userService.getCurrentUser(); // Find out who the user is.
+
+ String guestbookName = req.getParameter("guestbookName");
+ String content = req.getParameter("content");
+ if (user != null) {
+ greeting = new Greeting(guestbookName, content, user.getUserId(), user.getEmail());
+ } else {
+ greeting = new Greeting(guestbookName, content);
+ }
+
+ // Use Objectify to save the greeting and now() is used to make the call synchronously as we
+ // will immediately get a new page using redirect and we want the data to be present.
+ ObjectifyService.ofy().save().entity(greeting).now();
+
+ resp.sendRedirect("/guestbook.jsp?guestbookName=" + guestbookName);
+ }
+}
+//[END all]
diff --git a/appengine-java8/guestbook-objectify/src/main/webapp/WEB-INF/appengine-web.xml b/appengine-java8/guestbook-objectify/src/main/webapp/WEB-INF/appengine-web.xml
new file mode 100644
index 00000000000..b85f72b69e3
--- /dev/null
+++ b/appengine-java8/guestbook-objectify/src/main/webapp/WEB-INF/appengine-web.xml
@@ -0,0 +1,9 @@
+
+
+ java8
+ true
+
+
+
+
+
diff --git a/appengine-java8/guestbook-objectify/src/main/webapp/WEB-INF/logging.properties b/appengine-java8/guestbook-objectify/src/main/webapp/WEB-INF/logging.properties
new file mode 100644
index 00000000000..a17206681f0
--- /dev/null
+++ b/appengine-java8/guestbook-objectify/src/main/webapp/WEB-INF/logging.properties
@@ -0,0 +1,13 @@
+# A default java.util.logging configuration.
+# (All App Engine logging is through java.util.logging by default).
+#
+# To use this configuration, copy it into your application's WEB-INF
+# folder and add the following to your appengine-web.xml:
+#
+#
+#
+#
+#
+
+# Set the default logging level for all loggers to WARNING
+.level = WARNING
diff --git a/appengine-java8/guestbook-objectify/src/main/webapp/WEB-INF/web.xml b/appengine-java8/guestbook-objectify/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 00000000000..d5a23626f0c
--- /dev/null
+++ b/appengine-java8/guestbook-objectify/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+ sign
+ com.example.guestbook.SignGuestbookServlet
+
+
+
+ sign
+ /sign
+
+
+
+ guestbook.jsp
+
+
+
+
+
+ ObjectifyFilter
+ com.googlecode.objectify.ObjectifyFilter
+
+
+ ObjectifyFilter
+ /*
+
+
+ com.example.guestbook.OfyHelper
+
+
+
diff --git a/appengine-java8/guestbook-objectify/src/main/webapp/guestbook.jsp b/appengine-java8/guestbook-objectify/src/main/webapp/guestbook.jsp
new file mode 100644
index 00000000000..481274ceb84
--- /dev/null
+++ b/appengine-java8/guestbook-objectify/src/main/webapp/guestbook.jsp
@@ -0,0 +1,106 @@
+<%-- //[START all]--%>
+<%@ page contentType="text/html;charset=UTF-8" language="java" %>
+<%@ page import="com.google.appengine.api.users.User" %>
+<%@ page import="com.google.appengine.api.users.UserService" %>
+<%@ page import="com.google.appengine.api.users.UserServiceFactory" %>
+
+<%-- //[START imports]--%>
+<%@ page import="com.example.guestbook.Greeting" %>
+<%@ page import="com.example.guestbook.Guestbook" %>
+<%@ page import="com.googlecode.objectify.Key" %>
+<%@ page import="com.googlecode.objectify.ObjectifyService" %>
+<%-- //[END imports]--%>
+
+<%@ page import="java.util.List" %>
+<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
+
+
+
+
+
+
+
+
+<%
+ String guestbookName = request.getParameter("guestbookName");
+ if (guestbookName == null) {
+ guestbookName = "default";
+ }
+ pageContext.setAttribute("guestbookName", guestbookName);
+ UserService userService = UserServiceFactory.getUserService();
+ User user = userService.getCurrentUser();
+ if (user != null) {
+ pageContext.setAttribute("user", user);
+%>
+
+
Hello, ${fn:escapeXml(user.nickname)}! (You can
+ sign out.)
+<%
+ } else {
+%>
+
Hello!
+ Sign in
+ to include your name with greetings you post.
+<%
+ }
+%>
+
+<%-- //[START datastore]--%>
+<%
+ // Create the correct Ancestor key
+ Key theBook = Key.create(Guestbook.class, guestbookName);
+
+ // Run an ancestor query to ensure we see the most up-to-date
+ // view of the Greetings belonging to the selected Guestbook.
+ List greetings = ObjectifyService.ofy()
+ .load()
+ .type(Greeting.class) // We want only Greetings
+ .ancestor(theBook) // Anyone in this book
+ .order("-date") // Most recent first - date is indexed.
+ .limit(5) // Only show 5 of them.
+ .list();
+
+ if (greetings.isEmpty()) {
+%>
+
Guestbook '${fn:escapeXml(guestbookName)}' has no messages.
+<%
+ } else {
+%>
+
Messages in Guestbook '${fn:escapeXml(guestbookName)}'.
+<%
+ }
+ }
+%>
+
+
+<%-- //[END datastore]--%>
+
+
+
+
+<%-- //[END all]--%>
diff --git a/appengine-java8/guestbook-objectify/src/main/webapp/stylesheets/main.css b/appengine-java8/guestbook-objectify/src/main/webapp/stylesheets/main.css
new file mode 100644
index 00000000000..05d72d5536d
--- /dev/null
+++ b/appengine-java8/guestbook-objectify/src/main/webapp/stylesheets/main.css
@@ -0,0 +1,4 @@
+body {
+ font-family: Verdana, Helvetica, sans-serif;
+ background-color: #FFFFCC;
+}
diff --git a/appengine-java8/guestbook-objectify/src/test/java/com/example/guestbook/GreetingTest.java b/appengine-java8/guestbook-objectify/src/test/java/com/example/guestbook/GreetingTest.java
new file mode 100644
index 00000000000..27d3030149d
--- /dev/null
+++ b/appengine-java8/guestbook-objectify/src/test/java/com/example/guestbook/GreetingTest.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2016 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.guestbook;
+
+import static com.example.guestbook.GuestbookTestUtilities.cleanDatastore;
+import static org.junit.Assert.assertEquals;
+
+import com.google.appengine.api.datastore.DatastoreService;
+import com.google.appengine.api.datastore.DatastoreServiceFactory;
+import com.google.appengine.api.datastore.Entity;
+import com.google.appengine.api.datastore.KeyFactory;
+import com.google.appengine.api.datastore.PreparedQuery;
+import com.google.appengine.api.datastore.Query;
+import com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig;
+import com.google.appengine.tools.development.testing.LocalServiceTestHelper;
+
+import com.googlecode.objectify.ObjectifyService;
+import com.googlecode.objectify.util.Closeable;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+
+@RunWith(JUnit4.class)
+public class GreetingTest {
+ private static final String TEST_CONTENT = "The world is Blue today";
+
+ private final LocalServiceTestHelper helper =
+ new LocalServiceTestHelper(
+ // Set no eventual consistency, that way queries return all results.
+ // https://cloud.google.com/appengine/docs/java/tools/localunittesting#Java_Writing_High_Replication_Datastore_tests
+ new LocalDatastoreServiceTestConfig()
+ .setDefaultHighRepJobPolicyUnappliedJobPercentage(0));
+
+ private Closeable closeable;
+ private DatastoreService ds;
+
+ @Before
+ public void setUp() throws Exception {
+
+ helper.setUp();
+ ds = DatastoreServiceFactory.getDatastoreService();
+
+ ObjectifyService.register(Guestbook.class);
+ ObjectifyService.register(Greeting.class);
+
+ closeable = ObjectifyService.begin();
+
+ cleanDatastore(ds, "default");
+ }
+
+ @After
+ public void tearDown() {
+ cleanDatastore(ds, "default");
+ helper.tearDown();
+ closeable.close();
+ }
+
+ @Test
+ public void createSaveObject() throws Exception {
+
+ Greeting g = new Greeting("default", TEST_CONTENT);
+ ObjectifyService.ofy().save().entity(g).now();
+
+ Query query = new Query("Greeting")
+ .setAncestor(new KeyFactory.Builder("Guestbook", "default").getKey());
+ PreparedQuery pq = ds.prepare(query);
+ Entity greeting = pq.asSingleEntity(); // Should only be one at this point.
+ assertEquals(greeting.getProperty("content"), TEST_CONTENT);
+ }
+}
diff --git a/appengine-java8/guestbook-objectify/src/test/java/com/example/guestbook/GuestbookTestUtilities.java b/appengine-java8/guestbook-objectify/src/test/java/com/example/guestbook/GuestbookTestUtilities.java
new file mode 100644
index 00000000000..cbd72b57b28
--- /dev/null
+++ b/appengine-java8/guestbook-objectify/src/test/java/com/example/guestbook/GuestbookTestUtilities.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2016 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.guestbook;
+
+import com.google.appengine.api.datastore.DatastoreService;
+import com.google.appengine.api.datastore.Entity;
+import com.google.appengine.api.datastore.FetchOptions;
+import com.google.appengine.api.datastore.Key;
+import com.google.appengine.api.datastore.KeyFactory;
+import com.google.appengine.api.datastore.PreparedQuery;
+import com.google.appengine.api.datastore.Query;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class GuestbookTestUtilities {
+
+ public static void cleanDatastore(DatastoreService ds, String book) {
+ Query query = new Query("Greeting")
+ .setAncestor(new KeyFactory.Builder("Guestbook", book)
+ .getKey()).setKeysOnly();
+ PreparedQuery pq = ds.prepare(query);
+ List entities = pq.asList(FetchOptions.Builder.withDefaults());
+ ArrayList keys = new ArrayList<>(entities.size());
+
+ for (Entity e : entities) {
+ keys.add(e.getKey());
+ }
+ ds.delete(keys);
+ }
+
+}
diff --git a/appengine-java8/guestbook-objectify/src/test/java/com/example/guestbook/SignGuestbookServletTest.java b/appengine-java8/guestbook-objectify/src/test/java/com/example/guestbook/SignGuestbookServletTest.java
new file mode 100644
index 00000000000..1c01ab37f8d
--- /dev/null
+++ b/appengine-java8/guestbook-objectify/src/test/java/com/example/guestbook/SignGuestbookServletTest.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2015 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.guestbook;
+
+import static com.example.guestbook.GuestbookTestUtilities.cleanDatastore;
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.when;
+
+import com.google.appengine.api.datastore.DatastoreService;
+import com.google.appengine.api.datastore.DatastoreServiceFactory;
+import com.google.appengine.api.datastore.Entity;
+import com.google.appengine.api.datastore.KeyFactory;
+import com.google.appengine.api.datastore.PreparedQuery;
+import com.google.appengine.api.datastore.Query;
+import com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig;
+import com.google.appengine.tools.development.testing.LocalServiceTestHelper;
+
+import com.googlecode.objectify.ObjectifyService;
+import com.googlecode.objectify.util.Closeable;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Unit tests for {@link com.example.guestbook.SignGuestbookServlet}.
+ */
+@RunWith(JUnit4.class)
+public class SignGuestbookServletTest {
+ private static final String FAKE_URL = "fakey.org/sign";
+ private static final String FAKE_NAME = "Fake";
+
+ private final LocalServiceTestHelper helper =
+ new LocalServiceTestHelper(
+ // Set no eventual consistency, that way queries return all results.
+ // https://cloud.google.com/appengine/docs/java/tools/localunittesting#Java_Writing_High_Replication_Datastore_tests
+ new LocalDatastoreServiceTestConfig()
+ .setDefaultHighRepJobPolicyUnappliedJobPercentage(0));
+
+ private final String testPhrase = "Noew is the time";
+
+ @Mock private HttpServletRequest mockRequest;
+
+ @Mock
+ private HttpServletResponse mockResponse;
+
+ private StringWriter stringWriter;
+ private SignGuestbookServlet servletUnderTest;
+ private Closeable closeable;
+ private DatastoreService ds;
+
+ @Before
+ public void setUp() throws Exception {
+
+ MockitoAnnotations.initMocks(this);
+ helper.setUp();
+ ds = DatastoreServiceFactory.getDatastoreService();
+
+ // Set up some fake HTTP requests
+ when(mockRequest.getRequestURI()).thenReturn(FAKE_URL);
+ when(mockRequest.getParameter("guestbookName")).thenReturn( "default" );
+ when(mockRequest.getParameter("content")).thenReturn( testPhrase );
+
+ stringWriter = new StringWriter();
+ when(mockResponse.getWriter()).thenReturn(new PrintWriter(stringWriter));
+
+ servletUnderTest = new SignGuestbookServlet();
+
+ ObjectifyService.register(Guestbook.class);
+ ObjectifyService.register(Greeting.class);
+
+ closeable = ObjectifyService.begin();
+
+ cleanDatastore(ds, "default");
+ }
+
+ @After public void tearDown() {
+ cleanDatastore(ds, "default");
+ helper.tearDown();
+ closeable.close();
+ }
+
+ @Test
+ public void doPost_userNotLoggedIn() throws Exception {
+ servletUnderTest.doPost(mockRequest, mockResponse);
+
+ Query query = new Query("Greeting")
+ .setAncestor(new KeyFactory.Builder("Guestbook", "default").getKey());
+ PreparedQuery pq = ds.prepare(query);
+
+ Entity greeting = pq.asSingleEntity(); // Should only be one at this point.
+ assertEquals(greeting.getProperty("content"), testPhrase);
+ }
+
+}
diff --git a/appengine-java8/helloworld/README.md b/appengine-java8/helloworld/README.md
new file mode 100644
index 00000000000..204ac802e96
--- /dev/null
+++ b/appengine-java8/helloworld/README.md
@@ -0,0 +1,18 @@
+# Google App Engine Standard Environment Hello World Sample
+
+This sample demonstrates how to deploy an application on Google App Engine.
+
+See the [Google App Engine standard environment documentation][ae-docs] for more
+detailed instructions.
+
+[ae-docs]: https://cloud.google.com/appengine/docs/java/
+
+## Setup
+
+ gcloud init
+
+## Running locally
+ $ mvn appengine:run
+
+## Deploying
+ $ mvn appengine:deploy
diff --git a/appengine-java8/helloworld/jenkins.sh b/appengine-java8/helloworld/jenkins.sh
new file mode 100644
index 00000000000..22dfb5b12f4
--- /dev/null
+++ b/appengine-java8/helloworld/jenkins.sh
@@ -0,0 +1,21 @@
+#!/usr/bin/env bash
+
+# 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.
+
+set -xe
+
+mvn clean appengine:deploy -DskipTests=true
+
+curl -f "http://${GOOGLE_VERSION_ID}-dot-${GOOGLE_PROJECT_ID}.appspot.com/"
diff --git a/appengine-java8/helloworld/pom.xml b/appengine-java8/helloworld/pom.xml
new file mode 100644
index 00000000000..ad046383886
--- /dev/null
+++ b/appengine-java8/helloworld/pom.xml
@@ -0,0 +1,54 @@
+
+
+
+ 4.0.0
+ war
+ 1.0-SNAPSHOT
+ com.example.appengine
+ appengine-helloworld-j8
+
+ com.google.cloud
+ appengine-java8-samples
+ 1.0.0
+ ..
+
+
+
+ javax.servlet
+ javax.servlet-api
+ 3.1.0
+ jar
+ provided
+
+
+
+
+ ${project.build.directory}/${project.build.finalName}/WEB-INF/classes
+
+
+ com.google.cloud.tools
+ appengine-maven-plugin
+ 1.3.1
+
+ true
+ true
+
+
+
+
+
+
diff --git a/appengine-java8/helloworld/src/main/java/com/example/appengine/helloworld/HelloServlet.java b/appengine-java8/helloworld/src/main/java/com/example/appengine/helloworld/HelloServlet.java
new file mode 100644
index 00000000000..5868bf82f9d
--- /dev/null
+++ b/appengine-java8/helloworld/src/main/java/com/example/appengine/helloworld/HelloServlet.java
@@ -0,0 +1,36 @@
+/**
+ * Copyright 2015 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.helloworld;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+// [START example]
+@SuppressWarnings("serial")
+public class HelloServlet extends HttpServlet {
+
+ @Override
+ public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+ PrintWriter out = resp.getWriter();
+ out.println("Hello, world");
+ }
+}
+// [END example]
diff --git a/appengine-java8/helloworld/src/main/webapp/WEB-INF/appengine-web.xml b/appengine-java8/helloworld/src/main/webapp/WEB-INF/appengine-web.xml
new file mode 100644
index 00000000000..1f8086c81f4
--- /dev/null
+++ b/appengine-java8/helloworld/src/main/webapp/WEB-INF/appengine-web.xml
@@ -0,0 +1,19 @@
+
+
+
+
+ java8
+ true
+
+
diff --git a/appengine-java8/helloworld/src/main/webapp/WEB-INF/web.xml b/appengine-java8/helloworld/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 00000000000..1a1704104a2
--- /dev/null
+++ b/appengine-java8/helloworld/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,14 @@
+
+
+
+ hello
+ com.example.appengine.helloworld.HelloServlet
+
+
+ hello
+ /
+
+
diff --git a/appengine-java8/images/README.md b/appengine-java8/images/README.md
new file mode 100644
index 00000000000..88edaa825f2
--- /dev/null
+++ b/appengine-java8/images/README.md
@@ -0,0 +1,36 @@
+# Google App Engine Standard Environment Images Sample
+
+This sample demonstrates how to use the Images Java API.
+
+See the [Google App Engine standard environment documentation][ae-docs] for more
+detailed instructions.
+
+[ae-docs]: https://cloud.google.com/appengine/docs/java/
+
+## Modify the app
+
+Using the [Google Cloud SDK](https://cloud.google.com/sdk/) create a bucket
+
+ $ gsutil mb YOUR-PROJECT-ID.appspot.com
+
+* Edit `src/main/java/com/example/appengine/images/ImageServlet.java` and set your `bucket` name.
+
+## Running locally
+
+ This example uses the
+ [App Engine maven plugin](https://cloud.google.com/appengine/docs/java/tools/maven).
+ To run this sample locally:
+
+ $ mvn appengine:devserver
+
+ To see the results of the sample application, open
+ [localhost:8080](http://localhost:8080) in a web browser.
+
+
+## Deploying
+
+ In the following command, replace YOUR-PROJECT-ID with your
+ [Google Cloud Project ID](https://developers.google.com/console/help/new/#projectnumber)
+ and SOME-VERSION with a valid version number.
+
+ $ mvn appengine:update -Dappengine.appId=YOUR-PROJECT-ID -Dappengine.version=SOME-VERSION
diff --git a/appengine-java8/images/pom.xml b/appengine-java8/images/pom.xml
new file mode 100644
index 00000000000..c3cd74df480
--- /dev/null
+++ b/appengine-java8/images/pom.xml
@@ -0,0 +1,70 @@
+
+
+ 4.0.0
+ war
+ 1.0-SNAPSHOT
+ com.example.appengine
+ appengine-images-j8
+
+
+ 1.9.52
+
+
+
+ com.google.cloud
+ appengine-java8-samples
+ 1.0.0
+ ..
+
+
+
+
+ com.google.appengine
+ appengine-api-1.0-sdk
+ ${appengine.sdk.version}
+
+
+
+ com.google.appengine.tools
+ appengine-gcs-client
+ 0.6
+
+
+
+ javax.servlet
+ javax.servlet-api
+ 3.1.0
+ jar
+ provided
+
+
+
+
+ ${project.build.directory}/${project.build.finalName}/WEB-INF/classes
+
+
+ com.google.cloud.tools
+ appengine-maven-plugin
+ 1.3.1
+
+ true
+ true
+
+
+
+
+
diff --git a/appengine-java8/images/src/main/java/com/example/appengine/images/ImagesServlet.java b/appengine-java8/images/src/main/java/com/example/appengine/images/ImagesServlet.java
new file mode 100644
index 00000000000..2f0c1f9864a
--- /dev/null
+++ b/appengine-java8/images/src/main/java/com/example/appengine/images/ImagesServlet.java
@@ -0,0 +1,118 @@
+/**
+ * Copyright 2015 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.images;
+import com.google.appengine.api.blobstore.BlobKey;
+import com.google.appengine.api.blobstore.BlobstoreService;
+import com.google.appengine.api.blobstore.BlobstoreServiceFactory;
+import com.google.appengine.api.images.Image;
+import com.google.appengine.api.images.ImagesService;
+import com.google.appengine.api.images.ImagesServiceFactory;
+import com.google.appengine.api.images.Transform;
+import com.google.appengine.tools.cloudstorage.GcsFileOptions;
+import com.google.appengine.tools.cloudstorage.GcsFilename;
+import com.google.appengine.tools.cloudstorage.GcsService;
+import com.google.appengine.tools.cloudstorage.GcsServiceFactory;
+import com.google.appengine.tools.cloudstorage.RetryParams;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
+
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+// [START example]
+@SuppressWarnings("serial")
+public class ImagesServlet extends HttpServlet {
+ final String bucket = "YOUR-BUCKETNAME-HERE";
+
+ // [START gcs]
+ private final GcsService gcsService = GcsServiceFactory.createGcsService(new RetryParams.Builder()
+ .initialRetryDelayMillis(10)
+ .retryMaxAttempts(10)
+ .totalRetryPeriodMillis(15000)
+ .build());
+ // [END gcs]
+
+ @Override
+ public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+
+ //[START original_image]
+ // Read the image.jpg resource into a ByteBuffer.
+ FileInputStream fileInputStream = new FileInputStream(new File("WEB-INF/image.jpg"));
+ FileChannel fileChannel = fileInputStream.getChannel();
+ ByteBuffer byteBuffer = ByteBuffer.allocate((int)fileChannel.size());
+ fileChannel.read(byteBuffer);
+
+ byte[] imageBytes = byteBuffer.array();
+
+ // Write the original image to Cloud Storage
+ gcsService.createOrReplace(
+ new GcsFilename(bucket, "image.jpeg"),
+ new GcsFileOptions.Builder().mimeType("image/jpeg").build(),
+ ByteBuffer.wrap(imageBytes));
+ //[END original_image]
+
+ //[START resize]
+ // Get an instance of the imagesService we can use to transform images.
+ ImagesService imagesService = ImagesServiceFactory.getImagesService();
+
+ // Make an image directly from a byte array, and transform it.
+ Image image = ImagesServiceFactory.makeImage(imageBytes);
+ Transform resize = ImagesServiceFactory.makeResize(100, 50);
+ Image resizedImage = imagesService.applyTransform(resize, image);
+
+ // Write the transformed image back to a Cloud Storage object.
+ gcsService.createOrReplace(
+ new GcsFilename(bucket, "resizedImage.jpeg"),
+ new GcsFileOptions.Builder().mimeType("image/jpeg").build(),
+ ByteBuffer.wrap(resizedImage.getImageData()));
+ //[END resize]
+
+ //[START rotate]
+ // Make an image from a Cloud Storage object, and transform it.
+ BlobstoreService blobstoreService = BlobstoreServiceFactory.getBlobstoreService();
+ BlobKey blobKey = blobstoreService.createGsBlobKey("/gs/" + bucket + "/image.jpeg");
+ Image blobImage = ImagesServiceFactory.makeImageFromBlob(blobKey);
+ Transform rotate = ImagesServiceFactory.makeRotate(90);
+ Image rotatedImage = imagesService.applyTransform(rotate, blobImage);
+
+ // Write the transformed image back to a Cloud Storage object.
+ gcsService.createOrReplace(
+ new GcsFilename(bucket, "rotatedImage.jpeg"),
+ new GcsFileOptions.Builder().mimeType("image/jpeg").build(),
+ ByteBuffer.wrap(rotatedImage.getImageData()));
+ //[END rotate]
+
+ // Output some simple HTML to display the images we wrote to Cloud Storage
+ // in the browser.
+ PrintWriter out = resp.getWriter();
+ out.println("\n");
+ out.println("");
+ out.println("");
+ out.println("");
+ out.println("\n");
+ }
+}
+// [END example]
diff --git a/appengine-java8/images/src/main/webapp/WEB-INF/appengine-web.xml b/appengine-java8/images/src/main/webapp/WEB-INF/appengine-web.xml
new file mode 100644
index 00000000000..d64ab6aafb7
--- /dev/null
+++ b/appengine-java8/images/src/main/webapp/WEB-INF/appengine-web.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+ java8
+ true
+
diff --git a/appengine-java8/images/src/main/webapp/WEB-INF/image.jpg b/appengine-java8/images/src/main/webapp/WEB-INF/image.jpg
new file mode 100644
index 00000000000..3a60da2619d
Binary files /dev/null and b/appengine-java8/images/src/main/webapp/WEB-INF/image.jpg differ
diff --git a/appengine-java8/images/src/main/webapp/WEB-INF/web.xml b/appengine-java8/images/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 00000000000..8ccba622877
--- /dev/null
+++ b/appengine-java8/images/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,14 @@
+
+
+
+ images
+ com.example.appengine.images.ImagesServlet
+
+
+ images
+ /
+
+
diff --git a/appengine-java8/logs/README.md b/appengine-java8/logs/README.md
new file mode 100644
index 00000000000..ce1f661c796
--- /dev/null
+++ b/appengine-java8/logs/README.md
@@ -0,0 +1,18 @@
+# Users Authentication sample for Google App Engine
+
+This sample demonstrates how to use the [Logs API][log-docs] on [Google App
+Engine][ae-docs].
+
+[log-docs]: https://cloud.google.com/appengine/docs/java/logs/
+[ae-docs]: https://cloud.google.com/appengine/docs/java/
+
+## Running locally
+
+The Logs API only generates output for deployed apps, so this program should not be run locally.
+
+## Deploying
+
+This example uses the
+[Cloud SDK maven plugin](https://cloud.google.com/appengine/docs/java/tools/using-maven).
+
+ mvn appengine:deploy
diff --git a/appengine-java8/logs/pom.xml b/appengine-java8/logs/pom.xml
new file mode 100644
index 00000000000..3746dd6b2b2
--- /dev/null
+++ b/appengine-java8/logs/pom.xml
@@ -0,0 +1,76 @@
+
+
+
+ 4.0.0
+ war
+ 1.0-SNAPSHOT
+ com.example.appengine
+ appengine-logs-j8
+
+
+ com.google.cloud
+ appengine-java8-samples
+ 1.0.0
+ ..
+
+
+
+ com.google.appengine
+ appengine-api-1.0-sdk
+ ${appengine.sdk.version}
+
+
+ com.google.guava
+ guava
+ 20.0
+
+
+
+ javax.servlet
+ javax.servlet-api
+ 3.1.0
+ jar
+ provided
+
+
+
+ org.json
+ json
+ 20160810
+
+
+ joda-time
+ joda-time
+ 2.9.9
+
+
+
+
+ ${project.build.directory}/${project.build.finalName}/WEB-INF/classes
+
+
+ com.google.cloud.tools
+ appengine-maven-plugin
+ 1.3.1
+
+ true
+ true
+
+
+
+
+
diff --git a/appengine-java8/logs/src/main/java/com/example/appengine/logs/LogsServlet.java b/appengine-java8/logs/src/main/java/com/example/appengine/logs/LogsServlet.java
new file mode 100644
index 00000000000..a889763e989
--- /dev/null
+++ b/appengine-java8/logs/src/main/java/com/example/appengine/logs/LogsServlet.java
@@ -0,0 +1,97 @@
+/* Copyright 2016 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.
+ */
+// [START logs_API_example]
+package com.example.appengine.logs;
+
+import com.google.appengine.api.log.AppLogLine;
+import com.google.appengine.api.log.LogQuery;
+import com.google.appengine.api.log.LogServiceFactory;
+import com.google.appengine.api.log.RequestLogs;
+
+import org.joda.time.DateTime;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+
+// Get request logs along with their app log lines and display them 5 at
+// a time, using a Next link to cycle through to the next 5.
+public class LogsServlet extends HttpServlet {
+ @Override
+ public void doGet(HttpServletRequest req, HttpServletResponse resp)
+ throws IOException {
+
+ resp.setContentType("text/html");
+ PrintWriter writer = resp.getWriter();
+ writer.println("");
+ writer.println("");
+ writer.println("App Engine Logs Sample");
+
+ // We use this to break out of our iteration loop, limiting record
+ // display to 5 request logs at a time.
+ int limit = 5;
+
+ // This retrieves the offset from the Next link upon user click.
+ String offset = req.getParameter("offset");
+
+ // We want the App logs for each request log
+ LogQuery query = LogQuery.Builder.withDefaults();
+ query.includeAppLogs(true);
+
+ // Set the offset value retrieved from the Next link click.
+ if (offset != null) {
+ query.offset(offset);
+ }
+
+ // This gets filled from the last request log in the iteration
+ String lastOffset = null;
+ int count = 0;
+
+ // Display a few properties of each request log.
+ for (RequestLogs record : LogServiceFactory.getLogService().fetch(query)) {
+ writer.println(" REQUEST LOG ");
+ DateTime reqTime = new DateTime(record.getStartTimeUsec() / 1000);
+ writer.println("IP: " + record.getIp() + " ");
+ writer.println("Method: " + record.getMethod() + " ");
+ writer.println("Resource " + record.getResource() + " ");
+ writer.println(String.format(" Date: %s", reqTime.toString()));
+
+ lastOffset = record.getOffset();
+
+ // Display all the app logs for each request log.
+ for (AppLogLine appLog : record.getAppLogLines()) {
+ writer.println(" " + "APPLICATION LOG" + " ");
+ DateTime appTime = new DateTime(appLog.getTimeUsec() / 1000);
+ writer.println(String.format(" Date: %s", appTime.toString()));
+ writer.println(" Level: " + appLog.getLogLevel() + " ");
+ writer.println("Message: " + appLog.getLogMessage() + "
");
+ }
+
+ if (++count >= limit) {
+ break;
+ }
+ }
+
+ // When the user clicks this link, the offset is processed in the
+ // GET handler and used to cycle through to the next 5 request logs.
+ writer.println(String.format(" Next", lastOffset));
+ }
+}
+// [END logs_API_example]
+
diff --git a/appengine-java8/logs/src/main/webapp/WEB-INF/appengine-web.xml b/appengine-java8/logs/src/main/webapp/WEB-INF/appengine-web.xml
new file mode 100644
index 00000000000..202e048ae2e
--- /dev/null
+++ b/appengine-java8/logs/src/main/webapp/WEB-INF/appengine-web.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+ java8
+ true
+
+
+
+
diff --git a/appengine-java8/logs/src/main/webapp/WEB-INF/logging.properties b/appengine-java8/logs/src/main/webapp/WEB-INF/logging.properties
new file mode 100644
index 00000000000..3e7f85b9dc1
--- /dev/null
+++ b/appengine-java8/logs/src/main/webapp/WEB-INF/logging.properties
@@ -0,0 +1,14 @@
+# A default java.util.logging configuration.
+# (All App Engine logging is through java.util.logging by default).
+#
+# To use this configuration, copy it into your application's WEB-INF
+# folder and add the following to your appengine-web.xml:
+#
+#
+#
+#
+#
+
+# Set the default logging level for all loggers to WARNING
+.level = WARNING
+
diff --git a/appengine-java8/logs/src/main/webapp/WEB-INF/web.xml b/appengine-java8/logs/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 00000000000..3b156c9b98c
--- /dev/null
+++ b/appengine-java8/logs/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+ logs
+ com.example.appengine.logs.LogsServlet
+
+
+ logs
+ /
+
+
diff --git a/appengine-java8/mail/README.md b/appengine-java8/mail/README.md
new file mode 100644
index 00000000000..149eb8b485b
--- /dev/null
+++ b/appengine-java8/mail/README.md
@@ -0,0 +1,21 @@
+# JavaMail API Email Sample for Google App Engine Standard Environment
+
+This sample demonstrates how to use [JavaMail][javamail-api] on [Google App Engine
+standard environment][ae-docs].
+
+See the [sample application documentaion][sample-docs] for more detailed
+instructions.
+
+[ae-docs]: https://cloud.google.com/appengine/docs/java/
+[javamail-api]: http://javamail.java.net/
+[sample-docs]: https://cloud.google.com/appengine/docs/java/mail/
+
+## Setup
+
+ gcloud init
+
+## Running locally
+ $ mvn appengine:run
+
+## Deploying
+ $ mvn appengine:deploy
diff --git a/appengine-java8/mail/pom.xml b/appengine-java8/mail/pom.xml
new file mode 100644
index 00000000000..c696580c6d6
--- /dev/null
+++ b/appengine-java8/mail/pom.xml
@@ -0,0 +1,65 @@
+
+
+ 4.0.0
+ war
+ 1.0-SNAPSHOT
+ com.example.appengine
+ appengine-mail
+
+
+
+ com.google.cloud
+ appengine-doc-samples
+ 1.0.0
+ ..
+
+
+
+
+ javax.servlet
+ javax.servlet-api
+ 3.1.0
+ jar
+ provided
+
+
+
+ com.google.appengine
+ appengine-api-1.0-sdk
+
+
+ javax.mail
+ mail
+ 1.4.7
+
+
+
+
+ ${project.build.directory}/${project.build.finalName}/WEB-INF/classes
+
+
+ com.google.cloud.tools
+ appengine-maven-plugin
+ 1.3.1
+
+ true
+ true
+
+
+
+
+
diff --git a/appengine-java8/mail/src/main/java/com/example/appengine/mail/BounceHandlerServlet.java b/appengine-java8/mail/src/main/java/com/example/appengine/mail/BounceHandlerServlet.java
new file mode 100644
index 00000000000..51f4536ac8a
--- /dev/null
+++ b/appengine-java8/mail/src/main/java/com/example/appengine/mail/BounceHandlerServlet.java
@@ -0,0 +1,54 @@
+/**
+ * Copyright 2016 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.mail;
+
+// [START bounce_handler_servlet]
+import com.google.appengine.api.mail.BounceNotification;
+import com.google.appengine.api.mail.BounceNotificationParser;
+
+import java.io.IOException;
+import java.util.logging.Logger;
+import javax.mail.MessagingException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+public class BounceHandlerServlet extends HttpServlet {
+
+ private static final Logger log = Logger.getLogger(BounceHandlerServlet.class.getName());
+
+ @Override
+ public void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+ try {
+ BounceNotification bounce = BounceNotificationParser.parse(req);
+ log.warning("Bounced email notification.");
+ // The following data is available in a BounceNotification object
+ // bounce.getOriginal().getFrom()
+ // bounce.getOriginal().getTo()
+ // bounce.getOriginal().getSubject()
+ // bounce.getOriginal().getText()
+ // bounce.getNotification().getFrom()
+ // bounce.getNotification().getTo()
+ // bounce.getNotification().getSubject()
+ // bounce.getNotification().getText()
+ // ...
+ } catch (MessagingException e) {
+ // ...
+ }
+ }
+}
+// [END bounce_handler_servlet]
diff --git a/appengine-java8/mail/src/main/java/com/example/appengine/mail/HandleDiscussionEmail.java b/appengine-java8/mail/src/main/java/com/example/appengine/mail/HandleDiscussionEmail.java
new file mode 100644
index 00000000000..b673497f389
--- /dev/null
+++ b/appengine-java8/mail/src/main/java/com/example/appengine/mail/HandleDiscussionEmail.java
@@ -0,0 +1,43 @@
+/**
+ * Copyright 2016 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.mail;
+
+import javax.mail.internet.MimeMessage;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.ServletException;
+import java.util.logging.Logger;
+import java.util.regex.Matcher;
+
+// [START example]
+public class HandleDiscussionEmail extends MailHandlerBase {
+
+ private static final Logger log = Logger.getLogger(HandleDiscussionEmail.class.getName());
+ public HandleDiscussionEmail() { super("discuss-(.*)@(.*)"); }
+
+ @Override
+ protected boolean processMessage(HttpServletRequest req, HttpServletResponse res)
+ throws ServletException
+ {
+ log.info("Received e-mail sent to discuss list.");
+ MimeMessage msg = getMessageFromRequest(req);
+ Matcher match = getMatcherFromRequest(req);
+ // ...
+ return true;
+ }
+}
+// [END example]
diff --git a/appengine-java8/mail/src/main/java/com/example/appengine/mail/MailHandlerBase.java b/appengine-java8/mail/src/main/java/com/example/appengine/mail/MailHandlerBase.java
new file mode 100644
index 00000000000..dd16582bb39
--- /dev/null
+++ b/appengine-java8/mail/src/main/java/com/example/appengine/mail/MailHandlerBase.java
@@ -0,0 +1,116 @@
+/**
+ * Copyright 2016 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.mail;
+
+import javax.mail.internet.MimeMessage;
+import javax.mail.MessagingException;
+import javax.mail.Session;
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import java.io.IOException;
+import java.util.Properties;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Base class for handling the filtering of incoming emails in App Engine.
+ */
+// [START example]
+public abstract class MailHandlerBase implements Filter {
+
+ private Pattern pattern = null;
+
+ protected MailHandlerBase(String pattern) {
+ if (pattern == null || pattern.trim().length() == 0)
+ {
+ throw new IllegalArgumentException("Expected non-empty regular expression");
+ }
+ this.pattern = Pattern.compile("/_ah/mail/"+pattern);
+ }
+
+ @Override public void init(FilterConfig config) throws ServletException { }
+
+ @Override public void destroy() { }
+
+ /**
+ * Process the message. A message will only be passed to this method
+ * if the servletPath of the message (typically the recipient for
+ * appengine) satisfies the pattern passed to the constructor. If
+ * the implementation returns false, control is passed
+ * to the next filter in the chain. If the implementation returns
+ * true, the filter chain is terminated.
+ *
+ * The Matcher for the pattern can be retrieved via
+ * getMatcherFromRequest (e.g. if groups are used in the pattern).
+ */
+ protected abstract boolean processMessage(HttpServletRequest req, HttpServletResponse res) throws ServletException;
+
+ @Override
+ public void doFilter(ServletRequest sreq, ServletResponse sres, FilterChain chain)
+ throws IOException, ServletException {
+
+ HttpServletRequest req = (HttpServletRequest) sreq;
+ HttpServletResponse res = (HttpServletResponse) sres;
+
+ MimeMessage message = getMessageFromRequest(req);
+ Matcher m = applyPattern(req);
+
+ if (m != null && processMessage(req, res)) {
+ return;
+ }
+
+ chain.doFilter(req, res); // Try the next one
+
+ }
+
+ private Matcher applyPattern(HttpServletRequest req) {
+ Matcher m = pattern.matcher(req.getServletPath());
+ if (!m.matches()) m = null;
+
+ req.setAttribute("matcher", m);
+ return m;
+ }
+
+ protected Matcher getMatcherFromRequest(ServletRequest req) {
+ return (Matcher) req.getAttribute("matcher");
+ }
+
+ protected MimeMessage getMessageFromRequest(ServletRequest req) throws ServletException {
+ MimeMessage message = (MimeMessage) req.getAttribute("mimeMessage");
+ if (message == null) {
+ try {
+ Properties props = new Properties();
+ Session session = Session.getDefaultInstance(props, null);
+ message = new MimeMessage(session, req.getInputStream());
+ req.setAttribute("mimeMessage", message);
+
+ } catch (MessagingException e) {
+ throw new ServletException("Error processing inbound message", e);
+ } catch (IOException e) {
+ throw new ServletException("Error processing inbound message", e);
+ }
+ }
+ return message;
+ }
+}
+// [END example]
diff --git a/appengine-java8/mail/src/main/java/com/example/appengine/mail/MailHandlerServlet.java b/appengine-java8/mail/src/main/java/com/example/appengine/mail/MailHandlerServlet.java
new file mode 100644
index 00000000000..0262d41c95a
--- /dev/null
+++ b/appengine-java8/mail/src/main/java/com/example/appengine/mail/MailHandlerServlet.java
@@ -0,0 +1,49 @@
+/**
+ * Copyright 2016 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.mail;
+
+// [START mail_handler_servlet]
+import java.io.IOException;
+import java.util.logging.Logger;
+import java.util.Properties;
+
+import javax.mail.MessagingException;
+import javax.mail.Session;
+import javax.mail.internet.MimeMessage;
+
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+public class MailHandlerServlet extends HttpServlet {
+
+ private static final Logger log = Logger.getLogger(MailHandlerServlet.class.getName());
+
+ @Override
+ public void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+ Properties props = new Properties();
+ Session session = Session.getDefaultInstance(props, null);
+ try {
+ MimeMessage message = new MimeMessage(session, req.getInputStream());
+ log.info("Received mail message.");
+ } catch (MessagingException e) {
+ // ...
+ }
+ // ...
+ }
+}
+// [END mail_handler_servlet]
diff --git a/appengine-java8/mail/src/main/java/com/example/appengine/mail/MailServlet.java b/appengine-java8/mail/src/main/java/com/example/appengine/mail/MailServlet.java
new file mode 100644
index 00000000000..a51fe854c6a
--- /dev/null
+++ b/appengine-java8/mail/src/main/java/com/example/appengine/mail/MailServlet.java
@@ -0,0 +1,125 @@
+/**
+ * Copyright 2016 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.mail;
+
+// [START simple_includes]
+import java.io.IOException;
+import java.util.Properties;
+import javax.mail.Message;
+import javax.mail.MessagingException;
+import javax.mail.Session;
+import javax.mail.Transport;
+import javax.mail.internet.AddressException;
+import javax.mail.internet.InternetAddress;
+import javax.mail.internet.MimeMessage;
+// [END simple_includes]
+
+// [START multipart_includes]
+import java.io.InputStream;
+import java.io.ByteArrayInputStream;
+import java.io.UnsupportedEncodingException;
+import javax.activation.DataHandler;
+import javax.mail.Multipart;
+import javax.mail.internet.MimeBodyPart;
+import javax.mail.internet.MimeMultipart;
+// [END multipart_includes]
+
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+@SuppressWarnings("serial")
+public class MailServlet extends HttpServlet {
+
+ @Override
+ public void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+ String type = req.getParameter("type");
+ if (type != null && type.equals("multipart")) {
+ resp.getWriter().print("Sending HTML email with attachment.");
+ sendMultipartMail();
+ } else {
+ resp.getWriter().print("Sending simple email.");
+ sendSimpleMail();
+ }
+ }
+
+ private void sendSimpleMail() {
+ // [START simple_example]
+ Properties props = new Properties();
+ Session session = Session.getDefaultInstance(props, null);
+
+ try {
+ Message msg = new MimeMessage(session);
+ msg.setFrom(new InternetAddress("admin@example.com", "Example.com Admin"));
+ msg.addRecipient(Message.RecipientType.TO,
+ new InternetAddress("user@example.com", "Mr. User"));
+ msg.setSubject("Your Example.com account has been activated");
+ msg.setText("This is a test");
+ Transport.send(msg);
+ } catch (AddressException e) {
+ // ...
+ } catch (MessagingException e) {
+ // ...
+ } catch (UnsupportedEncodingException e) {
+ // ...
+ }
+ // [END simple_example]
+ }
+
+ private void sendMultipartMail() {
+ Properties props = new Properties();
+ Session session = Session.getDefaultInstance(props, null);
+
+ String msgBody = "...";
+
+ try {
+ Message msg = new MimeMessage(session);
+ msg.setFrom(new InternetAddress("admin@example.com", "Example.com Admin"));
+ msg.addRecipient(Message.RecipientType.TO,
+ new InternetAddress("user@example.com", "Mr. User"));
+ msg.setSubject("Your Example.com account has been activated");
+ msg.setText(msgBody);
+
+ // [START multipart_example]
+ String htmlBody = ""; // ...
+ byte[] attachmentData = null; // ...
+ Multipart mp = new MimeMultipart();
+
+ MimeBodyPart htmlPart = new MimeBodyPart();
+ htmlPart.setContent(htmlBody, "text/html");
+ mp.addBodyPart(htmlPart);
+
+ MimeBodyPart attachment = new MimeBodyPart();
+ InputStream attachmentDataStream = new ByteArrayInputStream(attachmentData);
+ attachment.setFileName("manual.pdf");
+ attachment.setContent(attachmentDataStream, "application/pdf");
+ mp.addBodyPart(attachment);
+
+ msg.setContent(mp);
+ // [END multipart_example]
+
+ Transport.send(msg);
+
+ } catch (AddressException e) {
+ // ...
+ } catch (MessagingException e) {
+ // ...
+ } catch (UnsupportedEncodingException e) {
+ // ...
+ }
+ }
+}
diff --git a/appengine-java8/mail/src/main/webapp/WEB-INF/appengine-web.xml b/appengine-java8/mail/src/main/webapp/WEB-INF/appengine-web.xml
new file mode 100644
index 00000000000..33b8f0522e6
--- /dev/null
+++ b/appengine-java8/mail/src/main/webapp/WEB-INF/appengine-web.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+ java8
+ true
+
+
+
+
+ mail
+
+ mail_bounce
+
+
+
diff --git a/appengine-java8/mail/src/main/webapp/WEB-INF/logging.properties b/appengine-java8/mail/src/main/webapp/WEB-INF/logging.properties
new file mode 100644
index 00000000000..45b39a32093
--- /dev/null
+++ b/appengine-java8/mail/src/main/webapp/WEB-INF/logging.properties
@@ -0,0 +1,27 @@
+#
+# Copyright 2016 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.
+#
+# A default java.util.logging configuration.
+# (All App Engine logging is through java.util.logging by default).
+#
+# To use this configuration, copy it into your application's WEB-INF
+# folder and add the following to your appengine-web.xml:
+#
+#
+#
+#
+#
+# Set the default logging level for all loggers to WARNING
+.level=INFO
diff --git a/appengine-java8/mail/src/main/webapp/WEB-INF/web.xml b/appengine-java8/mail/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 00000000000..393f67066ad
--- /dev/null
+++ b/appengine-java8/mail/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,79 @@
+
+
+
+
+ mail
+ com.example.appengine.mail.MailServlet
+
+
+ mail
+ /
+
+
+
+
+ HandleDiscussionEmail
+ com.example.appengine.mail.HandleDiscussionEmail
+
+
+ HandleDiscussionEmail
+ /_ah/mail/*
+
+
+
+
+
+
+
+
+ bouncehandler
+ com.example.appengine.mail.BounceHandlerServlet
+
+
+ bouncehandler
+ /_ah/bounce
+
+
+
+ bounce
+ /_ah/bounce
+
+
+ admin
+
+
+
+
diff --git a/appengine-java8/mailgun/README.md b/appengine-java8/mailgun/README.md
new file mode 100644
index 00000000000..582771ddb34
--- /dev/null
+++ b/appengine-java8/mailgun/README.md
@@ -0,0 +1,11 @@
+# Java Mailgun Email Sample for Google App Engine Standard Environment
+
+This sample demonstrates how to use [Mailgun][mailgun-api] on [Google App Engine
+standard environment][ae-docs].
+
+See the [sample application documentaion][sample-docs] for more detailed
+instructions.
+
+[ae-docs]: https://cloud.google.com/appengine/docs/java/
+[mailgun-api]: https://documentation.mailgun.com/
+[sample-docs]: https://cloud.google.com/appengine/docs/java/mail/mailgun
diff --git a/appengine-java8/mailgun/pom.xml b/appengine-java8/mailgun/pom.xml
new file mode 100644
index 00000000000..87576e1086f
--- /dev/null
+++ b/appengine-java8/mailgun/pom.xml
@@ -0,0 +1,69 @@
+
+
+ 4.0.0
+ war
+ 1.0-SNAPSHOT
+ com.example.appengine
+ appengine-mailgun-j8
+
+ com.google.cloud
+ appengine-java8-samples
+ 1.0.0
+ ..
+
+
+
+ javax.servlet
+ javax.servlet-api
+ 3.1.0
+ jar
+ provided
+
+
+
+ com.sun.jersey
+ jersey-core
+ 1.19.3
+
+
+ com.sun.jersey
+ jersey-client
+ 1.19.3
+
+
+ com.sun.jersey.contribs
+ jersey-multipart
+ 1.19.3
+
+
+
+
+
+ ${project.build.directory}/${project.build.finalName}/WEB-INF/classes
+
+
+ com.google.cloud.tools
+ appengine-maven-plugin
+ 1.3.1
+
+ true
+ true
+
+
+
+
+
diff --git a/appengine-java8/mailgun/src/main/java/com/example/appengine/mailgun/MailgunServlet.java b/appengine-java8/mailgun/src/main/java/com/example/appengine/mailgun/MailgunServlet.java
new file mode 100644
index 00000000000..ad30b5e983a
--- /dev/null
+++ b/appengine-java8/mailgun/src/main/java/com/example/appengine/mailgun/MailgunServlet.java
@@ -0,0 +1,92 @@
+/**
+ * Copyright 2015 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.mailgun;
+
+import com.sun.jersey.api.client.Client;
+import com.sun.jersey.api.client.ClientResponse;
+import com.sun.jersey.api.client.WebResource;
+import com.sun.jersey.api.client.filter.HTTPBasicAuthFilter;
+import com.sun.jersey.core.util.MultivaluedMapImpl;
+import com.sun.jersey.multipart.FormDataMultiPart;
+import com.sun.jersey.multipart.file.FileDataBodyPart;
+
+import java.io.File;
+import java.io.IOException;
+
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.ws.rs.core.MediaType;
+
+// [START example]
+@SuppressWarnings("serial")
+public class MailgunServlet extends HttpServlet {
+
+ private static final String MAILGUN_DOMAIN_NAME = System.getenv("MAILGUN_DOMAIN_NAME");
+ private static final String MAILGUN_API_KEY = System.getenv("MAILGUN_API_KEY");
+
+ @Override
+ public void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+ String type = req.getParameter("submit");
+ String recipient = req.getParameter("to");
+ ClientResponse clientResponse;
+ if (type.equals("Send simple email")) {
+ clientResponse = sendSimpleMessage(recipient);
+ } else {
+ clientResponse = sendComplexMessage(recipient);
+ }
+ if (clientResponse.getStatus() == 200) {
+ resp.getWriter().print("Email sent.");
+ }
+ }
+
+ // [START simple]
+ private ClientResponse sendSimpleMessage(String recipient) {
+ Client client = Client.create();
+ client.addFilter(new HTTPBasicAuthFilter("api", MAILGUN_API_KEY));
+ WebResource webResource = client.resource("https://api.mailgun.net/v3/" + MAILGUN_DOMAIN_NAME
+ + "/messages");
+ MultivaluedMapImpl formData = new MultivaluedMapImpl();
+ formData.add("from", "Mailgun User ");
+ formData.add("to", recipient);
+ formData.add("subject", "Simple Mailgun Example");
+ formData.add("text", "Plaintext content");
+ return webResource.type(MediaType.APPLICATION_FORM_URLENCODED).post(ClientResponse.class,
+ formData);
+ }
+ // [END simple]
+
+ // [START complex]
+ private ClientResponse sendComplexMessage(String recipient) {
+ Client client = Client.create();
+ client.addFilter(new HTTPBasicAuthFilter("api", MAILGUN_API_KEY));
+ WebResource webResource = client.resource("https://api.mailgun.net/v3/" + MAILGUN_DOMAIN_NAME
+ + "/messages");
+ FormDataMultiPart formData = new FormDataMultiPart();
+ formData.field("from", "Mailgun User ");
+ formData.field("to", recipient);
+ formData.field("subject", "Complex Mailgun Example");
+ formData.field("html", "HTML content");
+ ClassLoader classLoader = getClass().getClassLoader();
+ File txtFile = new File(classLoader.getResource("example-attachment.txt").getFile());
+ formData.bodyPart(new FileDataBodyPart("attachment", txtFile, MediaType.TEXT_PLAIN_TYPE));
+ return webResource.type(MediaType.MULTIPART_FORM_DATA_TYPE)
+ .post(ClientResponse.class, formData);
+ }
+ // [END complex]
+}
+// [END example]
diff --git a/appengine-java8/mailgun/src/main/webapp/WEB-INF/appengine-web.xml b/appengine-java8/mailgun/src/main/webapp/WEB-INF/appengine-web.xml
new file mode 100644
index 00000000000..dd000a989ec
--- /dev/null
+++ b/appengine-java8/mailgun/src/main/webapp/WEB-INF/appengine-web.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+ java8
+ true
+
+
+
+
+
+
diff --git a/appengine-java8/mailgun/src/main/webapp/WEB-INF/web.xml b/appengine-java8/mailgun/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 00000000000..9f22b168016
--- /dev/null
+++ b/appengine-java8/mailgun/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+ mailgun
+ com.example.appengine.mailgun.MailgunServlet
+
+
+ mailgun
+ /send/email
+
+
+
diff --git a/appengine-java8/mailgun/src/main/webapp/index.html b/appengine-java8/mailgun/src/main/webapp/index.html
new file mode 100644
index 00000000000..9ec44eaad7c
--- /dev/null
+++ b/appengine-java8/mailgun/src/main/webapp/index.html
@@ -0,0 +1,27 @@
+
+
+
+
+ Mailgun on Google App Engine Managed VMs
+
+
+
+
+
+
+
diff --git a/appengine-java8/mailjet/README.md b/appengine-java8/mailjet/README.md
new file mode 100644
index 00000000000..0a9bddaaac7
--- /dev/null
+++ b/appengine-java8/mailjet/README.md
@@ -0,0 +1,15 @@
+# Mailjet sample for Google App Engine
+This sample demonstrates how to use [Mailjet](https://www.mailjet.com/) on Google Managed VMs to
+send emails from a verified sender you own.
+
+## Setup
+1. Before using, ensure the address you plan to send from has been verified in Mailjet.
+
+## Running locally
+ $ export MAILJET_API_KEY=[your mailjet api key]
+ $ export MAILJET_SECRET_KEY=[your mailjet secret key]
+ $ mvn clean appengine:devserver
+
+## Deploying
+1. Edit the environment variables in the appengine-web.xml with the appropriate Mailjet values.
+ $ mvn clean appengine:update
diff --git a/appengine-java8/mailjet/pom.xml b/appengine-java8/mailjet/pom.xml
new file mode 100644
index 00000000000..a758834fdb8
--- /dev/null
+++ b/appengine-java8/mailjet/pom.xml
@@ -0,0 +1,79 @@
+
+
+ 4.0.0
+ war
+ 1.0-SNAPSHOT
+ com.example.appengine
+ appengine-mailjet-j8
+
+ com.google.cloud
+ appengine-java8-samples
+ 1.0.0
+ ..
+
+
+ 4.0.5
+
+
+
+ com.mailjet
+ mailjet-client
+ ${mailjet.version}
+
+
+
+ javax.servlet
+ javax.servlet-api
+ 3.1.0
+ jar
+ provided
+
+
+
+
+ com.sun.jersey
+ jersey-core
+ 1.19.3
+
+
+ com.sun.jersey
+ jersey-client
+ 1.19.3
+
+
+ com.sun.jersey.contribs
+ jersey-multipart
+ 1.19.3
+
+
+
+
+
+ ${project.build.directory}/${project.build.finalName}/WEB-INF/classes
+
+
+ com.google.cloud.tools
+ appengine-maven-plugin
+ 1.3.1
+
+ true
+ true
+
+
+
+
+
diff --git a/appengine-java8/mailjet/src/main/java/com/example/appengine/mailjet/MailjetServlet.java b/appengine-java8/mailjet/src/main/java/com/example/appengine/mailjet/MailjetServlet.java
new file mode 100644
index 00000000000..e9922a71438
--- /dev/null
+++ b/appengine-java8/mailjet/src/main/java/com/example/appengine/mailjet/MailjetServlet.java
@@ -0,0 +1,73 @@
+/**
+ * Copyright 2016 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.
+ */
+// [START mailjet_imports]
+package com.example.appengine.mailjet;
+
+import com.mailjet.client.MailjetClient;
+import com.mailjet.client.MailjetRequest;
+import com.mailjet.client.MailjetResponse;
+import com.mailjet.client.errors.MailjetException;
+import com.mailjet.client.errors.MailjetSocketTimeoutException;
+import com.mailjet.client.resource.Email;
+// [END mailjet_imports]
+
+import org.json.JSONArray;
+import org.json.JSONObject;
+
+import java.io.IOException;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+// [START app]
+@SuppressWarnings("serial")
+public class MailjetServlet extends HttpServlet {
+ private static final String MAILJET_API_KEY = System.getenv("MAILJET_API_KEY");
+ private static final String MAILJET_SECRET_KEY = System.getenv("MAILJET_SECRET_KEY");
+ private MailjetClient client = new MailjetClient(MAILJET_API_KEY, MAILJET_SECRET_KEY);
+
+ @Override
+ public void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException,
+ ServletException {
+ String recipient = req.getParameter("to");
+ String sender = req.getParameter("from");
+
+ MailjetRequest email = new MailjetRequest(Email.resource)
+ .property(Email.FROMEMAIL, sender)
+ .property(Email.FROMNAME, "pandora")
+ .property(Email.SUBJECT, "Your email flight plan!")
+ .property(Email.TEXTPART,
+ "Dear passenger, welcome to Mailjet! May the delivery force be with you!")
+ .property(Email.HTMLPART,
+ "
Dear passenger, welcome to Mailjet!
May the delivery force be with you!")
+ .property(Email.RECIPIENTS, new JSONArray().put(new JSONObject().put("Email", recipient)));
+
+ try {
+ // trigger the API call
+ MailjetResponse response = client.post(email);
+ // Read the response data and status
+ resp.getWriter().print(response.getStatus());
+ resp.getWriter().print(response.getData());
+ } catch (MailjetException e) {
+ throw new ServletException("Mailjet Exception", e);
+ } catch (MailjetSocketTimeoutException e) {
+ throw new ServletException("Mailjet socket timed out", e);
+ }
+ }
+}
+// [END app]
diff --git a/appengine-java8/mailjet/src/main/webapp/WEB-INF/appengine-web.xml b/appengine-java8/mailjet/src/main/webapp/WEB-INF/appengine-web.xml
new file mode 100644
index 00000000000..2be13ecdab1
--- /dev/null
+++ b/appengine-java8/mailjet/src/main/webapp/WEB-INF/appengine-web.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+ java8
+ true
+// [START env_variables]
+
+
+
+
+// [END env_variables]
+
diff --git a/appengine-java8/mailjet/src/main/webapp/WEB-INF/web.xml b/appengine-java8/mailjet/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 00000000000..9d906fce8f8
--- /dev/null
+++ b/appengine-java8/mailjet/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+ mailjet
+ com.example.appengine.mailjet.MailjetServlet
+
+
+ mailjet
+ /send/email
+
+
diff --git a/appengine-java8/mailjet/src/main/webapp/index.html b/appengine-java8/mailjet/src/main/webapp/index.html
new file mode 100644
index 00000000000..10f5dcc59dd
--- /dev/null
+++ b/appengine-java8/mailjet/src/main/webapp/index.html
@@ -0,0 +1,27 @@
+
+
+
+
+ Mailgun on Google App Engine Managed VMs
+
+
+
+
+
+
+
diff --git a/appengine-java8/memcache/pom.xml b/appengine-java8/memcache/pom.xml
new file mode 100644
index 00000000000..e9c5c27dc8e
--- /dev/null
+++ b/appengine-java8/memcache/pom.xml
@@ -0,0 +1,68 @@
+
+
+ 4.0.0
+ war
+ 1.0-SNAPSHOT
+ com.example.appengine
+ appengine-memcache-j8
+
+
+ appengine-java8-samples
+ com.google.cloud
+ 1.0.0
+ ..
+
+
+
+
+ javax.servlet
+ javax.servlet-api
+ 3.1.0
+ jar
+ provided
+
+
+
+
+
+ com.google.appengine
+ appengine-api-1.0-sdk
+ ${appengine.sdk.version}
+
+
+ com.googlecode.xmemcached
+ xmemcached
+ 2.3.1
+
+
+
+
+
+ ${project.build.directory}/${project.build.finalName}/WEB-INF/classes
+
+
+ com.google.cloud.tools
+ appengine-maven-plugin
+ 1.3.1
+
+ true
+ true
+
+
+
+
+
diff --git a/appengine-java8/memcache/src/main/java/com/example/appengine/memcache/MemcacheAsyncCacheServlet.java b/appengine-java8/memcache/src/main/java/com/example/appengine/memcache/MemcacheAsyncCacheServlet.java
new file mode 100644
index 00000000000..d1960e06cb5
--- /dev/null
+++ b/appengine-java8/memcache/src/main/java/com/example/appengine/memcache/MemcacheAsyncCacheServlet.java
@@ -0,0 +1,75 @@
+/**
+ * Copyright 2016 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.memcache;
+
+import com.google.appengine.api.memcache.AsyncMemcacheService;
+import com.google.appengine.api.memcache.ErrorHandlers;
+import com.google.appengine.api.memcache.MemcacheServiceFactory;
+
+import java.io.IOException;
+import java.math.BigInteger;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.logging.Level;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+@SuppressWarnings("serial")
+public class MemcacheAsyncCacheServlet extends HttpServlet {
+
+ @Override
+ public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException,
+ ServletException {
+ String path = req.getRequestURI();
+ if (path.startsWith("/favicon.ico")) {
+ return; // ignore the request for favicon.ico
+ }
+
+ // [START example]
+ AsyncMemcacheService asyncCache = MemcacheServiceFactory.getAsyncMemcacheService();
+ asyncCache.setErrorHandler(ErrorHandlers.getConsistentLogAndContinue(Level.INFO));
+ String key = "count-async";
+ byte[] value;
+ long count = 1;
+ Future
+
+
+
+
+
+
\ No newline at end of file
diff --git a/appengine-java8/users/README.md b/appengine-java8/users/README.md
new file mode 100644
index 00000000000..e58938a662a
--- /dev/null
+++ b/appengine-java8/users/README.md
@@ -0,0 +1,20 @@
+# Users Authentication sample for Google App Engine
+
+This sample demonstrates how to use the [Users API][appid] on [Google App
+Engine][ae-docs].
+
+[appid]: https://cloud.google.com/appengine/docs/java/users/
+[ae-docs]: https://cloud.google.com/appengine/docs/java/
+
+## Running locally
+This example uses the
+[Maven gcloud plugin](https://cloud.google.com/appengine/docs/java/tools/using-maven).
+To run this sample locally:
+
+ $ mvn appengine:run
+
+## Deploying
+In the following command, replace YOUR-PROJECT-ID with your
+[Google Cloud Project ID](https://developers.google.com/console/help/new/#projectnumber).
+
+ $ mvn appengine:deploy
diff --git a/appengine-java8/users/pom.xml b/appengine-java8/users/pom.xml
new file mode 100644
index 00000000000..16127e9f0fc
--- /dev/null
+++ b/appengine-java8/users/pom.xml
@@ -0,0 +1,113 @@
+
+
+
+ 4.0.0
+ war
+ 1.0-SNAPSHOT
+ com.example.appengine
+ appengine-users-j8
+
+
+
+ com.google.cloud
+ appengine-java8-samples
+ 1.0.0
+ ..
+
+
+
+
+ com.google.appengine
+ appengine-api-1.0-sdk
+ ${appengine.sdk.version}
+
+
+ com.google.guava
+ guava
+ 20.0
+
+
+
+ javax.servlet
+ javax.servlet-api
+ 3.1.0
+ jar
+ provided
+
+
+
+ org.json
+ json
+ 20160810
+
+
+
+
+ junit
+ junit
+ 4.12
+ test
+
+
+ org.mockito
+ mockito-all
+ 1.10.19
+ test
+
+
+ com.google.appengine
+ appengine-testing
+ ${appengine.sdk.version}
+ test
+
+
+ com.google.appengine
+ appengine-api-stubs
+ ${appengine.sdk.version}
+ test
+
+
+ com.google.appengine
+ appengine-tools-sdk
+ ${appengine.sdk.version}
+ test
+
+
+ com.google.truth
+ truth
+ 0.32
+ test
+
+
+
+
+
+ ${project.build.directory}/${project.build.finalName}/WEB-INF/classes
+
+
+ com.google.cloud.tools
+ appengine-maven-plugin
+ 1.3.1
+
+ true
+ true
+
+
+
+
+
diff --git a/appengine-java8/users/src/main/java/com/example/appengine/users/UsersServlet.java b/appengine-java8/users/src/main/java/com/example/appengine/users/UsersServlet.java
new file mode 100644
index 00000000000..d654fc78384
--- /dev/null
+++ b/appengine-java8/users/src/main/java/com/example/appengine/users/UsersServlet.java
@@ -0,0 +1,51 @@
+/* Copyright 2016 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.
+ */
+
+// [START users_API_example]
+package com.example.appengine.users;
+
+import com.google.appengine.api.users.UserService;
+import com.google.appengine.api.users.UserServiceFactory;
+
+import java.io.IOException;
+
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+public class UsersServlet extends HttpServlet {
+ @Override
+ public void doGet(HttpServletRequest req, HttpServletResponse resp)
+ throws IOException {
+ UserService userService = UserServiceFactory.getUserService();
+
+ String thisUrl = req.getRequestURI();
+
+ resp.setContentType("text/html");
+ if (req.getUserPrincipal() != null) {
+ resp.getWriter().println("
Hello, "
+ + req.getUserPrincipal().getName()
+ + "! You can sign out.
");
+ }
+ }
+}
+// [END users_API_example]
+
diff --git a/appengine-java8/users/src/main/webapp/WEB-INF/appengine-web.xml b/appengine-java8/users/src/main/webapp/WEB-INF/appengine-web.xml
new file mode 100644
index 00000000000..29bc35cee0b
--- /dev/null
+++ b/appengine-java8/users/src/main/webapp/WEB-INF/appengine-web.xml
@@ -0,0 +1,5 @@
+
+
+ java8
+ true
+
diff --git a/appengine-java8/users/src/main/webapp/WEB-INF/web.xml b/appengine-java8/users/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 00000000000..4b2b77234dc
--- /dev/null
+++ b/appengine-java8/users/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,14 @@
+
+
+
+ users
+ com.example.appengine.users.UsersServlet
+
+
+ users
+ /
+
+
diff --git a/appengine-java8/users/src/test/java/com/example/appengine/users/UsersServletTest.java b/appengine-java8/users/src/test/java/com/example/appengine/users/UsersServletTest.java
new file mode 100644
index 00000000000..ffc0a74785a
--- /dev/null
+++ b/appengine-java8/users/src/test/java/com/example/appengine/users/UsersServletTest.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2015 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.users;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.when;
+
+import com.google.appengine.tools.development.testing.LocalServiceTestHelper;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+import javax.management.remote.JMXPrincipal;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Unit tests for {@link UsersServlet}.
+ */
+@RunWith(JUnit4.class)
+public class UsersServletTest {
+ private static final String FAKE_URL = "fakey.fake.fak";
+ private static final String FAKE_NAME = "Fake";
+ // Set up a helper so that the ApiProxy returns a valid environment for local testing.
+ private final LocalServiceTestHelper helper = new LocalServiceTestHelper();
+
+ @Mock private HttpServletRequest mockRequestNotLoggedIn;
+ @Mock private HttpServletRequest mockRequestLoggedIn;
+ @Mock private HttpServletResponse mockResponse;
+ private StringWriter responseWriter;
+ private UsersServlet servletUnderTest;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ helper.setUp();
+
+ // Set up some fake HTTP requests
+ // If the user isn't logged in, use this request
+ when(mockRequestNotLoggedIn.getRequestURI()).thenReturn(FAKE_URL);
+ when(mockRequestNotLoggedIn.getUserPrincipal()).thenReturn(null);
+
+ // If the user is logged in, use this request
+ when(mockRequestLoggedIn.getRequestURI()).thenReturn(FAKE_URL);
+ // Most of the classes that implement Principal have been
+ // deprecated. JMXPrincipal seems like a safe choice.
+ when(mockRequestLoggedIn.getUserPrincipal()).thenReturn(new JMXPrincipal(FAKE_NAME));
+
+ // Set up a fake HTTP response.
+ responseWriter = new StringWriter();
+ when(mockResponse.getWriter()).thenReturn(new PrintWriter(responseWriter));
+
+ servletUnderTest = new UsersServlet();
+ }
+
+ @After public void tearDown() {
+ helper.tearDown();
+ }
+
+ @Test
+ public void doGet_userNotLoggedIn_writesResponse() throws Exception {
+ servletUnderTest.doGet(mockRequestNotLoggedIn, mockResponse);
+
+ // If a user isn't logged in, we expect a prompt
+ // to login to be returned.
+ assertThat(responseWriter.toString())
+ .named("UsersServlet response")
+ .contains("
");
+ }
+
+ @Test
+ public void doGet_userLoggedIn_writesResponse() throws Exception {
+ servletUnderTest.doGet(mockRequestLoggedIn, mockResponse);
+
+ // If a user is logged in, we expect a prompt
+ // to logout to be returned.
+ assertThat(responseWriter.toString())
+ .named("UsersServlet response")
+ .contains("
Hello, " + FAKE_NAME + "!");
+ assertThat(responseWriter.toString())
+ .named("UsersServlet response")
+ .contains("sign out");
+ }
+}
diff --git a/appengine/.gitignore b/appengine/.gitignore
deleted file mode 100644
index c94f025e98b..00000000000
--- a/appengine/.gitignore
+++ /dev/null
@@ -1,44 +0,0 @@
-# Copyright 2015 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.
-
-# Google App Engine generated folder
-appengine-generated/
-
-# Java
-*.class
-
-# Mobile Tools for Java (J2ME)
-.mtj.tmp/
-
-# Package Files #
-*.jar
-*.war
-*.ear
-
-# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
-hs_err_pid*
-
-# maven
-target/
-pom.xml.tag
-pom.xml.releaseBackup
-pom.xml.versionsBackup
-pom.xml.next
-release.properties
-dependency-reduced-pom.xml
-buildNumber.properties
-
-service-account.json
-
-#eclipse
-.classpath
-.settings
-.project
diff --git a/appengine/appidentity/README.md b/appengine/appidentity/README.md
index 5f5c13bd111..59a46a909a1 100644
--- a/appengine/appidentity/README.md
+++ b/appengine/appidentity/README.md
@@ -8,26 +8,11 @@ Engine][ae-docs].
## Running locally
This example uses the
-[Maven gcloud plugin](https://cloud.google.com/appengine/docs/java/managed-vms/maven).
+[Maven Cloud SDK plugin](https://cloud.google.com/appengine/docs/java/tools/using-maven).
To run this sample locally:
- $ mvn gcloud:run
+ $ mvn appengine:run
## Deploying
-In the following command, replace YOUR-PROJECT-ID with your
-[Google Cloud Project ID](https://developers.google.com/console/help/new/#projectnumber).
- $ mvn gcloud:deploy -Dgcloud.gcloud_project=YOUR-PROJECT-ID
-
-## Setup
-To save your project settings so that you don't need to enter the
-`-Dgcloud.gcloud_project=YOUR-CLOUD-PROJECT-ID` parameters, you can:
-
-1. Update the tag in src/main/webapp/WEB-INF/appengine-web.xml
- with your project name.
-
-You will now be able to run
-
- $ mvn gcloud:deploy
-
-without the need for any additional parameters.
+ $ mvn appengine:deploy
diff --git a/appengine/appidentity/pom.xml b/appengine/appidentity/pom.xml
index 8c950af5ac3..9a3c97bac20 100644
--- a/appengine/appidentity/pom.xml
+++ b/appengine/appidentity/pom.xml
@@ -28,7 +28,7 @@
- 2.0.9.133.v201611104
+ 1.9.52
@@ -98,9 +98,13 @@
${project.build.directory}/${project.build.finalName}/WEB-INF/classes
- com.google.appengine
- gcloud-maven-plugin
- ${gcloud-maven-plugin-version}
+ com.google.cloud.tools
+ appengine-maven-plugin
+ 1.3.1
+
+ true
+ true
+
diff --git a/appengine/appidentity/src/main/webapp/WEB-INF/appengine-web.xml b/appengine/appidentity/src/main/webapp/WEB-INF/appengine-web.xml
index 21d1d476a5f..3136a5e9072 100644
--- a/appengine/appidentity/src/main/webapp/WEB-INF/appengine-web.xml
+++ b/appengine/appidentity/src/main/webapp/WEB-INF/appengine-web.xml
@@ -2,5 +2,4 @@
YOUR-PROJECT-IDtrue
- true
diff --git a/appengine/channel/README.md b/appengine/channel/README.md
new file mode 100644
index 00000000000..0c478d5997b
--- /dev/null
+++ b/appengine/channel/README.md
@@ -0,0 +1,5 @@
+AppEngine Channel demo
+========
+
+# WARNING - the Channel API has been deprecated. This sample is for historical purposes only.
+
diff --git a/pom.xml b/pom.xml
index d04690edd50..756fb3ee783 100644
--- a/pom.xml
+++ b/pom.xml
@@ -47,6 +47,7 @@
appengine
+ appengine-java8flexible/analytics