diff --git a/appengine-java21/ee8/datastore/README.md b/appengine-java21/ee8/datastore/README.md
new file mode 100644
index 00000000000..cb258be8741
--- /dev/null
+++ b/appengine-java21/ee8/datastore/README.md
@@ -0,0 +1,32 @@
+# 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/legacy/standard/java/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 clean package appengine:deploy
+ $ mvn appengine:deployIndex
diff --git a/appengine-java21/ee8/datastore/pom.xml b/appengine-java21/ee8/datastore/pom.xml
new file mode 100644
index 00000000000..ae955be85df
--- /dev/null
+++ b/appengine-java21/ee8/datastore/pom.xml
@@ -0,0 +1,181 @@
+
+
+ 4.0.0
+ war
+ 1.0-SNAPSHOT
+ com.example.appengine
+ appengine-datastore-j21
+
+
+
+ com.google.cloud.samples
+ shared-configuration
+ 1.2.0
+
+
+
+ 21
+ 21
+
+
+
+
+
+ libraries-bom
+ com.google.cloud
+ import
+ pom
+ 26.28.0
+
+
+
+
+
+
+ com.google.appengine
+ appengine-api-1.0-sdk
+ 2.0.39
+
+
+
+ jakarta.servlet
+ jakarta.servlet-api
+ 4.0.4
+ jar
+ provided
+
+
+
+ taglibs
+ standard
+ 1.1.2
+
+
+ jakarta.servlet.jsp.jstl
+ jakarta.servlet.jsp.jstl-api
+ 1.2.7
+
+
+
+ com.google.auto.value
+ auto-value
+ 1.11.0
+ provided
+
+
+
+ com.google.auto.value
+ auto-value-annotations
+
+
+
+ com.google.code.findbugs
+ jsr305
+ 3.0.2
+
+
+
+ com.google.guava
+ guava
+
+
+
+
+ junit
+ junit
+ 4.13.2
+ test
+
+
+ org.mockito
+ mockito-core
+ 4.11.0
+ test
+
+
+
+ com.google.appengine
+ appengine-testing
+ 2.0.39
+ test
+
+
+ com.google.appengine
+ appengine-api-stubs
+ 2.0.39
+ test
+
+
+ com.google.appengine
+ appengine-tools-sdk
+ 2.0.39
+ test
+
+
+ com.google.truth
+ truth
+ 1.4.4
+ test
+
+
+
+
+
+ ${project.build.directory}/${project.build.finalName}/WEB-INF/classes
+
+
+ org.apache.maven.plugins
+ maven-war-plugin
+ 3.4.0
+
+
+ org.jacoco
+ jacoco-maven-plugin
+ 0.8.13
+
+
+ com.google.cloud.tools
+ appengine-maven-plugin
+ 2.5.0
+
+ GCLOUD_CONFIG
+ GCLOUD_CONFIG
+ true
+ true
+
+
+
+
+ maven-compiler-plugin
+ 3.11.0
+
+
+
+ com.google.auto.value
+ auto-value
+ 1.11.0
+
+
+
+
+
+
+
diff --git a/appengine-java21/ee8/datastore/src/main/java/com/example/appengine/AbstractGuestbook.java b/appengine-java21/ee8/datastore/src/main/java/com/example/appengine/AbstractGuestbook.java
new file mode 100644
index 00000000000..a7b46bc4727
--- /dev/null
+++ b/appengine-java21/ee8/datastore/src/main/java/com/example/appengine/AbstractGuestbook.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2016 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.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) {
+ return Greeting.create(
+ createGreeting(datastore, userService.getCurrentUser(), Date.from(clock.now()), content));
+ }
+
+ /**
+ * 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-java21/ee8/datastore/src/main/java/com/example/appengine/AbstractGuestbookServlet.java b/appengine-java21/ee8/datastore/src/main/java/com/example/appengine/AbstractGuestbookServlet.java
new file mode 100644
index 00000000000..d9a0d518b92
--- /dev/null
+++ b/appengine-java21/ee8/datastore/src/main/java/com/example/appengine/AbstractGuestbookServlet.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2016 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.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-java21/ee8/datastore/src/main/java/com/example/appengine/Greeting.java b/appengine-java21/ee8/datastore/src/main/java/com/example/appengine/Greeting.java
new file mode 100644
index 00000000000..ff45e508205
--- /dev/null
+++ b/appengine-java21/ee8/datastore/src/main/java/com/example/appengine/Greeting.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2016 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.appengine;
+
+import com.google.appengine.api.datastore.Entity;
+import com.google.appengine.api.users.User;
+import com.google.auto.value.AutoValue;
+import java.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 = ((Date) entity.getProperty("date")).toInstant();
+ 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-java21/ee8/datastore/src/main/java/com/example/appengine/Guestbook.java b/appengine-java21/ee8/datastore/src/main/java/com/example/appengine/Guestbook.java
new file mode 100644
index 00000000000..ebacbf540ae
--- /dev/null
+++ b/appengine-java21/ee8/datastore/src/main/java/com/example/appengine/Guestbook.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2016 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.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-java21/ee8/datastore/src/main/java/com/example/appengine/GuestbookServlet.java b/appengine-java21/ee8/datastore/src/main/java/com/example/appengine/GuestbookServlet.java
new file mode 100644
index 00000000000..019e5a4210c
--- /dev/null
+++ b/appengine-java21/ee8/datastore/src/main/java/com/example/appengine/GuestbookServlet.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2016 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.appengine;
+
+import com.example.time.SystemClock;
+
+public class GuestbookServlet extends AbstractGuestbookServlet {
+
+ public GuestbookServlet() {
+ super(new Guestbook(new SystemClock()));
+ }
+}
diff --git a/appengine-java21/ee8/datastore/src/main/java/com/example/appengine/GuestbookStrong.java b/appengine-java21/ee8/datastore/src/main/java/com/example/appengine/GuestbookStrong.java
new file mode 100644
index 00000000000..a3c1bc08743
--- /dev/null
+++ b/appengine-java21/ee8/datastore/src/main/java/com/example/appengine/GuestbookStrong.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2016 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.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-java21/ee8/datastore/src/main/java/com/example/appengine/GuestbookStrongServlet.java b/appengine-java21/ee8/datastore/src/main/java/com/example/appengine/GuestbookStrongServlet.java
new file mode 100644
index 00000000000..2861c88d991
--- /dev/null
+++ b/appengine-java21/ee8/datastore/src/main/java/com/example/appengine/GuestbookStrongServlet.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2016 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.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-java21/ee8/datastore/src/main/java/com/example/appengine/ListPeopleServlet.java b/appengine-java21/ee8/datastore/src/main/java/com/example/appengine/ListPeopleServlet.java
new file mode 100644
index 00000000000..8bb61cbbadc
--- /dev/null
+++ b/appengine-java21/ee8/datastore/src/main/java/com/example/appengine/ListPeopleServlet.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2016 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.appengine;
+
+// [START gae_java21_datastore_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 gae_java21_datastore_cursors]
diff --git a/appengine-java21/ee8/datastore/src/main/java/com/example/appengine/ProjectionServlet.java b/appengine-java21/ee8/datastore/src/main/java/com/example/appengine/ProjectionServlet.java
new file mode 100644
index 00000000000..d062f9d6d17
--- /dev/null
+++ b/appengine-java21/ee8/datastore/src/main/java/com/example/appengine/ProjectionServlet.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2016 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.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-java21/ee8/datastore/src/main/java/com/example/appengine/StartupServlet.java b/appengine-java21/ee8/datastore/src/main/java/com/example/appengine/StartupServlet.java
new file mode 100644
index 00000000000..9f4d2d06970
--- /dev/null
+++ b/appengine-java21/ee8/datastore/src/main/java/com/example/appengine/StartupServlet.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright 2016 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.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-java21/ee8/datastore/src/main/java/com/example/appengine/StatsServlet.java b/appengine-java21/ee8/datastore/src/main/java/com/example/appengine/StatsServlet.java
new file mode 100644
index 00000000000..74531a56e56
--- /dev/null
+++ b/appengine-java21/ee8/datastore/src/main/java/com/example/appengine/StatsServlet.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2016 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.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 gae_java21_datastore_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 gae_java21_datastore_stat_example]
+
+ resp.setContentType("text/plain");
+ resp.setCharacterEncoding("UTF-8");
+ PrintWriter w = resp.getWriter();
+ w.printf("%d bytes\n%d entities\n", totalBytes, totalEntities);
+ }
+}
diff --git a/appengine-java21/ee8/datastore/src/main/java/com/example/time/Clock.java b/appengine-java21/ee8/datastore/src/main/java/com/example/time/Clock.java
new file mode 100644
index 00000000000..a703577d7f9
--- /dev/null
+++ b/appengine-java21/ee8/datastore/src/main/java/com/example/time/Clock.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2016 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.time;
+
+import java.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
+ * java.time.Instant#now() 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-java21/ee8/datastore/src/main/java/com/example/time/SystemClock.java b/appengine-java21/ee8/datastore/src/main/java/com/example/time/SystemClock.java
new file mode 100644
index 00000000000..dcde41330f6
--- /dev/null
+++ b/appengine-java21/ee8/datastore/src/main/java/com/example/time/SystemClock.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2016 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.time;
+
+import java.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 Instant.now();
+ }
+}
diff --git a/appengine-java21/ee8/datastore/src/main/java/com/example/time/testing/FakeClock.java b/appengine-java21/ee8/datastore/src/main/java/com/example/time/testing/FakeClock.java
new file mode 100644
index 00000000000..a08343e51b6
--- /dev/null
+++ b/appengine-java21/ee8/datastore/src/main/java/com/example/time/testing/FakeClock.java
@@ -0,0 +1,180 @@
+/*
+ * Copyright 2016 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.time.testing;
+
+import com.example.time.Clock;
+import java.time.Duration;
+import java.time.Instant;
+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 = Instant.ofEpochMilli(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(Instant now) {
+ baseTimeMs = now.toEpochMilli();
+ fakeNowMs = new AtomicLong(baseTimeMs);
+ }
+
+ /**
+ * Sets the value of the underlying instance for testing purposes.
+ *
+ * @return this
+ */
+ public FakeClock setNow(Instant now) {
+ fakeNowMs.set(now.toEpochMilli());
+ 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 Instant.ofEpochMilli(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(Duration duration) {
+ incrementTime(duration.toMillis());
+ 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(Duration duration) {
+ incrementTime(-duration.toMillis());
+ 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(Duration autoIncrementStep) {
+ setAutoIncrementStep(autoIncrementStep.toMillis());
+ 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.
+ *
+ * @param durationMs the duration to add, in milliseconds
+ * @return the updated current time
+ * @see AtomicLong#addAndGet
+ */
+ protected final Instant addAndGet(long durationMs) {
+ return Instant.ofEpochMilli(fakeNowMs.addAndGet(durationMs));
+ }
+
+ /**
+ * Atomically adds the given value to the current time.
+ *
+ * @param durationMs the duration to add, in milliseconds
+ * @return the previous time
+ * @see AtomicLong#getAndAdd
+ */
+ protected final Instant getAndAdd(long durationMs) {
+ return Instant.ofEpochMilli(fakeNowMs.getAndAdd(durationMs));
+ }
+}
diff --git a/appengine-java21/ee8/datastore/src/main/webapp/WEB-INF/appengine-web.xml b/appengine-java21/ee8/datastore/src/main/webapp/WEB-INF/appengine-web.xml
new file mode 100644
index 00000000000..56e91137870
--- /dev/null
+++ b/appengine-java21/ee8/datastore/src/main/webapp/WEB-INF/appengine-web.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+ java21
+ true
+
+
+
+
diff --git a/appengine-java21/ee8/datastore/src/main/webapp/WEB-INF/datastore-indexes.xml b/appengine-java21/ee8/datastore/src/main/webapp/WEB-INF/datastore-indexes.xml
new file mode 100644
index 00000000000..c99175eba3b
--- /dev/null
+++ b/appengine-java21/ee8/datastore/src/main/webapp/WEB-INF/datastore-indexes.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/appengine-java21/ee8/datastore/src/main/webapp/WEB-INF/web.xml b/appengine-java21/ee8/datastore/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 00000000000..dddc47141c7
--- /dev/null
+++ b/appengine-java21/ee8/datastore/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,98 @@
+
+
+
+
+ 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-java21/ee8/datastore/src/main/webapp/guestbook.jsp b/appengine-java21/ee8/datastore/src/main/webapp/guestbook.jsp
new file mode 100644
index 00000000000..5d2d5708f7e
--- /dev/null
+++ b/appengine-java21/ee8/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" %>
+
+
+
+ Guestbook
+
+
+ Latest Greetings
+
+
+ ${greeting.content}
+ Posted: ${greeting.date}
+
+
+
+ Add Greeting
+
+
+
+
diff --git a/appengine-java21/ee8/datastore/src/test/java/com/example/appengine/EntitiesTest.java b/appengine-java21/ee8/datastore/src/test/java/com/example/appengine/EntitiesTest.java
new file mode 100644
index 00000000000..99cc7643eeb
--- /dev/null
+++ b/appengine-java21/ee8/datastore/src/test/java/com/example/appengine/EntitiesTest.java
@@ -0,0 +1,373 @@
+/*
+ * Copyright 2016 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.appengine;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+import static org.junit.Assert.fail;
+
+import com.google.appengine.api.datastore.DatastoreService;
+import com.google.appengine.api.datastore.DatastoreServiceFactory;
+import com.google.appengine.api.datastore.EmbeddedEntity;
+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.appengine.api.datastore.KeyRange;
+import com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig;
+import com.google.appengine.tools.development.testing.LocalServiceTestHelper;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests to demonstrate App Engine Datastore entities. */
+@RunWith(JUnit4.class)
+public class EntitiesTest {
+
+ // 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
+ private final LocalServiceTestHelper helper =
+ new LocalServiceTestHelper(
+ new LocalDatastoreServiceTestConfig()
+ .setDefaultHighRepJobPolicyUnappliedJobPercentage(0));
+
+ private DatastoreService datastore;
+
+ @Before
+ public void setUp() {
+ helper.setUp();
+ datastore = DatastoreServiceFactory.getDatastoreService();
+ }
+
+ @After
+ public void tearDown() {
+ helper.tearDown();
+ }
+
+ @Test
+ public void kindExample_writesEntity() throws Exception {
+ // [START gae_java21_datastore_kind_example]
+ Entity employee = new Entity("Employee", "asalieri");
+ employee.setProperty("firstName", "Antonio");
+ employee.setProperty("lastName", "Salieri");
+ employee.setProperty("hireDate", new Date());
+ employee.setProperty("attendedHrTraining", true);
+
+ DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
+ datastore.put(employee);
+ // [END gae_java21_datastore_kind_example]
+
+ Entity got = datastore.get(employee.getKey());
+ assertWithMessage("got.firstName")
+ .that((String) got.getProperty("firstName"))
+ .isEqualTo("Antonio");
+ assertWithMessage("got.lastName")
+ .that((String) got.getProperty("lastName"))
+ .isEqualTo("Salieri");
+ assertWithMessage("got.hireDate").that((Date) got.getProperty("hireDate")).isNotNull();
+ assertWithMessage("got.attendedHrTraining")
+ .that((boolean) got.getProperty("attendedHrTraining"))
+ .isTrue();
+ }
+
+ @Test
+ public void identifiers_keyName_setsKeyName() throws Exception {
+ // [START gae_java21_datastore_identifiers_1]
+ Entity employee = new Entity("Employee", "asalieri");
+ // [END gae_java21_datastore_identifiers_1]
+ datastore.put(employee);
+
+ assertWithMessage("key name").that(employee.getKey().getName()).isEqualTo("asalieri");
+ }
+
+ @Test
+ public void identifiers_autoId_setsUnallocatedId() throws Exception {
+ KeyRange keys = datastore.allocateIds("Employee", 1);
+ long usedId = keys.getStart().getId();
+
+ // [START gae_java21_datastore_identifiers_2]
+ Entity employee = new Entity("Employee");
+ // [END gae_java21_datastore_identifiers_2]
+ datastore.put(employee);
+
+ assertWithMessage("key id").that(employee.getKey().getId()).isNotEqualTo(usedId);
+ }
+
+ @Test
+ public void parent_withinEntityConstructor_setsParent() throws Exception {
+ // [START gae_java21_datastore_parent_1]
+ Entity employee = new Entity("Employee");
+ datastore.put(employee);
+
+ Entity address = new Entity("Address", employee.getKey());
+ datastore.put(address);
+ // [END gae_java21_datastore_parent_1]
+
+ assertWithMessage("address parent").that(address.getParent()).isEqualTo(employee.getKey());
+ }
+
+ @Test
+ public void parent_withKeyName_setsKeyName() throws Exception {
+ Entity employee = new Entity("Employee");
+ datastore.put(employee);
+
+ // [START gae_java21_datastore_parent_2]
+ Entity address = new Entity("Address", "addr1", employee.getKey());
+ // [END gae_java21_datastore_parent_2]
+ datastore.put(address);
+
+ assertWithMessage("address key name").that(address.getKey().getName()).isEqualTo("addr1");
+ }
+
+ @Test
+ public void datastoreServiceFactory_returnsDatastoreService() throws Exception {
+ // [START gae_java21_datastore_working_with_entities]
+ DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
+ // [END gae_java21_datastore_working_with_entities]
+ assertWithMessage("datastore").that(datastore).isNotNull();
+ }
+
+ @Test
+ public void creatingAnEntity_withKeyName_writesEntity() throws Exception {
+ // [START gae_java21_datastore_creating_an_entity_1]
+ Entity employee = new Entity("Employee", "asalieri");
+ // Set the entity properties.
+ // ...
+ datastore.put(employee);
+ // [END gae_java21_datastore_creating_an_entity_1]
+
+ assertWithMessage("employee key name").that(employee.getKey().getName()).isEqualTo("asalieri");
+ }
+
+ private Key writeEmptyEmployee() {
+ // [START gae_java21_datastore_creating_an_entity_2]
+ Entity employee = new Entity("Employee");
+ // Set the entity properties.
+ // ...
+ datastore.put(employee);
+ // [END gae_java21_datastore_creating_an_entity_2]
+ return employee.getKey();
+ }
+
+ @Test
+ public void creatingAnEntity_withoutKeyName_writesEntity() throws Exception {
+ Key employeeKey = writeEmptyEmployee();
+ // [START gae_java21_datastore_retrieving_an_entity]
+ // Key employeeKey = ...;
+ Entity employee = datastore.get(employeeKey);
+ // [END gae_java21_datastore_retrieving_an_entity]
+
+ assertWithMessage("retrieved key ID")
+ .that(employee.getKey().getId())
+ .isEqualTo(employeeKey.getId());
+ }
+
+ @Test
+ public void deletingAnEntity_deletesAnEntity() throws Exception {
+ Entity employee = new Entity("Employee", "asalieri");
+ datastore.put(employee);
+
+ Key employeeKey = KeyFactory.createKey("Employee", "asalieri");
+ // [START gae_java21_datastore_deleting_an_entity]
+ // Key employeeKey = ...;
+ datastore.delete(employeeKey);
+ // [END gae_java21_datastore_deleting_an_entity]
+
+ try {
+ Entity got = datastore.get(employeeKey);
+ fail("Expected EntityNotFoundException");
+ } catch (EntityNotFoundException expected) {
+ assertWithMessage("exception key name")
+ .that(expected.getKey().getName())
+ .isEqualTo("asalieri");
+ }
+ }
+
+ @Test
+ public void repeatedProperties_storesList() throws Exception {
+ // [START gae_java21_datastore_repeated_properties]
+ Entity employee = new Entity("Employee");
+ ArrayList favoriteFruit = new ArrayList<>();
+ favoriteFruit.add("Pear");
+ favoriteFruit.add("Apple");
+ employee.setProperty("favoriteFruit", favoriteFruit);
+ datastore.put(employee);
+
+ // Sometime later
+ employee = datastore.get(employee.getKey());
+ @SuppressWarnings("unchecked") // Cast can't verify generic type.
+ ArrayList retrievedFruits = (ArrayList) employee.getProperty("favoriteFruit");
+ // [END gae_java21_datastore_repeated_properties]
+
+ assertThat(retrievedFruits).containsExactlyElementsIn(favoriteFruit).inOrder();
+ }
+
+ // CHECKSTYLE.OFF: VariableDeclarationUsageDistance
+ @SuppressWarnings("VariableDeclarationUsageDistance")
+ @Test
+ public void embeddedEntity_fromEmbedded_embedsProperties() throws Exception {
+ Entity employee = new Entity("Employee");
+ // [START gae_java21_datastore_embedded_entities_1]
+ // Entity employee = ...;
+ EmbeddedEntity embeddedContactInfo = new EmbeddedEntity();
+
+ embeddedContactInfo.setProperty("homeAddress", "123 Fake St, Made, UP 45678");
+ embeddedContactInfo.setProperty("phoneNumber", "555-555-5555");
+ embeddedContactInfo.setProperty("emailAddress", "test@example.com");
+
+ employee.setProperty("contactInfo", embeddedContactInfo);
+ // [END gae_java21_datastore_embedded_entities_1]
+ datastore.put(employee);
+
+ Entity gotEmployee = datastore.get(employee.getKey());
+ EmbeddedEntity got = (EmbeddedEntity) gotEmployee.getProperty("contactInfo");
+ assertWithMessage("got.homeAddress")
+ .that((String) got.getProperty("homeAddress"))
+ .isEqualTo("123 Fake St, Made, UP 45678");
+ }
+ // CHECKSTYLE.ON: VariableDeclarationUsageDistance
+
+ private Key putEmployeeWithContactInfo(Entity contactInfo) {
+ Entity employee = new Entity("Employee");
+ // [START gae_java21_datastore_embedded_entities_2]
+ // Entity employee = ...;
+ // Entity contactInfo = ...;
+ EmbeddedEntity embeddedContactInfo = new EmbeddedEntity();
+
+ embeddedContactInfo.setKey(contactInfo.getKey()); // Optional, used so we can recover original.
+ embeddedContactInfo.setPropertiesFrom(contactInfo);
+
+ employee.setProperty("contactInfo", embeddedContactInfo);
+ // [END gae_java21_datastore_embedded_entities_2]
+ datastore.put(employee);
+ return employee.getKey();
+ }
+
+ @Test
+ public void embeddedEntity_fromExisting_canRecover() throws Exception {
+ Entity initialContactInfo = new Entity("Contact");
+ initialContactInfo.setProperty("homeAddress", "123 Fake St, Made, UP 45678");
+ initialContactInfo.setProperty("phoneNumber", "555-555-5555");
+ initialContactInfo.setProperty("emailAddress", "test@example.com");
+ datastore.put(initialContactInfo);
+ Key employeeKey = putEmployeeWithContactInfo(initialContactInfo);
+
+ // [START gae_java21_datastore_embedded_entities_3]
+ Entity employee = datastore.get(employeeKey);
+ EmbeddedEntity embeddedContactInfo = (EmbeddedEntity) employee.getProperty("contactInfo");
+
+ Key infoKey = embeddedContactInfo.getKey();
+ Entity contactInfo = new Entity(infoKey);
+ contactInfo.setPropertiesFrom(embeddedContactInfo);
+ // [END gae_java21_datastore_embedded_entities_3]
+ datastore.put(contactInfo);
+
+ Entity got = datastore.get(infoKey);
+ assertThat(got.getKey()).isEqualTo(initialContactInfo.getKey());
+ assertWithMessage("got.homeAddress")
+ .that((String) got.getProperty("homeAddress"))
+ .isEqualTo("123 Fake St, Made, UP 45678");
+ }
+
+ @Test
+ public void batchOperations_putsEntities() {
+ // [START gae_java21_datastore_gae_batch_operations]
+ Entity employee1 = new Entity("Employee");
+ Entity employee2 = new Entity("Employee");
+ Entity employee3 = new Entity("Employee");
+ // [START_EXCLUDE]
+ employee1.setProperty("firstName", "Bill");
+ employee2.setProperty("firstName", "Jane");
+ employee3.setProperty("firstName", "Alex");
+ // [END_EXCLUDE]
+
+ List employees = Arrays.asList(employee1, employee2, employee3);
+ datastore.put(employees);
+ // [END gae_java21_datastore_gae_batch_operations]
+
+ Map got =
+ datastore.get(Arrays.asList(employee1.getKey(), employee2.getKey(), employee3.getKey()));
+ assertWithMessage("employee1.firstName")
+ .that((String) got.get(employee1.getKey()).getProperty("firstName"))
+ .isEqualTo("Bill");
+ assertWithMessage("employee2.firstName")
+ .that((String) got.get(employee2.getKey()).getProperty("firstName"))
+ .isEqualTo("Jane");
+ assertWithMessage("employee3.firstName")
+ .that((String) got.get(employee3.getKey()).getProperty("firstName"))
+ .isEqualTo("Alex");
+ }
+
+ @Test
+ public void createKey_makesKey() {
+ // [START gae_java21_datastore_generating_keys_1]
+ Key k1 = KeyFactory.createKey("Person", "GreatGrandpa");
+ Key k2 = KeyFactory.createKey("Person", 74219);
+ // [END gae_java21_datastore_generating_keys_1]
+
+ assertThat(k1).isNotNull();
+ assertThat(k2).isNotNull();
+ }
+
+ @Test
+ public void keyFactoryBuilder_makeKeyWithParents() {
+ Key greatKey = KeyFactory.createKey("Person", "GreatGrandpa");
+ Key grandKey = KeyFactory.createKey(greatKey, "Person", "Grandpa");
+ Key dadKey = KeyFactory.createKey(grandKey, "Person", "Dad");
+ Key meKey = KeyFactory.createKey(dadKey, "Person", "Me");
+
+ // [START gae_java21_datastore_generating_keys_2]
+ Key k =
+ new KeyFactory.Builder("Person", "GreatGrandpa")
+ .addChild("Person", "Grandpa")
+ .addChild("Person", "Dad")
+ .addChild("Person", "Me")
+ .getKey();
+ // [END gae_java21_datastore_generating_keys_2]
+
+ assertThat(k).isEqualTo(meKey);
+ }
+
+ @Test
+ public void keyToString_getsPerson() throws Exception {
+ Entity p = new Entity("Person");
+ p.setProperty("relationship", "Me");
+ datastore.put(p);
+ Key k = p.getKey();
+
+ // [START gae_java21_datastore_generating_keys_3]
+ String personKeyStr = KeyFactory.keyToString(k);
+
+ // Some time later (for example, after using personKeyStr in a link).
+ Key personKey = KeyFactory.stringToKey(personKeyStr);
+ Entity person = datastore.get(personKey);
+ // [END gae_java21_datastore_generating_keys_3]
+
+ assertThat(personKey).isEqualTo(k);
+ assertWithMessage("person.relationship")
+ .that((String) person.getProperty("relationship"))
+ .isEqualTo("Me");
+ }
+}
diff --git a/appengine-java21/ee8/datastore/src/test/java/com/example/appengine/GuestbookStrongTest.java b/appengine-java21/ee8/datastore/src/test/java/com/example/appengine/GuestbookStrongTest.java
new file mode 100644
index 00000000000..ca73e082d82
--- /dev/null
+++ b/appengine-java21/ee8/datastore/src/test/java/com/example/appengine/GuestbookStrongTest.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2016 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.appengine;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import com.example.time.testing.FakeClock;
+import com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig;
+import com.google.appengine.tools.development.testing.LocalServiceTestHelper;
+import com.google.appengine.tools.development.testing.LocalUserServiceTestConfig;
+import java.time.Instant;
+import java.util.List;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Unit tests for {@link GuestbookStrong}.
+ */
+@RunWith(JUnit4.class)
+public class GuestbookStrongTest {
+
+ private static final Instant FAKE_NOW = Instant.ofEpochMilli(1234567890L);
+ private static final String GUESTBOOK_ID = "my guestbook";
+
+ // Set maximum eventual consistency.
+ // https://cloud.google.com/appengine/docs/java/tools/localunittesting
+ // #Java_Writing_High_Replication_Datastore_tests
+ private final LocalServiceTestHelper helper =
+ new LocalServiceTestHelper(
+ new LocalDatastoreServiceTestConfig()
+ .setDefaultHighRepJobPolicyUnappliedJobPercentage(100),
+ // Make sure there is a user logged in. We enforce this in web.xml.
+ new LocalUserServiceTestConfig())
+ .setEnvIsLoggedIn(true)
+ .setEnvEmail("test@example.com")
+ .setEnvAuthDomain("gmail.com");
+
+ private FakeClock clock;
+ private GuestbookStrong guestbookUnderTest;
+
+ @Before
+ public void setUp() throws Exception {
+ helper.setUp();
+ clock = new FakeClock(FAKE_NOW);
+ guestbookUnderTest = new GuestbookStrong(GUESTBOOK_ID, clock);
+ }
+
+ @After
+ public void tearDown() {
+ helper.tearDown();
+ }
+
+ @Test
+ public void appendGreeting_normalData_setsContentProperty() {
+ Greeting got = guestbookUnderTest.appendGreeting("Hello, Datastore!");
+
+ assertWithMessage("content property").that(got.getContent()).isEqualTo("Hello, Datastore!");
+ }
+
+ @Test
+ public void appendGreeting_normalData_setsDateProperty() {
+ Greeting got = guestbookUnderTest.appendGreeting("Hello, Datastore!");
+
+ assertWithMessage("date property").that(got.getDate()).isEqualTo(FAKE_NOW);
+ }
+
+ @Test
+ public void listGreetings_maximumEventualConsistency_returnsAllGreetings() {
+ // Arrange
+ guestbookUnderTest.appendGreeting("Hello, Datastore!");
+ guestbookUnderTest.appendGreeting("Hello, Eventual Consistency!");
+ guestbookUnderTest.appendGreeting("Hello, World!");
+
+ // Act
+ List got = guestbookUnderTest.listGreetings();
+
+ // Assert
+ // Since we use an ancestor query, all greetings should be available.
+ assertThat(got).hasSize(3);
+ }
+}
diff --git a/appengine-java21/ee8/datastore/src/test/java/com/example/appengine/GuestbookTest.java b/appengine-java21/ee8/datastore/src/test/java/com/example/appengine/GuestbookTest.java
new file mode 100644
index 00000000000..f24f0740a34
--- /dev/null
+++ b/appengine-java21/ee8/datastore/src/test/java/com/example/appengine/GuestbookTest.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright 2016 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.appengine;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import com.example.time.testing.FakeClock;
+import com.google.appengine.api.datastore.Key;
+import com.google.appengine.api.datastore.dev.HighRepJobPolicy;
+import com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig;
+import com.google.appengine.tools.development.testing.LocalServiceTestHelper;
+import com.google.appengine.tools.development.testing.LocalUserServiceTestConfig;
+import java.util.List;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Unit tests for {@link Guestbook}.
+ */
+@RunWith(JUnit4.class)
+public class GuestbookTest {
+
+ private static final class CustomHighRepJobPolicy implements HighRepJobPolicy {
+
+ static int newJobCounter = 0;
+ static int existingJobCounter = 0;
+
+ @Override
+ public boolean shouldApplyNewJob(Key entityGroup) {
+ // Every other new job fails to apply.
+ return newJobCounter++ % 2 == 0;
+ }
+
+ @Override
+ public boolean shouldRollForwardExistingJob(Key entityGroup) {
+ // Existing jobs always apply after every Get and every Query.
+ return true;
+ }
+ }
+
+ // Set custom, deterministic, eventual consistency.
+ // https://cloud.google.com/appengine/docs/java/tools/localunittesting
+ // #Java_Writing_High_Replication_Datastore_tests
+ private final LocalServiceTestHelper helper =
+ new LocalServiceTestHelper(
+ new LocalDatastoreServiceTestConfig()
+ .setAlternateHighRepJobPolicyClass(CustomHighRepJobPolicy.class),
+ // Make sure there is a user logged in. We enforce this in web.xml.
+ new LocalUserServiceTestConfig())
+ .setEnvIsLoggedIn(true)
+ .setEnvEmail("test@example.com")
+ .setEnvAuthDomain("gmail.com");
+
+ private FakeClock clock;
+ private Guestbook guestbookUnderTest;
+
+ @Before
+ public void setUp() throws Exception {
+ helper.setUp();
+ clock = new FakeClock();
+ guestbookUnderTest = new Guestbook(clock);
+ }
+
+ @After
+ public void tearDown() {
+ helper.tearDown();
+ }
+
+ @Test
+ public void appendGreeting_normalData_setsContentProperty() {
+ Greeting got = guestbookUnderTest.appendGreeting("Hello, Datastore!");
+
+ assertWithMessage("content property").that(got.getContent()).isEqualTo("Hello, Datastore!");
+ }
+
+ @Test
+ public void listGreetings_eventualConsistency_returnsPartialGreetings() {
+ // Arrange
+ guestbookUnderTest.appendGreeting("Hello, Datastore!");
+ guestbookUnderTest.appendGreeting("Hello, Eventual Consistency!");
+ guestbookUnderTest.appendGreeting("Hello, World!");
+ guestbookUnderTest.appendGreeting("Güten Tag!");
+
+ // Act
+ List got = guestbookUnderTest.listGreetings();
+
+ // The first time we query we should half of the results due to the fact that we simulate
+ // eventual consistency by applying every other write.
+ assertThat(got).hasSize(2);
+ }
+
+ @Test
+ public void listGreetings_groomedDatastore_returnsAllGreetings() {
+ // Arrange
+ guestbookUnderTest.appendGreeting("Hello, Datastore!");
+ guestbookUnderTest.appendGreeting("Hello, Eventual Consistency!");
+ guestbookUnderTest.appendGreeting("Hello, World!");
+
+ // Act
+ guestbookUnderTest.listGreetings();
+ // Second global query sees both Entities because we "groom" (attempt to
+ // apply unapplied jobs) after every query.
+ List got = guestbookUnderTest.listGreetings();
+
+ assertThat(got).hasSize(3);
+ }
+}
diff --git a/appengine-java21/ee8/datastore/src/test/java/com/example/appengine/IndexesTest.java b/appengine-java21/ee8/datastore/src/test/java/com/example/appengine/IndexesTest.java
new file mode 100644
index 00000000000..cbb83d7a393
--- /dev/null
+++ b/appengine-java21/ee8/datastore/src/test/java/com/example/appengine/IndexesTest.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2016 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.appengine;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+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.Query;
+import com.google.appengine.api.datastore.Query.Filter;
+import com.google.appengine.api.datastore.Query.FilterOperator;
+import com.google.appengine.api.datastore.Query.FilterPredicate;
+import com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig;
+import com.google.appengine.tools.development.testing.LocalServiceTestHelper;
+import java.util.List;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Unit tests to demonstrate App Engine Datastore queries.
+ */
+@RunWith(JUnit4.class)
+public class IndexesTest {
+
+ // 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
+ private final LocalServiceTestHelper helper =
+ new LocalServiceTestHelper(
+ new LocalDatastoreServiceTestConfig()
+ .setDefaultHighRepJobPolicyUnappliedJobPercentage(0));
+
+ private DatastoreService datastore;
+
+ @Before
+ public void setUp() {
+ helper.setUp();
+ datastore = DatastoreServiceFactory.getDatastoreService();
+ }
+
+ @After
+ public void tearDown() {
+ helper.tearDown();
+ }
+
+ @Test
+ public void propertyFilterExample_returnsMatchingEntities() throws Exception {
+ // [START gae_java21_datastore_unindexed_properties_1]
+ DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
+
+ Key acmeKey = KeyFactory.createKey("Company", "Acme");
+
+ Entity tom = new Entity("Person", "Tom", acmeKey);
+ tom.setProperty("name", "Tom");
+ tom.setProperty("age", 32);
+ datastore.put(tom);
+
+ Entity lucy = new Entity("Person", "Lucy", acmeKey);
+ lucy.setProperty("name", "Lucy");
+ lucy.setUnindexedProperty("age", 29);
+ datastore.put(lucy);
+
+ Filter ageFilter = new FilterPredicate("age", FilterOperator.GREATER_THAN, 25);
+
+ Query q = new Query("Person").setAncestor(acmeKey).setFilter(ageFilter);
+
+ // Returns tom but not lucy, because her age is unindexed
+ List results = datastore.prepare(q).asList(FetchOptions.Builder.withDefaults());
+ // [END gae_java21_datastore_unindexed_properties_1]
+
+ assertWithMessage("query results").that(results).containsExactly(tom);
+ }
+}
diff --git a/appengine-java21/ee8/datastore/src/test/java/com/example/appengine/ListPeopleServletTest.java b/appengine-java21/ee8/datastore/src/test/java/com/example/appengine/ListPeopleServletTest.java
new file mode 100644
index 00000000000..b2dc94cafed
--- /dev/null
+++ b/appengine-java21/ee8/datastore/src/test/java/com/example/appengine/ListPeopleServletTest.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright 2016 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.appengine;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+import static org.mockito.Mockito.verify;
+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.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 com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig;
+import com.google.appengine.tools.development.testing.LocalServiceTestHelper;
+import com.google.common.collect.ImmutableList;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+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;
+
+/**
+ * Unit tests for {@link ListPeopleServlet}.
+ */
+@RunWith(JUnit4.class)
+public class ListPeopleServletTest {
+
+ private static final ImmutableList TEST_NAMES =
+ // Keep in alphabetical order, so this is the same as the query order.
+ ImmutableList.builder()
+ .add("Alpha")
+ .add("Bravo")
+ .add("Charlie")
+ .add("Delta")
+ .add("Echo")
+ .add("Foxtrot")
+ .add("Golf")
+ .add("Hotel")
+ .add("India")
+ .add("Juliett")
+ .add("Kilo")
+ .add("Lima")
+ .add("Mike")
+ .add("November")
+ .add("Oscar")
+ .add("Papa")
+ .add("Quebec")
+ .add("Romeo")
+ .add("Sierra")
+ .add("Tango")
+ .build();
+
+ // 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
+ private final LocalServiceTestHelper helper =
+ new LocalServiceTestHelper(
+ new LocalDatastoreServiceTestConfig()
+ .setDefaultHighRepJobPolicyUnappliedJobPercentage(0));
+
+ @Mock
+ private HttpServletRequest mockRequest;
+ @Mock
+ private HttpServletResponse mockResponse;
+ private StringWriter responseWriter;
+ private DatastoreService datastore;
+
+ private ListPeopleServlet servletUnderTest;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.openMocks(this);
+ helper.setUp();
+ datastore = DatastoreServiceFactory.getDatastoreService();
+
+ // Add test data.
+ ImmutableList.Builder people = ImmutableList.builder();
+ for (String name : TEST_NAMES) {
+ people.add(createPerson(name));
+ }
+ datastore.put(people.build());
+
+ // Set up a fake HTTP response.
+ responseWriter = new StringWriter();
+ when(mockResponse.getWriter()).thenReturn(new PrintWriter(responseWriter));
+
+ servletUnderTest = new ListPeopleServlet();
+ }
+
+ @After
+ public void tearDown() {
+ helper.tearDown();
+ }
+
+ private Entity createPerson(String name) {
+ Entity person = new Entity("Person");
+ person.setProperty("name", name);
+ return person;
+ }
+
+ @Test
+ public void doGet_noCursor_writesNames() throws Exception {
+ servletUnderTest.doGet(mockRequest, mockResponse);
+
+ String response = responseWriter.toString();
+ for (int i = 0; i < ListPeopleServlet.PAGE_SIZE; i++) {
+ assertWithMessage("ListPeopleServlet response").that(response).contains(TEST_NAMES.get(i));
+ }
+ }
+
+ private String getFirstCursor() {
+ Query q = new Query("Person").addSort("name", SortDirection.ASCENDING);
+ PreparedQuery pq = datastore.prepare(q);
+ FetchOptions fetchOptions = FetchOptions.Builder.withLimit(ListPeopleServlet.PAGE_SIZE);
+ QueryResultList results = pq.asQueryResultList(fetchOptions);
+ return results.getCursor().toWebSafeString();
+ }
+
+ @Test
+ public void doGet_withValidCursor_writesNames() throws Exception {
+ when(mockRequest.getParameter("cursor")).thenReturn(getFirstCursor());
+
+ servletUnderTest.doGet(mockRequest, mockResponse);
+
+ String response = responseWriter.toString();
+ int i = 0;
+ while (i + ListPeopleServlet.PAGE_SIZE < TEST_NAMES.size() && i < ListPeopleServlet.PAGE_SIZE) {
+ assertWithMessage("ListPeopleServlet response")
+ .that(response)
+ .contains(TEST_NAMES.get(i + ListPeopleServlet.PAGE_SIZE));
+ i++;
+ }
+ }
+
+ @Test
+ public void doGet_withInvalidCursor_writesRedirect() throws Exception {
+ when(mockRequest.getParameter("cursor")).thenReturn("ThisCursorIsTotallyInvalid");
+ servletUnderTest.doGet(mockRequest, mockResponse);
+ verify(mockResponse).sendRedirect("/people");
+ }
+}
diff --git a/appengine-java21/ee8/datastore/src/test/java/com/example/appengine/MetadataEntityGroupTest.java b/appengine-java21/ee8/datastore/src/test/java/com/example/appengine/MetadataEntityGroupTest.java
new file mode 100644
index 00000000000..18d8ac9eff3
--- /dev/null
+++ b/appengine-java21/ee8/datastore/src/test/java/com/example/appengine/MetadataEntityGroupTest.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright 2016 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.appengine;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.appengine.api.datastore.DatastoreService;
+import com.google.appengine.api.datastore.DatastoreServiceFactory;
+import com.google.appengine.api.datastore.Entities;
+import com.google.appengine.api.datastore.Entity;
+import com.google.appengine.api.datastore.EntityNotFoundException;
+import com.google.appengine.api.datastore.FetchOptions;
+import com.google.appengine.api.datastore.Key;
+import com.google.appengine.api.datastore.PreparedQuery;
+import com.google.appengine.api.datastore.Query;
+import com.google.appengine.api.datastore.Transaction;
+import com.google.appengine.api.memcache.MemcacheService;
+import com.google.appengine.api.memcache.MemcacheServiceFactory;
+import com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig;
+import com.google.appengine.tools.development.testing.LocalMemcacheServiceTestConfig;
+import com.google.appengine.tools.development.testing.LocalServiceTestHelper;
+import java.io.PrintWriter;
+import java.io.Serializable;
+import java.io.StringWriter;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Unit tests to demonstrate App Engine Datastore entity group metadata.
+ */
+@RunWith(JUnit4.class)
+public class MetadataEntityGroupTest {
+
+ // 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
+ private final LocalServiceTestHelper helper =
+ new LocalServiceTestHelper(
+ new LocalDatastoreServiceTestConfig().setDefaultHighRepJobPolicyUnappliedJobPercentage(0),
+ new LocalMemcacheServiceTestConfig());
+
+ private DatastoreService datastore;
+
+ @Before
+ public void setUp() {
+ helper.setUp();
+ datastore = DatastoreServiceFactory.getDatastoreService();
+ }
+
+ @After
+ public void tearDown() {
+ helper.tearDown();
+ }
+
+ // [START gae_java21_datastore_entity_group_1]
+ private static long getEntityGroupVersion(DatastoreService ds, Transaction tx, Key entityKey) {
+ try {
+ return Entities.getVersionProperty(ds.get(tx, Entities.createEntityGroupKey(entityKey)));
+ } catch (EntityNotFoundException e) {
+ // No entity group information, return a value strictly smaller than any
+ // possible version
+ return 0;
+ }
+ }
+
+ private static void printEntityGroupVersions(DatastoreService ds, PrintWriter writer) {
+ Entity entity1 = new Entity("Simple");
+ Key key1 = ds.put(entity1);
+ Key entityGroupKey = Entities.createEntityGroupKey(key1);
+
+ // Print entity1's entity group version
+ writer.println("version " + getEntityGroupVersion(ds, null, key1));
+
+ // Write to a different entity group
+ Entity entity2 = new Entity("Simple");
+ ds.put(entity2);
+
+ // Will print the same version, as entity1's entity group has not changed
+ writer.println("version " + getEntityGroupVersion(ds, null, key1));
+
+ // Change entity1's entity group by adding a new child entity
+ Entity entity3 = new Entity("Simple", entity1.getKey());
+ ds.put(entity3);
+
+ // Will print a higher version, as entity1's entity group has changed
+ writer.println("version " + getEntityGroupVersion(ds, null, key1));
+ }
+ // [END gae_java21_datastore_entity_group_1]
+
+ @Test
+ public void printEntityGroupVersions_printsVersions() throws Exception {
+ StringWriter responseWriter = new StringWriter();
+ printEntityGroupVersions(datastore, new PrintWriter(responseWriter));
+ assertThat(responseWriter.toString()).contains("version");
+ }
+
+ // [START gae_java21_datastore_entity_group_2]
+ // A simple class for tracking consistent entity group counts.
+ private static class EntityGroupCount implements Serializable {
+
+ long version; // Version of the entity group whose count we are tracking
+ int count;
+
+ EntityGroupCount(long version, int count) {
+ this.version = version;
+ this.count = count;
+ }
+
+ // Display count of entities in an entity group, with consistent caching
+ void showEntityGroupCount(
+ DatastoreService ds, MemcacheService cache, PrintWriter writer, Key entityGroupKey) {
+ EntityGroupCount egCount = (EntityGroupCount) cache.get(entityGroupKey);
+ // Reuses getEntityGroupVersion method from the previous example.
+ if (egCount != null && egCount.version == getEntityGroupVersion(ds, null, entityGroupKey)) {
+ // Cached value matched current entity group version, use that
+ writer.println(egCount.count + " entities (cached)");
+ } else {
+ // Need to actually count entities. Using a transaction to get a consistent count
+ // and entity group version.
+ Transaction tx = ds.beginTransaction();
+ PreparedQuery pq = ds.prepare(tx, new Query(entityGroupKey));
+ int count = pq.countEntities(FetchOptions.Builder.withLimit(5000));
+ cache.put(
+ entityGroupKey,
+ new EntityGroupCount(getEntityGroupVersion(ds, tx, entityGroupKey), count));
+ tx.rollback();
+ writer.println(count + " entities");
+ }
+ }
+ }
+ // [END gae_java21_datastore_entity_group_2]
+
+ @Test
+ public void entityGroupCount_printsCount() throws Exception {
+ StringWriter responseWriter = new StringWriter();
+ MemcacheService cache = MemcacheServiceFactory.getMemcacheService();
+ Entity entity1 = new Entity("Simple");
+ Key key1 = datastore.put(entity1);
+ Key entityGroupKey = Entities.createEntityGroupKey(key1);
+
+ EntityGroupCount groupCount = new EntityGroupCount(0, 0);
+ groupCount.showEntityGroupCount(
+ datastore, cache, new PrintWriter(responseWriter), entityGroupKey);
+
+ assertThat(responseWriter.toString()).contains(" entities");
+ }
+}
diff --git a/appengine-java21/ee8/datastore/src/test/java/com/example/appengine/MetadataKindsTest.java b/appengine-java21/ee8/datastore/src/test/java/com/example/appengine/MetadataKindsTest.java
new file mode 100644
index 00000000000..68394559c6d
--- /dev/null
+++ b/appengine-java21/ee8/datastore/src/test/java/com/example/appengine/MetadataKindsTest.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2016 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.appengine;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.appengine.api.datastore.DatastoreService;
+import com.google.appengine.api.datastore.DatastoreServiceFactory;
+import com.google.appengine.api.datastore.Entities;
+import com.google.appengine.api.datastore.Entity;
+import com.google.appengine.api.datastore.Query;
+import com.google.appengine.api.datastore.Query.CompositeFilterOperator;
+import com.google.appengine.api.datastore.Query.Filter;
+import com.google.appengine.api.datastore.Query.FilterOperator;
+import com.google.appengine.api.datastore.Query.FilterPredicate;
+import com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig;
+import com.google.appengine.tools.development.testing.LocalServiceTestHelper;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.List;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Unit tests to demonstrate App Engine Datastore kinds metadata.
+ */
+@RunWith(JUnit4.class)
+public class MetadataKindsTest {
+
+ 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 StringWriter responseWriter;
+ private DatastoreService datastore;
+
+ @Before
+ public void setUp() {
+ helper.setUp();
+ datastore = DatastoreServiceFactory.getDatastoreService();
+ responseWriter = new StringWriter();
+ }
+
+ @After
+ public void tearDown() {
+ helper.tearDown();
+ }
+
+ // [START gae_java21_datastore_kind_query]
+ void printLowercaseKinds(DatastoreService ds, PrintWriter writer) {
+
+ // Start with unrestricted kind query
+ Query q = new Query(Entities.KIND_METADATA_KIND);
+
+ List subFils = new ArrayList<>();
+
+ // Limit to lowercase initial letters
+ subFils.add(
+ new FilterPredicate(
+ Entity.KEY_RESERVED_PROPERTY,
+ FilterOperator.GREATER_THAN_OR_EQUAL,
+ Entities.createKindKey("a")));
+
+ String endChar = Character.toString((char) ('z' + 1)); // Character after 'z'
+
+ subFils.add(
+ new FilterPredicate(
+ Entity.KEY_RESERVED_PROPERTY,
+ FilterOperator.LESS_THAN,
+ Entities.createKindKey(endChar)));
+
+ q.setFilter(CompositeFilterOperator.and(subFils));
+
+ // Print heading
+ writer.println("Lowercase kinds:");
+
+ // Print query results
+ for (Entity e : ds.prepare(q).asIterable()) {
+ writer.println(" " + e.getKey().getName());
+ }
+ }
+ // [END gae_java21_datastore_kind_query]
+
+ @Test
+ public void printLowercaseKinds_printsKinds() throws Exception {
+ datastore.put(new Entity("alpha"));
+ datastore.put(new Entity("beta"));
+ datastore.put(new Entity("NotIncluded"));
+ datastore.put(new Entity("zed"));
+
+ printLowercaseKinds(datastore, new PrintWriter(responseWriter));
+
+ String response = responseWriter.toString();
+ assertThat(response).contains("alpha");
+ assertThat(response).contains("beta");
+ assertThat(response).contains("zed");
+ assertThat(response).doesNotContain("NotIncluded");
+ }
+}
diff --git a/appengine-java21/ee8/datastore/src/test/java/com/example/appengine/MetadataNamespacesTest.java b/appengine-java21/ee8/datastore/src/test/java/com/example/appengine/MetadataNamespacesTest.java
new file mode 100644
index 00000000000..04924853263
--- /dev/null
+++ b/appengine-java21/ee8/datastore/src/test/java/com/example/appengine/MetadataNamespacesTest.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright 2016 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.appengine;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.appengine.api.NamespaceManager;
+import com.google.appengine.api.datastore.DatastoreService;
+import com.google.appengine.api.datastore.DatastoreServiceFactory;
+import com.google.appengine.api.datastore.Entities;
+import com.google.appengine.api.datastore.Entity;
+import com.google.appengine.api.datastore.Query;
+import com.google.appengine.api.datastore.Query.CompositeFilterOperator;
+import com.google.appengine.api.datastore.Query.Filter;
+import com.google.appengine.api.datastore.Query.FilterOperator;
+import com.google.appengine.api.datastore.Query.FilterPredicate;
+import com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig;
+import com.google.appengine.tools.development.testing.LocalServiceTestHelper;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.List;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Unit tests to demonstrate App Engine Datastore namespaces metadata.
+ */
+@RunWith(JUnit4.class)
+public class MetadataNamespacesTest {
+
+ 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 StringWriter responseWriter;
+ private DatastoreService datastore;
+
+ @Before
+ public void setUp() {
+ helper.setUp();
+ datastore = DatastoreServiceFactory.getDatastoreService();
+ responseWriter = new StringWriter();
+ }
+
+ @After
+ public void tearDown() {
+ helper.tearDown();
+ }
+
+ // [START gae_java21_datastore_queries_intro]
+ void printAllNamespaces(DatastoreService ds, PrintWriter writer) {
+ Query q = new Query(Entities.NAMESPACE_METADATA_KIND);
+
+ for (Entity e : ds.prepare(q).asIterable()) {
+ // A nonzero numeric id denotes the default namespace;
+ // see Namespace Queries, below
+ if (e.getKey().getId() != 0) {
+ writer.println("");
+ } else {
+ writer.println(e.getKey().getName());
+ }
+ }
+ }
+ // [END gae_java21_datastore_queries_intro]
+
+ @Test
+ public void printAllNamespaces_printsNamespaces() throws Exception {
+ datastore.put(new Entity("Simple"));
+ NamespaceManager.set("another-namespace");
+ datastore.put(new Entity("Simple"));
+
+ printAllNamespaces(datastore, new PrintWriter(responseWriter));
+
+ String response = responseWriter.toString();
+ assertThat(response).contains("");
+ assertThat(response).contains("another-namespace");
+ }
+
+ // [START gae_java21_datastore_namespace_query]
+ List getNamespaces(DatastoreService ds, String start, String end) {
+
+ // Start with unrestricted namespace query
+ Query q = new Query(Entities.NAMESPACE_METADATA_KIND);
+ List subFilters = new ArrayList<>();
+ // Limit to specified range, if any
+ if (start != null) {
+ subFilters.add(
+ new FilterPredicate(
+ Entity.KEY_RESERVED_PROPERTY,
+ FilterOperator.GREATER_THAN_OR_EQUAL,
+ Entities.createNamespaceKey(start)));
+ }
+ if (end != null) {
+ subFilters.add(
+ new FilterPredicate(
+ Entity.KEY_RESERVED_PROPERTY,
+ FilterOperator.LESS_THAN_OR_EQUAL,
+ Entities.createNamespaceKey(end)));
+ }
+
+ q.setFilter(CompositeFilterOperator.and(subFilters));
+
+ // Initialize result list
+ List results = new ArrayList<>();
+
+ // Build list of query results
+ for (Entity e : ds.prepare(q).asIterable()) {
+ results.add(Entities.getNamespaceFromNamespaceKey(e.getKey()));
+ }
+
+ // Return result list
+ return results;
+ }
+ // [END gae_java21_datastore_namespace_query]
+
+ @Test
+ public void getNamespaces_returnsNamespaces() throws Exception {
+ NamespaceManager.set("alpha");
+ datastore.put(new Entity("Simple"));
+ NamespaceManager.set("bravo");
+ datastore.put(new Entity("Simple"));
+ NamespaceManager.set("charlie");
+ datastore.put(new Entity("Simple"));
+ NamespaceManager.set("zed");
+ datastore.put(new Entity("Simple"));
+
+ List results = getNamespaces(datastore, "bravo", "echo");
+
+ assertThat(results).containsExactly("bravo", "charlie");
+ }
+}
diff --git a/appengine-java21/ee8/datastore/src/test/java/com/example/appengine/MetadataPropertiesTest.java b/appengine-java21/ee8/datastore/src/test/java/com/example/appengine/MetadataPropertiesTest.java
new file mode 100644
index 00000000000..5386f42cfe5
--- /dev/null
+++ b/appengine-java21/ee8/datastore/src/test/java/com/example/appengine/MetadataPropertiesTest.java
@@ -0,0 +1,235 @@
+/*
+ * Copyright 2016 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.appengine;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.appengine.api.datastore.DatastoreService;
+import com.google.appengine.api.datastore.DatastoreServiceFactory;
+import com.google.appengine.api.datastore.Entities;
+import com.google.appengine.api.datastore.Entity;
+import com.google.appengine.api.datastore.Query;
+import com.google.appengine.api.datastore.Query.CompositeFilterOperator;
+import com.google.appengine.api.datastore.Query.FilterPredicate;
+import com.google.appengine.api.datastore.Query.SortDirection;
+import com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig;
+import com.google.appengine.tools.development.testing.LocalServiceTestHelper;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Date;
+import java.util.List;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Unit tests to demonstrate App Engine Datastore properties metadata.
+ */
+@RunWith(JUnit4.class)
+public class MetadataPropertiesTest {
+
+ 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 StringWriter responseWriter;
+ private DatastoreService datastore;
+
+ @Before
+ public void setUp() {
+ helper.setUp();
+ datastore = DatastoreServiceFactory.getDatastoreService();
+ responseWriter = new StringWriter();
+ }
+
+ @After
+ public void tearDown() {
+ helper.tearDown();
+ }
+
+ // [START gae_java21_datastore_property_query_example]
+ void printProperties(DatastoreService ds, PrintWriter writer) {
+
+ // Create unrestricted keys-only property query
+ Query q = new Query(Entities.PROPERTY_METADATA_KIND).setKeysOnly();
+
+ // Print query results
+ for (Entity e : ds.prepare(q).asIterable()) {
+ writer.println(e.getKey().getParent().getName() + ": " + e.getKey().getName());
+ }
+ }
+ // [END gae_java21_datastore_property_query_example]
+
+ @Test
+ public void printProperties_printsProperties() throws Exception {
+ Entity a = new Entity("Widget");
+ a.setProperty("combobulators", 2);
+ a.setProperty("oscillatorState", "harmonzing");
+ Entity b = new Entity("Ship");
+ b.setProperty("sails", 2);
+ b.setProperty("captain", "Blackbeard");
+ Entity c = new Entity("Ship");
+ c.setProperty("captain", "Redbeard");
+ c.setProperty("motor", "outboard");
+ datastore.put(Arrays.asList(a, b, c));
+
+ printProperties(datastore, new PrintWriter(responseWriter));
+
+ String response = responseWriter.toString();
+ assertThat(response).contains("Widget: combobulators");
+ assertThat(response).contains("Widget: oscillatorState");
+ assertThat(response).contains("Ship: sails");
+ assertThat(response).contains("Ship: captain");
+ assertThat(response).contains("Ship: motor");
+ }
+
+ // [START gae_java21_datastore_property_filtering_example]
+ void printPropertyRange(DatastoreService ds, PrintWriter writer) {
+
+ // Start with unrestricted keys-only property query
+ Query q = new Query(Entities.PROPERTY_METADATA_KIND).setKeysOnly();
+
+ // Limit range
+ q.setFilter(
+ CompositeFilterOperator.and(
+ new FilterPredicate(
+ Entity.KEY_RESERVED_PROPERTY,
+ Query.FilterOperator.GREATER_THAN_OR_EQUAL,
+ Entities.createPropertyKey("Employee", "salary")),
+ new FilterPredicate(
+ Entity.KEY_RESERVED_PROPERTY,
+ Query.FilterOperator.LESS_THAN_OR_EQUAL,
+ Entities.createPropertyKey("Manager", "salary"))));
+ q.addSort(Entity.KEY_RESERVED_PROPERTY, SortDirection.ASCENDING);
+
+ // Print query results
+ for (Entity e : ds.prepare(q).asIterable()) {
+ writer.println(e.getKey().getParent().getName() + ": " + e.getKey().getName());
+ }
+ }
+ // [END gae_java21_datastore_property_filtering_example]
+
+ @Test
+ public void printPropertyRange_printsProperties() throws Exception {
+ Entity account = new Entity("Account");
+ account.setProperty("balance", "10.30");
+ account.setProperty("company", "General Company");
+ Entity employee = new Entity("Employee");
+ employee.setProperty("name", "John Doe");
+ employee.setProperty("ssn", "987-65-4321");
+ Entity invoice = new Entity("Invoice");
+ invoice.setProperty("date", new Date());
+ invoice.setProperty("amount", "99.98");
+ Entity manager = new Entity("Manager");
+ manager.setProperty("name", "Jane Doe");
+ manager.setProperty("title", "Technical Director");
+ Entity product = new Entity("Product");
+ product.setProperty("description", "Widget to re-ionize an oscillator");
+ product.setProperty("price", "19.97");
+ datastore.put(Arrays.asList(account, employee, invoice, manager, product));
+
+ printPropertyRange(datastore, new PrintWriter(responseWriter));
+
+ String response = responseWriter.toString();
+ assertThat(response)
+ .isEqualTo("Employee: ssn\nInvoice: amount\nInvoice: date\nManager: name\n");
+ }
+
+ // [START gae_java21_datastore_property_ancestor_query_example]
+ List propertiesOfKind(DatastoreService ds, String kind) {
+
+ // Start with unrestricted keys-only property query
+ Query q = new Query(Entities.PROPERTY_METADATA_KIND).setKeysOnly();
+
+ // Limit to specified kind
+ q.setAncestor(Entities.createKindKey(kind));
+
+ // Initialize result list
+ ArrayList results = new ArrayList<>();
+
+ //Build list of query results
+ for (Entity e : ds.prepare(q).asIterable()) {
+ results.add(e.getKey().getName());
+ }
+
+ // Return result list
+ return results;
+ }
+ // [END gae_java21_datastore_property_ancestor_query_example]
+
+ @Test
+ public void propertiesOfKind_returnsProperties() throws Exception {
+ Entity a = new Entity("Alpha");
+ a.setProperty("beta", 12);
+ a.setProperty("charlie", "misc.");
+ Entity b = new Entity("Alpha");
+ b.setProperty("charlie", "assorted");
+ b.setProperty("delta", new Date());
+ Entity c = new Entity("Charlie");
+ c.setProperty("charlie", "some");
+ c.setProperty("echo", new Date());
+ datastore.put(Arrays.asList(a, b, c));
+
+ List properties = propertiesOfKind(datastore, "Alpha");
+
+ assertThat(properties).containsExactly("beta", "charlie", "delta");
+ }
+
+ // [START gae_java21_datastore_property_representation_query_example]
+ Collection representationsOfProperty(DatastoreService ds, String kind, String property) {
+
+ // Start with unrestricted non-keys-only property query
+ Query q = new Query(Entities.PROPERTY_METADATA_KIND);
+
+ // Limit to specified kind and property
+ q.setFilter(
+ new FilterPredicate(
+ "__key__", Query.FilterOperator.EQUAL, Entities.createPropertyKey(kind, property)));
+
+ // Get query result
+ Entity propInfo = ds.prepare(q).asSingleEntity();
+
+ // Return collection of property representations
+ return (Collection) propInfo.getProperty("property_representation");
+ }
+ // [END gae_java21_datastore_property_representation_query_example]
+
+ @Test
+ public void representationsOfProperty_returnsRepresentations() throws Exception {
+ Entity a = new Entity("Alpha");
+ a.setProperty("beta", 12);
+ Entity b = new Entity("Alpha");
+ b.setProperty("beta", true);
+ Entity c = new Entity("Alpha");
+ c.setProperty("beta", new Date());
+ datastore.put(Arrays.asList(a, b, c));
+
+ Collection results = representationsOfProperty(datastore, "Alpha", "beta");
+
+ assertThat(results).containsExactly("INT64", "BOOLEAN");
+ }
+}
diff --git a/appengine-java21/ee8/datastore/src/test/java/com/example/appengine/ProjectionServletTest.java b/appengine-java21/ee8/datastore/src/test/java/com/example/appengine/ProjectionServletTest.java
new file mode 100644
index 00000000000..42e5574da0e
--- /dev/null
+++ b/appengine-java21/ee8/datastore/src/test/java/com/example/appengine/ProjectionServletTest.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2016 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.appengine;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+import static org.mockito.Mockito.when;
+
+import com.example.time.testing.FakeClock;
+import com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig;
+import com.google.appengine.tools.development.testing.LocalServiceTestHelper;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+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;
+
+/**
+ * Unit tests for {@link ProjectionServlet}.
+ */
+@RunWith(JUnit4.class)
+public class ProjectionServletTest {
+
+ private final LocalServiceTestHelper helper =
+ new LocalServiceTestHelper(new LocalDatastoreServiceTestConfig());
+
+ @Mock
+ private HttpServletRequest mockRequest;
+ @Mock
+ private HttpServletResponse mockResponse;
+ private StringWriter responseWriter;
+ private ProjectionServlet servletUnderTest;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.openMocks(this);
+ helper.setUp();
+
+ // Set up a fake HTTP response.
+ responseWriter = new StringWriter();
+ when(mockResponse.getWriter()).thenReturn(new PrintWriter(responseWriter));
+
+ servletUnderTest = new ProjectionServlet();
+ }
+
+ @After
+ public void tearDown() {
+ helper.tearDown();
+ }
+
+ @Test
+ public void doGet_emptyDatastore_writesNoGreetings() throws Exception {
+ servletUnderTest.doGet(mockRequest, mockResponse);
+
+ assertWithMessage("ProjectionServlet response")
+ .that(responseWriter.toString())
+ .doesNotContain("Message");
+ }
+
+ @Test
+ public void doGet_manyGreetings_writesLatestGreetings() throws Exception {
+ // Arrange
+ GuestbookStrong guestbook =
+ new GuestbookStrong(GuestbookStrongServlet.GUESTBOOK_ID, new FakeClock());
+ guestbook.appendGreeting("Hello.");
+ guestbook.appendGreeting("Güten Tag!");
+ guestbook.appendGreeting("Hi.");
+ guestbook.appendGreeting("Hola.");
+
+ // Act
+ servletUnderTest.doGet(mockRequest, mockResponse);
+ String output = responseWriter.toString();
+
+ assertWithMessage("ProjectionServlet response").that(output).contains("Message Hello.");
+ assertWithMessage("ProjectionServlet response").that(output).contains("Message Güten Tag!");
+ assertWithMessage("ProjectionServlet response").that(output).contains("Message Hola.");
+ }
+}
diff --git a/appengine-java21/ee8/datastore/src/test/java/com/example/appengine/ProjectionTest.java b/appengine-java21/ee8/datastore/src/test/java/com/example/appengine/ProjectionTest.java
new file mode 100644
index 00000000000..e1ad82e84f5
--- /dev/null
+++ b/appengine-java21/ee8/datastore/src/test/java/com/example/appengine/ProjectionTest.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2016 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.appengine;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+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.PropertyProjection;
+import com.google.appengine.api.datastore.Query;
+import com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig;
+import com.google.appengine.tools.development.testing.LocalServiceTestHelper;
+import java.util.List;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests to demonstrate App Engine Datastore projection queries. */
+@RunWith(JUnit4.class)
+public class ProjectionTest {
+
+ 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 DatastoreService datastore;
+
+ @Before
+ public void setUp() throws Exception {
+ helper.setUp();
+ datastore = DatastoreServiceFactory.getDatastoreService();
+ }
+
+ @After
+ public void tearDown() {
+ helper.tearDown();
+ }
+
+ @Test
+ public void projectionQuery_grouping_filtersDuplicates() {
+ putTestData("some duplicate", 0L);
+ putTestData("some duplicate", 0L);
+ putTestData("too big", 1L);
+
+ // [START gae_java21_datastore_grouping]
+ Query q = new Query("TestKind");
+ q.addProjection(new PropertyProjection("A", String.class));
+ q.addProjection(new PropertyProjection("B", Long.class));
+ q.setDistinct(true);
+ q.setFilter(Query.FilterOperator.LESS_THAN.of("B", 1L));
+ q.addSort("B", Query.SortDirection.DESCENDING);
+ q.addSort("A");
+ // [END gae_java21_datastore_grouping]
+
+ List entities = datastore.prepare(q).asList(FetchOptions.Builder.withLimit(5));
+ assertThat(entities).hasSize(1);
+ Entity entity = entities.get(0);
+ assertWithMessage("entity.A")
+ .that((String) entity.getProperty("A"))
+ .isEqualTo("some duplicate");
+ assertWithMessage("entity.B").that((long) entity.getProperty("B")).isEqualTo(0L);
+ }
+
+ private void putTestData(String a, long b) {
+ Entity entity = new Entity("TestKind");
+ entity.setProperty("A", a);
+ entity.setProperty("B", b);
+ datastore.put(entity);
+ }
+}
diff --git a/appengine-java21/ee8/datastore/src/test/java/com/example/appengine/QueriesTest.java b/appengine-java21/ee8/datastore/src/test/java/com/example/appengine/QueriesTest.java
new file mode 100644
index 00000000000..6cb2e03d13d
--- /dev/null
+++ b/appengine-java21/ee8/datastore/src/test/java/com/example/appengine/QueriesTest.java
@@ -0,0 +1,844 @@
+/*
+ * Copyright 2016 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.appengine;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+import static org.junit.Assert.fail;
+
+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.PreparedQuery;
+import com.google.appengine.api.datastore.PreparedQuery.TooManyResultsException;
+import com.google.appengine.api.datastore.Query;
+import com.google.appengine.api.datastore.Query.CompositeFilter;
+import com.google.appengine.api.datastore.Query.CompositeFilterOperator;
+import com.google.appengine.api.datastore.Query.Filter;
+import com.google.appengine.api.datastore.Query.FilterOperator;
+import com.google.appengine.api.datastore.Query.FilterPredicate;
+import com.google.appengine.api.datastore.Query.SortDirection;
+import com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig;
+import com.google.appengine.tools.development.testing.LocalServiceTestHelper;
+import com.google.common.collect.ImmutableList;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.Arrays;
+import java.util.List;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests to demonstrate App Engine Datastore queries. */
+@RunWith(JUnit4.class)
+public class QueriesTest {
+
+ 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 DatastoreService datastore;
+
+ @Before
+ public void setUp() {
+ helper.setUp();
+ datastore = DatastoreServiceFactory.getDatastoreService();
+ }
+
+ @After
+ public void tearDown() {
+ helper.tearDown();
+ }
+
+ @Test
+ public void propertyFilterExample_returnsMatchingEntities() throws Exception {
+ // Arrange
+ Entity p1 = new Entity("Person");
+ p1.setProperty("height", 120);
+ Entity p2 = new Entity("Person");
+ p2.setProperty("height", 180);
+ Entity p3 = new Entity("Person");
+ p3.setProperty("height", 160);
+ datastore.put(ImmutableList.of(p1, p2, p3));
+
+ // Act
+ long minHeight = 160;
+ // [START gae_java21_datastore_datastore_property_filter]]
+ Filter propertyFilter =
+ new FilterPredicate("height", FilterOperator.GREATER_THAN_OR_EQUAL, minHeight);
+ Query q = new Query("Person").setFilter(propertyFilter);
+ // [END gae_java21_datastore_datastore_property_filter]]
+
+ // Assert
+ List results =
+ datastore.prepare(q.setKeysOnly()).asList(FetchOptions.Builder.withDefaults());
+ assertWithMessage("query results").that(results).containsExactly(p2, p3);
+ }
+
+ @Test
+ public void keyFilterExample_returnsMatchingEntities() throws Exception {
+ // Arrange
+ Entity a = new Entity("Person", "a");
+ Entity b = new Entity("Person", "b");
+ Entity c = new Entity("Person", "c");
+ Entity aa = new Entity("Person", "aa", b.getKey());
+ Entity bb = new Entity("Person", "bb", b.getKey());
+ Entity aaa = new Entity("Person", "aaa", bb.getKey());
+ Entity bbb = new Entity("Person", "bbb", bb.getKey());
+ datastore.put(ImmutableList.of(a, b, c, aa, bb, aaa, bbb));
+
+ // Act
+ Key lastSeenKey = bb.getKey();
+ // [START gae_java21_datastore_datastore_key_filter]]
+ Filter keyFilter =
+ new FilterPredicate(Entity.KEY_RESERVED_PROPERTY, FilterOperator.GREATER_THAN, lastSeenKey);
+ Query q = new Query("Person").setFilter(keyFilter);
+ // [END gae_java21_datastore_datastore_key_filter]]
+
+ // Assert
+ List results =
+ datastore.prepare(q.setKeysOnly()).asList(FetchOptions.Builder.withDefaults());
+ assertWithMessage("query results")
+ .that(results)
+ .containsExactly(
+ aaa, // Ancestor path "b/bb/aaa" is greater than "b/bb".
+ bbb, // Ancestor path "b/bb/bbb" is greater than "b/bb".
+ c); // Key name identifier "c" is greater than b.
+ }
+
+ @Test
+ public void keyFilterExample_kindless_returnsMatchingEntities() throws Exception {
+ // Arrange
+ Entity a = new Entity("Child", "a");
+ Entity b = new Entity("Child", "b");
+ Entity c = new Entity("Child", "c");
+ Entity aa = new Entity("Child", "aa", b.getKey());
+ Entity bb = new Entity("Child", "bb", b.getKey());
+ Entity aaa = new Entity("Child", "aaa", bb.getKey());
+ Entity bbb = new Entity("Child", "bbb", bb.getKey());
+ Entity adult = new Entity("Adult", "a");
+ Entity zooAnimal = new Entity("ZooAnimal", "a");
+ datastore.put(ImmutableList.of(a, b, c, aa, bb, aaa, bbb, adult, zooAnimal));
+
+ // Act
+ Key lastSeenKey = bb.getKey();
+ // [START gae_java21_datastore_kindless_query]
+ Filter keyFilter =
+ new FilterPredicate(Entity.KEY_RESERVED_PROPERTY, FilterOperator.GREATER_THAN, lastSeenKey);
+ Query q = new Query().setFilter(keyFilter);
+ // [END gae_java21_datastore_kindless_query]
+
+ // Assert
+ List results =
+ datastore.prepare(q.setKeysOnly()).asList(FetchOptions.Builder.withDefaults());
+ assertWithMessage("query results")
+ .that(results)
+ .containsExactly(
+ aaa, // Ancestor path "b/bb/aaa" is greater than "b/bb".
+ bbb, // Ancestor path "b/bb/bbb" is greater than "b/bb".
+ zooAnimal, // Kind "ZooAnimal" is greater than "Child"
+ c); // Key name identifier "c" is greater than b.
+ }
+
+ @Test
+ public void ancestorFilterExample_returnsMatchingEntities() throws Exception {
+ Entity a = new Entity("Person", "a");
+ Entity b = new Entity("Person", "b");
+ Entity aa = new Entity("Person", "aa", a.getKey());
+ Entity ab = new Entity("Person", "ab", a.getKey());
+ Entity bb = new Entity("Person", "bb", b.getKey());
+ datastore.put(ImmutableList.of(a, b, aa, ab, bb));
+
+ Key ancestorKey = a.getKey();
+ // [START gae_java21_datastore_ancestor_filter]
+ Query q = new Query("Person").setAncestor(ancestorKey);
+ // [END gae_java21_datastore_ancestor_filter]
+
+ // Assert
+ List results =
+ datastore.prepare(q.setKeysOnly()).asList(FetchOptions.Builder.withDefaults());
+ assertWithMessage("query results").that(results).containsExactly(a, aa, ab);
+ }
+
+ @Test
+ public void ancestorQueryExample_returnsMatchingEntities() throws Exception {
+ // [START gae_java21_datastore_ancestor_query]
+ DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
+
+ Entity tom = new Entity("Person", "Tom");
+ Key tomKey = tom.getKey();
+ datastore.put(tom);
+
+ Entity weddingPhoto = new Entity("Photo", tomKey);
+ weddingPhoto.setProperty("imageURL", "http://domain.com/some/path/to/wedding_photo.jpg");
+
+ Entity babyPhoto = new Entity("Photo", tomKey);
+ babyPhoto.setProperty("imageURL", "http://domain.com/some/path/to/baby_photo.jpg");
+
+ Entity dancePhoto = new Entity("Photo", tomKey);
+ dancePhoto.setProperty("imageURL", "http://domain.com/some/path/to/dance_photo.jpg");
+
+ Entity campingPhoto = new Entity("Photo");
+ campingPhoto.setProperty("imageURL", "http://domain.com/some/path/to/camping_photo.jpg");
+
+ List photoList = Arrays.asList(weddingPhoto, babyPhoto, dancePhoto, campingPhoto);
+ datastore.put(photoList);
+
+ Query photoQuery = new Query("Photo").setAncestor(tomKey);
+
+ // This returns weddingPhoto, babyPhoto, and dancePhoto,
+ // but not campingPhoto, because tom is not an ancestor
+ List results =
+ datastore.prepare(photoQuery).asList(FetchOptions.Builder.withDefaults());
+ // [END gae_java21_datastore_ancestor_query]
+
+ assertWithMessage("query results")
+ .that(results)
+ .containsExactly(weddingPhoto, babyPhoto, dancePhoto);
+ }
+
+ @Test
+ public void ancestorQueryExample_kindlessKeyFilter_returnsMatchingEntities() throws Exception {
+ // Arrange
+ Entity a = new Entity("Grandparent", "a");
+ Entity b = new Entity("Grandparent", "b");
+ Entity c = new Entity("Grandparent", "c");
+ Entity aa = new Entity("Parent", "aa", a.getKey());
+ Entity ba = new Entity("Parent", "ba", b.getKey());
+ Entity bb = new Entity("Parent", "bb", b.getKey());
+ Entity bc = new Entity("Parent", "bc", b.getKey());
+ Entity cc = new Entity("Parent", "cc", c.getKey());
+ Entity aaa = new Entity("Child", "aaa", aa.getKey());
+ Entity bbb = new Entity("Child", "bbb", bb.getKey());
+ datastore.put(ImmutableList.of(a, b, c, aa, ba, bb, bc, cc, aaa, bbb));
+
+ // Act
+ Key ancestorKey = b.getKey();
+ Key lastSeenKey = bb.getKey();
+ // [START gae_java21_datastore_kindless_ancestor_key_query]
+ Filter keyFilter =
+ new FilterPredicate(Entity.KEY_RESERVED_PROPERTY, FilterOperator.GREATER_THAN, lastSeenKey);
+ Query q = new Query().setAncestor(ancestorKey).setFilter(keyFilter);
+ // [END gae_java21_datastore_kindless_ancestor_key_query]
+
+ // Assert
+ List results =
+ datastore.prepare(q.setKeysOnly()).asList(FetchOptions.Builder.withDefaults());
+ assertWithMessage("query results").that(results).containsExactly(bc, bbb);
+ }
+
+ @Test
+ public void ancestorQueryExample_kindlessKeyFilterFull_returnsMatchingEntities()
+ throws Exception {
+ // [START gae_java21_datastore_kindless_ancestor_query]
+ DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
+
+ Entity tom = new Entity("Person", "Tom");
+ Key tomKey = tom.getKey();
+ datastore.put(tom);
+
+ Entity weddingPhoto = new Entity("Photo", tomKey);
+ weddingPhoto.setProperty("imageURL", "http://domain.com/some/path/to/wedding_photo.jpg");
+
+ Entity weddingVideo = new Entity("Video", tomKey);
+ weddingVideo.setProperty("videoURL", "http://domain.com/some/path/to/wedding_video.avi");
+
+ List mediaList = Arrays.asList(weddingPhoto, weddingVideo);
+ datastore.put(mediaList);
+
+ // By default, ancestor queries include the specified ancestor itself.
+ // The following filter excludes the ancestor from the query results.
+ Filter keyFilter =
+ new FilterPredicate(Entity.KEY_RESERVED_PROPERTY, FilterOperator.GREATER_THAN, tomKey);
+
+ Query mediaQuery = new Query().setAncestor(tomKey).setFilter(keyFilter);
+
+ // Returns both weddingPhoto and weddingVideo,
+ // even though they are of different entity kinds
+ List results =
+ datastore.prepare(mediaQuery).asList(FetchOptions.Builder.withDefaults());
+ // [END gae_java21_datastore_kindless_ancestor_query]
+
+ assertWithMessage("query result keys")
+ .that(results)
+ .containsExactly(weddingPhoto, weddingVideo);
+ }
+
+ @Test
+ public void keysOnlyExample_returnsMatchingEntities() throws Exception {
+ // Arrange
+ Entity a = new Entity("Person", "a");
+ Entity b = new Entity("Building", "b");
+ Entity c = new Entity("Person", "c");
+ datastore.put(ImmutableList.of(a, b, c));
+
+ // [START gae_java21_datastore_keys_only]
+ Query q = new Query("Person").setKeysOnly();
+ // [END gae_java21_datastore_keys_only]
+
+ // Assert
+ List results = datastore.prepare(q).asList(FetchOptions.Builder.withDefaults());
+ assertWithMessage("query results").that(results).containsExactly(a, c);
+ }
+
+ @Test
+ public void sortOrderExample_returnsSortedEntities() throws Exception {
+ // Arrange
+ Entity a = new Entity("Person", "a");
+ a.setProperty("lastName", "Alpha");
+ a.setProperty("height", 100);
+ Entity b = new Entity("Person", "b");
+ b.setProperty("lastName", "Bravo");
+ b.setProperty("height", 200);
+ Entity c = new Entity("Person", "c");
+ c.setProperty("lastName", "Charlie");
+ c.setProperty("height", 300);
+ datastore.put(ImmutableList.of(a, b, c));
+
+ // Act
+ // [START gae_java21_datastore_sort_order]
+ // Order alphabetically by last name:
+ Query q1 = new Query("Person").addSort("lastName", SortDirection.ASCENDING);
+
+ // Order by height, tallest to shortest:
+ Query q2 = new Query("Person").addSort("height", SortDirection.DESCENDING);
+ // [END gae_java21_datastore_sort_order]
+
+ // Assert
+ List lastNameResults =
+ datastore.prepare(q1.setKeysOnly()).asList(FetchOptions.Builder.withDefaults());
+ assertWithMessage("last name query results")
+ .that(lastNameResults)
+ .containsExactly(a, b, c)
+ .inOrder();
+ List heightResults =
+ datastore.prepare(q2.setKeysOnly()).asList(FetchOptions.Builder.withDefaults());
+ assertWithMessage("height query results")
+ .that(heightResults)
+ .containsExactly(c, b, a)
+ .inOrder();
+ }
+
+ @Test
+ public void sortOrderExample_multipleSortOrders_returnsSortedEntities() throws Exception {
+ // Arrange
+ Entity a = new Entity("Person", "a");
+ a.setProperty("lastName", "Alpha");
+ a.setProperty("height", 100);
+ Entity b1 = new Entity("Person", "b1");
+ b1.setProperty("lastName", "Bravo");
+ b1.setProperty("height", 150);
+ Entity b2 = new Entity("Person", "b2");
+ b2.setProperty("lastName", "Bravo");
+ b2.setProperty("height", 200);
+ Entity c = new Entity("Person", "c");
+ c.setProperty("lastName", "Charlie");
+ c.setProperty("height", 300);
+ datastore.put(ImmutableList.of(a, b1, b2, c));
+
+ // Act
+ // [START gae_java21_datastore_multiple_sort_orders]
+ Query q =
+ new Query("Person")
+ .addSort("lastName", SortDirection.ASCENDING)
+ .addSort("height", SortDirection.DESCENDING);
+ // [END gae_java21_datastore_multiple_sort_orders]
+
+ // Assert
+ List results =
+ datastore.prepare(q.setKeysOnly()).asList(FetchOptions.Builder.withDefaults());
+ assertWithMessage("query results").that(results).containsExactly(a, b2, b1, c).inOrder();
+ }
+
+ @Test
+ public void queryInterface_multipleFilters_printsMatchedEntities() throws Exception {
+ // Arrange
+ Entity a = new Entity("Person", "a");
+ a.setProperty("firstName", "Alph");
+ a.setProperty("lastName", "Alpha");
+ a.setProperty("height", 60);
+ Entity b = new Entity("Person", "b");
+ b.setProperty("firstName", "Bee");
+ b.setProperty("lastName", "Bravo");
+ b.setProperty("height", 70);
+ Entity c = new Entity("Person", "c");
+ c.setProperty("firstName", "Charles");
+ c.setProperty("lastName", "Charlie");
+ c.setProperty("height", 100);
+ datastore.put(ImmutableList.of(a, b, c));
+
+ StringWriter buf = new StringWriter();
+ PrintWriter out = new PrintWriter(buf);
+ long minHeight = 60;
+ long maxHeight = 72;
+
+ // Act
+ // [START gae_java21_datastore_interface_1]
+ DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
+
+ Filter heightMinFilter =
+ new FilterPredicate("height", FilterOperator.GREATER_THAN_OR_EQUAL, minHeight);
+
+ Filter heightMaxFilter =
+ new FilterPredicate("height", FilterOperator.LESS_THAN_OR_EQUAL, maxHeight);
+
+ // Use CompositeFilter to combine multiple filters
+ CompositeFilter heightRangeFilter =
+ CompositeFilterOperator.and(heightMinFilter, heightMaxFilter);
+
+ // Use class Query to assemble a query
+ Query q = new Query("Person").setFilter(heightRangeFilter);
+
+ // Use PreparedQuery interface to retrieve results
+ PreparedQuery pq = datastore.prepare(q);
+
+ for (Entity result : pq.asIterable()) {
+ String firstName = (String) result.getProperty("firstName");
+ String lastName = (String) result.getProperty("lastName");
+ Long height = (Long) result.getProperty("height");
+
+ out.println(firstName + " " + lastName + ", " + height + " inches tall");
+ }
+ // [END gae_java21_datastore_interface_1]
+
+ // Assert
+ assertThat(buf.toString()).contains("Alph Alpha, 60 inches tall");
+ assertThat(buf.toString()).contains("Bee Bravo, 70 inches tall");
+ assertThat(buf.toString()).doesNotContain("Charlie");
+ }
+
+ @Test
+ public void queryInterface_orFilter_printsMatchedEntities() throws Exception {
+ // Arrange
+ Entity a = new Entity("Person", "a");
+ a.setProperty("height", 100);
+ Entity b = new Entity("Person", "b");
+ b.setProperty("height", 150);
+ Entity c = new Entity("Person", "c");
+ c.setProperty("height", 200);
+ datastore.put(ImmutableList.of(a, b, c));
+
+ StringWriter buf = new StringWriter();
+ PrintWriter out = new PrintWriter(buf);
+ long minHeight = 125;
+ long maxHeight = 175;
+
+ // Act
+ // [START gae_java21_datastore_interface_3]
+ Filter tooShortFilter = new FilterPredicate("height", FilterOperator.LESS_THAN, minHeight);
+
+ Filter tooTallFilter = new FilterPredicate("height", FilterOperator.GREATER_THAN, maxHeight);
+
+ Filter heightOutOfRangeFilter = CompositeFilterOperator.or(tooShortFilter, tooTallFilter);
+
+ Query q = new Query("Person").setFilter(heightOutOfRangeFilter);
+ // [END gae_java21_datastore_interface_3]
+
+ // Assert
+ List results =
+ datastore.prepare(q.setKeysOnly()).asList(FetchOptions.Builder.withDefaults());
+ assertWithMessage("query results").that(results).containsExactly(a, c);
+ }
+
+ @Test
+ public void queryRestrictions_compositeFilter_returnsMatchedEntities() throws Exception {
+ // Arrange
+ Entity a = new Entity("Person", "a");
+ a.setProperty("birthYear", 1930);
+ Entity b = new Entity("Person", "b");
+ b.setProperty("birthYear", 1960);
+ Entity c = new Entity("Person", "c");
+ c.setProperty("birthYear", 1990);
+ datastore.put(ImmutableList.of(a, b, c));
+
+ // Act
+ long minBirthYear = 1940;
+ long maxBirthYear = 1980;
+ // [START gae_java21_datastore_inequality_filters_one_property_valid_1]
+ Filter birthYearMinFilter =
+ new FilterPredicate("birthYear", FilterOperator.GREATER_THAN_OR_EQUAL, minBirthYear);
+
+ Filter birthYearMaxFilter =
+ new FilterPredicate("birthYear", FilterOperator.LESS_THAN_OR_EQUAL, maxBirthYear);
+
+ Filter birthYearRangeFilter =
+ CompositeFilterOperator.and(birthYearMinFilter, birthYearMaxFilter);
+
+ Query q = new Query("Person").setFilter(birthYearRangeFilter);
+ // [END gae_java21_datastore_inequality_filters_one_property_valid_1]
+
+ // Assert
+ List results =
+ datastore.prepare(q.setKeysOnly()).asList(FetchOptions.Builder.withDefaults());
+ assertWithMessage("query results").that(results).containsExactly(b);
+ }
+
+ @Test
+ public void queryRestrictions_compositeFilter_isInvalid() throws Exception {
+ long minBirthYear = 1940;
+ long maxHeight = 200;
+ // [START gae_java21_datastore_inequality_filters_one_property_invalid]
+ Filter birthYearMinFilter =
+ new FilterPredicate("birthYear", FilterOperator.GREATER_THAN_OR_EQUAL, minBirthYear);
+
+ Filter heightMaxFilter =
+ new FilterPredicate("height", FilterOperator.LESS_THAN_OR_EQUAL, maxHeight);
+
+ Filter invalidFilter = CompositeFilterOperator.and(birthYearMinFilter, heightMaxFilter);
+
+ Query q = new Query("Person").setFilter(invalidFilter);
+ // [END gae_java21_datastore_inequality_filters_one_property_invalid]
+
+ // Note: The local devserver behavior is different than the production
+ // version of Cloud Datastore, so there aren't any assertions we can make
+ // in this test. The query appears to work with the local test runner,
+ // but will fail in production.
+ }
+
+ @Test
+ public void queryRestrictions_compositeEqualFilter_returnsMatchedEntities() throws Exception {
+ // Arrange
+ Entity a = new Entity("Person", "a");
+ a.setProperty("birthYear", 1930);
+ a.setProperty("city", "Somewhere");
+ a.setProperty("lastName", "Someone");
+ Entity b = new Entity("Person", "b");
+ b.setProperty("birthYear", 1960);
+ b.setProperty("city", "Somewhere");
+ b.setProperty("lastName", "Someone");
+ Entity c = new Entity("Person", "c");
+ c.setProperty("birthYear", 1990);
+ c.setProperty("city", "Somewhere");
+ c.setProperty("lastName", "Someone");
+ Entity d = new Entity("Person", "d");
+ d.setProperty("birthYear", 1960);
+ d.setProperty("city", "Nowhere");
+ d.setProperty("lastName", "Someone");
+ Entity e = new Entity("Person", "e");
+ e.setProperty("birthYear", 1960);
+ e.setProperty("city", "Somewhere");
+ e.setProperty("lastName", "Noone");
+ datastore.put(ImmutableList.of(a, b, c, d, e));
+ long minBirthYear = 1940;
+ long maxBirthYear = 1980;
+ String targetCity = "Somewhere";
+ String targetLastName = "Someone";
+
+ // [START gae_java21_datastore_inequality_filters_one_property_valid_2]
+ Filter lastNameFilter = new FilterPredicate("lastName", FilterOperator.EQUAL, targetLastName);
+
+ Filter cityFilter = new FilterPredicate("city", FilterOperator.EQUAL, targetCity);
+
+ Filter birthYearMinFilter =
+ new FilterPredicate("birthYear", FilterOperator.GREATER_THAN_OR_EQUAL, minBirthYear);
+
+ Filter birthYearMaxFilter =
+ new FilterPredicate("birthYear", FilterOperator.LESS_THAN_OR_EQUAL, maxBirthYear);
+
+ Filter validFilter =
+ CompositeFilterOperator.and(
+ lastNameFilter, cityFilter, birthYearMinFilter, birthYearMaxFilter);
+
+ Query q = new Query("Person").setFilter(validFilter);
+ // [END gae_java21_datastore_inequality_filters_one_property_valid_2]
+
+ // Assert
+ List results =
+ datastore.prepare(q.setKeysOnly()).asList(FetchOptions.Builder.withDefaults());
+ assertWithMessage("query results").that(results).containsExactly(b);
+ }
+
+ @Test
+ public void queryRestrictions_inequalitySortedFirst_returnsMatchedEntities() throws Exception {
+ // Arrange
+ Entity a = new Entity("Person", "a");
+ a.setProperty("birthYear", 1930);
+ a.setProperty("lastName", "Someone");
+ Entity b = new Entity("Person", "b");
+ b.setProperty("birthYear", 1990);
+ b.setProperty("lastName", "Bravo");
+ Entity c = new Entity("Person", "c");
+ c.setProperty("birthYear", 1960);
+ c.setProperty("lastName", "Charlie");
+ Entity d = new Entity("Person", "d");
+ d.setProperty("birthYear", 1960);
+ d.setProperty("lastName", "Delta");
+ datastore.put(ImmutableList.of(a, b, c, d));
+ long minBirthYear = 1940;
+
+ Filter birthYearMinFilter =
+ new FilterPredicate("birthYear", FilterOperator.GREATER_THAN_OR_EQUAL, minBirthYear);
+
+ Query q =
+ new Query("Person")
+ .setFilter(birthYearMinFilter)
+ .addSort("birthYear", SortDirection.ASCENDING)
+ .addSort("lastName", SortDirection.ASCENDING);
+
+ // Assert
+ List results =
+ datastore.prepare(q.setKeysOnly()).asList(FetchOptions.Builder.withDefaults());
+ assertWithMessage("query results").that(results).containsExactly(c, d, b).inOrder();
+ }
+
+ @Test
+ public void queryRestrictions_missingSortOnInequality_isInvalid() throws Exception {
+ long minBirthYear = 1940;
+ // [START gae_java21_datastore_inequality_filters_sort_orders_invalid_1]
+ Filter birthYearMinFilter =
+ new FilterPredicate("birthYear", FilterOperator.GREATER_THAN_OR_EQUAL, minBirthYear);
+
+ // Not valid. Missing sort on birthYear.
+ Query q =
+ new Query("Person")
+ .setFilter(birthYearMinFilter)
+ .addSort("lastName", SortDirection.ASCENDING);
+ // [END gae_java21_datastore_inequality_filters_sort_orders_invalid_1]
+
+ // Note: The local devserver behavior is different than the production
+ // version of Cloud Datastore, so there aren't any assertions we can make
+ // in this test. The query appears to work with the local test runner,
+ // but will fail in production.
+ }
+
+ @Test
+ public void queryRestrictions_sortWrongOrderOnInequality_isInvalid() throws Exception {
+ long minBirthYear = 1940;
+ // [START gae_java21_datastore_inequality_filters_sort_orders_invalid_2]
+ Filter birthYearMinFilter =
+ new FilterPredicate("birthYear", FilterOperator.GREATER_THAN_OR_EQUAL, minBirthYear);
+
+ // Not valid. Sort on birthYear needs to be first.
+ Query q =
+ new Query("Person")
+ .setFilter(birthYearMinFilter)
+ .addSort("lastName", SortDirection.ASCENDING)
+ .addSort("birthYear", SortDirection.ASCENDING);
+ // [END gae_java21_datastore_inequality_filters_sort_orders_invalid_2]
+
+ // Note: The local devserver behavior is different than the production
+ // version of Cloud Datastore, so there aren't any assertions we can make
+ // in this test. The query appears to work with the local test runner,
+ // but will fail in production.
+ }
+
+ @Test
+ public void queryRestrictions_surprisingMultipleValuesAllMustMatch_returnsNoEntities()
+ throws Exception {
+ Entity a = new Entity("Widget", "a");
+ List xs = Arrays.asList(1L, 2L);
+ a.setProperty("x", xs);
+ datastore.put(a);
+
+ // [START gae_java21_datastore_surprising_behavior_1]
+ Query q =
+ new Query("Widget")
+ .setFilter(
+ CompositeFilterOperator.and(
+ new FilterPredicate("x", FilterOperator.GREATER_THAN, 1),
+ new FilterPredicate("x", FilterOperator.LESS_THAN, 2)));
+ // [END gae_java21_datastore_surprising_behavior_1]
+
+ // Entity "a" will not match because no individual value matches all filters.
+ // See the documentation for more details:
+ // https://cloud.google.com/appengine/docs/java/datastore/query-restrictions
+ // #properties_with_multiple_values_can_behave_in_surprising_ways
+ List results =
+ datastore.prepare(q.setKeysOnly()).asList(FetchOptions.Builder.withDefaults());
+ assertWithMessage("query results").that(results).isEmpty();
+ }
+
+ @Test
+ public void queryRestrictions_surprisingMultipleValuesEquals_returnsMatchedEntities()
+ throws Exception {
+ Entity a = new Entity("Widget", "a");
+ a.setProperty("x", ImmutableList.of(1L, 2L));
+ Entity b = new Entity("Widget", "b");
+ b.setProperty("x", ImmutableList.of(1L, 3L));
+ Entity c = new Entity("Widget", "c");
+ c.setProperty("x", ImmutableList.of(-6L, 2L));
+ Entity d = new Entity("Widget", "d");
+ d.setProperty("x", ImmutableList.of(-6L, 4L));
+ Entity e = new Entity("Widget", "e");
+ e.setProperty("x", ImmutableList.of(1L, 2L, 3L));
+ datastore.put(ImmutableList.of(a, b, c, d, e));
+
+ // [START gae_java21_datastore_surprising_behavior_2]
+ Query q =
+ new Query("Widget")
+ .setFilter(
+ CompositeFilterOperator.and(
+ new FilterPredicate("x", FilterOperator.EQUAL, 1),
+ new FilterPredicate("x", FilterOperator.EQUAL, 2)));
+ // [END gae_java21_datastore_surprising_behavior_2]
+
+ // Only "a" and "e" have both 1 and 2 in the "x" array-valued property.
+ // See the documentation for more details:
+ // https://cloud.google.com/appengine/docs/java/datastore/query-restrictions
+ // #properties_with_multiple_values_can_behave_in_surprising_ways
+ List results =
+ datastore.prepare(q.setKeysOnly()).asList(FetchOptions.Builder.withDefaults());
+ assertWithMessage("query results").that(results).containsExactly(a, e);
+ }
+
+ @Test
+ public void queryRestrictions_surprisingMultipleValuesNotEquals_returnsMatchedEntities()
+ throws Exception {
+ Entity a = new Entity("Widget", "a");
+ a.setProperty("x", ImmutableList.of(1L, 2L));
+ Entity b = new Entity("Widget", "b");
+ b.setProperty("x", ImmutableList.of(1L, 3L));
+ Entity c = new Entity("Widget", "c");
+ c.setProperty("x", ImmutableList.of(-6L, 2L));
+ Entity d = new Entity("Widget", "d");
+ d.setProperty("x", ImmutableList.of(-6L, 4L));
+ Entity e = new Entity("Widget", "e");
+ e.setProperty("x", ImmutableList.of(1L));
+ datastore.put(ImmutableList.of(a, b, c, d, e));
+
+ // [START gae_java21_datastore_surprising_behavior_3]
+ Query q = new Query("Widget").setFilter(new FilterPredicate("x", FilterOperator.NOT_EQUAL, 1));
+ // [END gae_java21_datastore_surprising_behavior_3]
+
+ // The query matches any entity that has a some value other than 1. Only
+ // entity "e" is not matched. See the documentation for more details:
+ // https://cloud.google.com/appengine/docs/java/datastore/query-restrictions
+ // #properties_with_multiple_values_can_behave_in_surprising_ways
+ List results =
+ datastore.prepare(q.setKeysOnly()).asList(FetchOptions.Builder.withDefaults());
+ assertWithMessage("query results").that(results).containsExactly(a, b, c, d);
+ }
+
+ @Test
+ public void queryRestrictions_surprisingMultipleValuesTwoNotEquals_returnsMatchedEntities()
+ throws Exception {
+ Entity a = new Entity("Widget", "a");
+ a.setProperty("x", ImmutableList.of(1L, 2L));
+ Entity b = new Entity("Widget", "b");
+ b.setProperty("x", ImmutableList.of(1L, 2L, 3L));
+ datastore.put(ImmutableList.of(a, b));
+
+ // [START gae_java21_datastore_surprising_behavior_4]
+ Query q =
+ new Query("Widget")
+ .setFilter(
+ CompositeFilterOperator.and(
+ new FilterPredicate("x", FilterOperator.NOT_EQUAL, 1),
+ new FilterPredicate("x", FilterOperator.NOT_EQUAL, 2)));
+ // [END gae_java21_datastore_surprising_behavior_4]
+
+ // The two NOT_EQUAL filters in the query become like the combination of queries:
+ // x < 1 OR (x > 1 AND x < 2) OR x > 2
+ //
+ // Only "b" has some value which matches the "x > 2" portion of this query.
+ //
+ // See the documentation for more details:
+ // https://cloud.google.com/appengine/docs/java/datastore/query-restrictions
+ // #properties_with_multiple_values_can_behave_in_surprising_ways
+ List results =
+ datastore.prepare(q.setKeysOnly()).asList(FetchOptions.Builder.withDefaults());
+ assertWithMessage("query results").that(results).containsExactly(b);
+ }
+
+ private Entity retrievePersonWithLastName(String targetLastName) {
+ // [START gae_java21_datastore_single_retrieval]
+ Query q =
+ new Query("Person")
+ .setFilter(new FilterPredicate("lastName", FilterOperator.EQUAL, targetLastName));
+
+ PreparedQuery pq = datastore.prepare(q);
+ Entity result = pq.asSingleEntity();
+ // [END gae_java21_datastore_single_retrieval]
+ return result;
+ }
+
+ @Test
+ public void singleRetrievalExample_singleEntity_returnsEntity() throws Exception {
+ Entity a = new Entity("Person", "a");
+ a.setProperty("lastName", "Johnson");
+ Entity b = new Entity("Person", "b");
+ b.setProperty("lastName", "Smith");
+ datastore.put(ImmutableList.of(a, b));
+
+ Entity result = retrievePersonWithLastName("Johnson");
+
+ assertWithMessage("result")
+ .that(result)
+ .isEqualTo(a); // Note: Entity.equals() only checks the Key.
+ }
+
+ @Test
+ public void singleRetrievalExample_multitpleEntities_throwsException() throws Exception {
+ Entity a = new Entity("Person", "a");
+ a.setProperty("lastName", "Johnson");
+ Entity b = new Entity("Person", "b");
+ b.setProperty("lastName", "Johnson");
+ datastore.put(ImmutableList.of(a, b));
+
+ try {
+ Entity result = retrievePersonWithLastName("Johnson");
+ fail("Expected TooManyResultsException");
+ } catch (TooManyResultsException expected) {
+ // TooManyResultsException does not provide addition details.
+ }
+ }
+
+ // [START gae_java21_datastore_query_limit]
+ private List getTallestPeople() {
+ DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
+
+ Query q = new Query("Person").addSort("height", SortDirection.DESCENDING);
+
+ PreparedQuery pq = datastore.prepare(q);
+ return pq.asList(FetchOptions.Builder.withLimit(5));
+ }
+ // [END gae_java21_datastore_query_limit]
+
+ @Test
+ public void queryLimitExample_returnsLimitedEntities() throws Exception {
+ Entity a = new Entity("Person", "a");
+ a.setProperty("height", 200);
+ Entity b = new Entity("Person", "b");
+ b.setProperty("height", 199);
+ Entity c = new Entity("Person", "c");
+ c.setProperty("height", 201);
+ Entity d = new Entity("Person", "d");
+ d.setProperty("height", 198);
+ Entity e = new Entity("Person", "e");
+ e.setProperty("height", 202);
+ Entity f = new Entity("Person", "f");
+ f.setProperty("height", 197);
+ Entity g = new Entity("Person", "g");
+ g.setProperty("height", 203);
+ Entity h = new Entity("Person", "h");
+ h.setProperty("height", 196);
+ datastore.put(ImmutableList.of(a, b, c, d, e, f, g, h));
+
+ List results = getTallestPeople();
+
+ assertWithMessage("results").that(results).containsExactly(g, e, c, a, b).inOrder();
+ }
+}
diff --git a/appengine-java21/ee8/datastore/src/test/java/com/example/appengine/ReadPolicyTest.java b/appengine-java21/ee8/datastore/src/test/java/com/example/appengine/ReadPolicyTest.java
new file mode 100644
index 00000000000..31c303d6cd2
--- /dev/null
+++ b/appengine-java21/ee8/datastore/src/test/java/com/example/appengine/ReadPolicyTest.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2016 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.appengine;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import com.google.appengine.api.datastore.DatastoreService;
+import com.google.appengine.api.datastore.DatastoreServiceConfig;
+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.ReadPolicy;
+import com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig;
+import com.google.appengine.tools.development.testing.LocalServiceTestHelper;
+import com.google.common.collect.ImmutableList;
+import java.util.List;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Unit tests for {@link ReadPolicy}.
+ */
+@RunWith(JUnit4.class)
+public class ReadPolicyTest {
+
+ private final LocalServiceTestHelper helper =
+ new LocalServiceTestHelper(
+ // Set 100% eventual consistency, so we can test with other job policies.
+ // https://cloud.google
+ // .com/appengine/docs/java/tools/localunittesting
+ // #Java_Writing_High_Replication_Datastore_tests
+ new LocalDatastoreServiceTestConfig()
+ .setDefaultHighRepJobPolicyUnappliedJobPercentage(100));
+
+ @Before
+ public void setUp() {
+ helper.setUp();
+ }
+
+ @After
+ public void tearDown() {
+ helper.tearDown();
+ }
+
+ @Test
+ public void readPolicy_eventual_returnsNoResults() {
+ // [START gae_java21_datastore_data_consistency]
+ double deadline = 5.0;
+
+ // Construct a read policy for eventual consistency
+ ReadPolicy policy = new ReadPolicy(ReadPolicy.Consistency.EVENTUAL);
+
+ // Set the read policy
+ DatastoreServiceConfig eventuallyConsistentConfig =
+ DatastoreServiceConfig.Builder.withReadPolicy(policy);
+
+ // Set the call deadline
+ DatastoreServiceConfig deadlineConfig = DatastoreServiceConfig.Builder.withDeadline(deadline);
+
+ // Set both the read policy and the call deadline
+ DatastoreServiceConfig datastoreConfig =
+ DatastoreServiceConfig.Builder.withReadPolicy(policy).deadline(deadline);
+
+ // Get Datastore service with the given configuration
+ DatastoreService datastore = DatastoreServiceFactory.getDatastoreService(datastoreConfig);
+ // [END gae_java21_datastore_data_consistency]
+
+ Entity parent = new Entity("Person", "a");
+ Entity child = new Entity("Person", "b", parent.getKey());
+ datastore.put(ImmutableList.of(parent, child));
+
+ // Even though we are using an ancestor query, the policy is set to
+ // eventual, so we should get eventually-consistent results. Since the
+ // local data store test config is set to 100% unapplied jobs, there
+ // should be no results.
+ Query q = new Query("Person").setAncestor(parent.getKey());
+ List results = datastore.prepare(q).asList(FetchOptions.Builder.withDefaults());
+ assertWithMessage("query results").that(results).isEmpty();
+ }
+
+ @Test
+ public void readPolicy_strong_returnsAllResults() {
+ double deadline = 5.0;
+ ReadPolicy policy = new ReadPolicy(ReadPolicy.Consistency.STRONG);
+ DatastoreServiceConfig datastoreConfig =
+ DatastoreServiceConfig.Builder.withReadPolicy(policy).deadline(deadline);
+ DatastoreService datastore = DatastoreServiceFactory.getDatastoreService(datastoreConfig);
+
+ Entity parent = new Entity("Person", "a");
+ Entity child = new Entity("Person", "b", parent.getKey());
+ datastore.put(ImmutableList.of(parent, child));
+
+ Query q = new Query("Person").setAncestor(parent.getKey());
+ List results = datastore.prepare(q).asList(FetchOptions.Builder.withDefaults());
+ assertWithMessage("query results").that(results).hasSize(2);
+ }
+}
diff --git a/appengine-java21/ee8/datastore/src/test/java/com/example/appengine/StartupServletTest.java b/appengine-java21/ee8/datastore/src/test/java/com/example/appengine/StartupServletTest.java
new file mode 100644
index 00000000000..2da82842484
--- /dev/null
+++ b/appengine-java21/ee8/datastore/src/test/java/com/example/appengine/StartupServletTest.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2016 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.appengine;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+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.Query;
+import com.google.appengine.api.datastore.Query.Filter;
+import com.google.appengine.api.datastore.Query.FilterOperator;
+import com.google.appengine.api.datastore.Query.FilterPredicate;
+import com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig;
+import com.google.appengine.tools.development.testing.LocalServiceTestHelper;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+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;
+
+/**
+ * Unit tests for {@link StartupServlet}.
+ */
+@RunWith(JUnit4.class)
+public class StartupServletTest {
+
+ 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 DatastoreService datastore;
+
+ private StartupServlet servletUnderTest;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.openMocks(this);
+ helper.setUp();
+ datastore = DatastoreServiceFactory.getDatastoreService();
+
+ // Set up a fake HTTP response.
+ responseWriter = new StringWriter();
+ when(mockResponse.getWriter()).thenReturn(new PrintWriter(responseWriter));
+
+ servletUnderTest = new StartupServlet();
+ }
+
+ @After
+ public void tearDown() {
+ helper.tearDown();
+ }
+
+ @Test
+ public void doGet_emptyDatastore_writesOkay() throws Exception {
+ servletUnderTest.doGet(mockRequest, mockResponse);
+ assertWithMessage("StartupServlet response").that(responseWriter.toString()).isEqualTo("ok\n");
+ }
+
+ @Test
+ public void doGet_emptyDatastore_writesPresidents() throws Exception {
+ servletUnderTest.doGet(mockRequest, mockResponse);
+
+ Filter nameFilter = new FilterPredicate("name", FilterOperator.EQUAL, "George Washington");
+ Query q = new Query("Person").setFilter(nameFilter);
+ Entity result = datastore.prepare(q).asSingleEntity();
+ assertWithMessage("name").that(result.getProperty("name")).isEqualTo("George Washington");
+ }
+
+ @Test
+ public void doGet_alreadyPopulated_writesOkay() throws Exception {
+ datastore.put(
+ new Entity(StartupServlet.IS_POPULATED_ENTITY, StartupServlet.IS_POPULATED_KEY_NAME));
+ servletUnderTest.doGet(mockRequest, mockResponse);
+ assertWithMessage("StartupServlet response").that(responseWriter.toString()).isEqualTo("ok\n");
+ }
+}
diff --git a/appengine-java21/ee8/datastore/src/test/java/com/example/appengine/TransactionsTest.java b/appengine-java21/ee8/datastore/src/test/java/com/example/appengine/TransactionsTest.java
new file mode 100644
index 00000000000..8faf1144fff
--- /dev/null
+++ b/appengine-java21/ee8/datastore/src/test/java/com/example/appengine/TransactionsTest.java
@@ -0,0 +1,309 @@
+/*
+ * Copyright 2016 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.appengine;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+import static org.junit.Assert.fail;
+
+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.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 com.google.appengine.api.datastore.Transaction;
+import com.google.appengine.api.datastore.TransactionOptions;
+import com.google.appengine.api.taskqueue.Queue;
+import com.google.appengine.api.taskqueue.QueueFactory;
+import com.google.appengine.api.taskqueue.TaskOptions;
+import com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig;
+import com.google.appengine.tools.development.testing.LocalServiceTestHelper;
+import java.util.ConcurrentModificationException;
+import java.util.Date;
+import java.util.List;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Unit tests to demonstrate App Engine Datastore transactions.
+ */
+@RunWith(JUnit4.class)
+public class TransactionsTest {
+
+ private final LocalServiceTestHelper helper =
+ new LocalServiceTestHelper(
+ // Use High Rep job policy to allow cross group transactions in tests.
+ new LocalDatastoreServiceTestConfig().setApplyAllHighRepJobPolicy());
+
+ private DatastoreService datastore;
+
+ @Before
+ public void setUp() {
+ helper.setUp();
+ datastore = DatastoreServiceFactory.getDatastoreService();
+ }
+
+ @After
+ public void tearDown() {
+ // Clean up any dangling transactions.
+ Transaction txn = datastore.getCurrentTransaction(null);
+ if (txn != null && txn.isActive()) {
+ txn.rollback();
+ }
+ helper.tearDown();
+ }
+
+ @Test
+ public void usingTransactions() throws Exception {
+ Entity joe = new Entity("Employee", "Joe");
+ datastore.put(joe);
+
+ // [START gae_java21_datastore_using_transactions]
+ DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
+ Transaction txn = datastore.beginTransaction();
+ try {
+ Key employeeKey = KeyFactory.createKey("Employee", "Joe");
+ Entity employee = datastore.get(employeeKey);
+ employee.setProperty("vacationDays", 10);
+
+ datastore.put(txn, employee);
+
+ txn.commit();
+ } finally {
+ if (txn.isActive()) {
+ txn.rollback();
+ }
+ }
+ // [END gae_java21_datastore_using_transactions]
+ }
+
+ @Test
+ public void entityGroups() throws Exception {
+ try {
+ // [START gae_java21_datastore_entity_groups]
+ DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
+ Entity person = new Entity("Person", "tom");
+ datastore.put(person);
+
+ // Transactions on root entities
+ Transaction txn = datastore.beginTransaction();
+
+ Entity tom = datastore.get(person.getKey());
+ tom.setProperty("age", 40);
+ datastore.put(txn, tom);
+ txn.commit();
+
+ // Transactions on child entities
+ txn = datastore.beginTransaction();
+ tom = datastore.get(person.getKey());
+ Entity photo = new Entity("Photo", tom.getKey());
+
+ // Create a Photo that is a child of the Person entity named "tom"
+ photo.setProperty("photoUrl", "http://domain.com/path/to/photo.jpg");
+ datastore.put(txn, photo);
+ txn.commit();
+
+ // Transactions on entities in different entity groups
+ txn = datastore.beginTransaction();
+ tom = datastore.get(person.getKey());
+ Entity photoNotaChild = new Entity("Photo");
+ photoNotaChild.setProperty("photoUrl", "http://domain.com/path/to/photo.jpg");
+ datastore.put(txn, photoNotaChild);
+
+ // Throws IllegalArgumentException because the Person entity
+ // and the Photo entity belong to different entity groups.
+ txn.commit();
+ // [END gae_java21_datastore_entity_groups]
+ fail("Expected IllegalArgumentException");
+ } catch (IllegalArgumentException expected) {
+ // We expect to get an exception that complains that we don't have a XG-transaction.
+ }
+ }
+
+ @Test
+ @SuppressWarnings("checkstyle:AbbreviationAsWordInName")
+ public void creatingAnEntityInASpecificEntityGroup() throws Exception {
+ String boardName = "my-message-board";
+
+ // [START gae_java21_datastore_creating_an_entity_in_a_specific_entity_group]
+ DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
+
+ String messageTitle = "Some Title";
+ String messageText = "Some message.";
+ Date postDate = new Date();
+
+ Key messageBoardKey = KeyFactory.createKey("MessageBoard", boardName);
+
+ Entity message = new Entity("Message", messageBoardKey);
+ message.setProperty("message_title", messageTitle);
+ message.setProperty("message_text", messageText);
+ message.setProperty("post_date", postDate);
+
+ Transaction txn = datastore.beginTransaction();
+ datastore.put(txn, message);
+
+ txn.commit();
+ // [END gae_java21_datastore_creating_an_entity_in_a_specific_entity_group]
+ }
+
+ @Test
+ public void crossGroupTransactions() throws Exception {
+ // [START gae_java21_datastore_cross_group_transactions]
+ DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
+ TransactionOptions options = TransactionOptions.Builder.withXG(true);
+ Transaction txn = datastore.beginTransaction(options);
+
+ Entity a = new Entity("A");
+ a.setProperty("a", 22);
+ datastore.put(txn, a);
+
+ Entity b = new Entity("B");
+ b.setProperty("b", 11);
+ datastore.put(txn, b);
+
+ txn.commit();
+ // [END gae_java21_datastore_cross_group_transactions]
+ }
+
+ @Test
+ public void usesForTransactions_relativeUpdates() throws Exception {
+ String boardName = "my-message-board";
+ Entity b = new Entity("MessageBoard", boardName);
+ b.setProperty("count", 41);
+ datastore.put(b);
+
+ // [START gae_java21_datastore_uses_for_transactions_1]
+ int retries = 3;
+ while (true) {
+ Transaction txn = datastore.beginTransaction();
+ try {
+ Key boardKey = KeyFactory.createKey("MessageBoard", boardName);
+ Entity messageBoard = datastore.get(boardKey);
+
+ long count = (Long) messageBoard.getProperty("count");
+ ++count;
+ messageBoard.setProperty("count", count);
+ datastore.put(txn, messageBoard);
+
+ txn.commit();
+ break;
+ } catch (ConcurrentModificationException e) {
+ if (retries == 0) {
+ throw e;
+ }
+ // Allow retry to occur
+ --retries;
+ } finally {
+ if (txn.isActive()) {
+ txn.rollback();
+ }
+ }
+ }
+ // [END gae_java21_datastore_uses_for_transactions_1]
+
+ b = datastore.get(KeyFactory.createKey("MessageBoard", boardName));
+ assertWithMessage("board.count").that((long) b.getProperty("count")).isEqualTo(42L);
+ }
+
+ private Entity fetchOrCreate(String boardName) {
+ // [START gae_java21_datastore_uses_for_transactions_2]
+ Transaction txn = datastore.beginTransaction();
+ Entity messageBoard;
+ Key boardKey;
+ try {
+ boardKey = KeyFactory.createKey("MessageBoard", boardName);
+ messageBoard = datastore.get(boardKey);
+ } catch (EntityNotFoundException e) {
+ messageBoard = new Entity("MessageBoard", boardName);
+ messageBoard.setProperty("count", 0L);
+ boardKey = datastore.put(txn, messageBoard);
+ }
+ txn.commit();
+ // [END gae_java21_datastore_uses_for_transactions_2]
+
+ return messageBoard;
+ }
+
+ @Test
+ public void usesForTransactions_fetchOrCreate_fetchesExisting() throws Exception {
+ Entity b = new Entity("MessageBoard", "my-message-board");
+ b.setProperty("count", 7);
+ datastore.put(b);
+
+ Entity board = fetchOrCreate("my-message-board");
+
+ assertWithMessage("board.count").that((long) board.getProperty("count")).isEqualTo(7L);
+ }
+
+ @Test
+ public void usesForTransactions_fetchOrCreate_createsNew() throws Exception {
+ Entity board = fetchOrCreate("my-message-board");
+ assertWithMessage("board.count").that((long) board.getProperty("count")).isEqualTo(0L);
+ }
+
+ @Test
+ public void usesForTransactions_readSnapshot() throws Exception {
+ String boardName = "my-message-board";
+ Entity b = new Entity("MessageBoard", boardName);
+ b.setProperty("count", 13);
+ datastore.put(b);
+
+ // [START gae_java21_datastore_uses_for_transactions_3]
+ DatastoreService ds = DatastoreServiceFactory.getDatastoreService();
+
+ // Display information about a message board and its first 10 messages.
+ Key boardKey = KeyFactory.createKey("MessageBoard", boardName);
+
+ Transaction txn = datastore.beginTransaction();
+
+ Entity messageBoard = datastore.get(boardKey);
+ long count = (Long) messageBoard.getProperty("count");
+
+ Query q = new Query("Message", boardKey);
+
+ // This is an ancestor query.
+ PreparedQuery pq = datastore.prepare(txn, q);
+ List messages = pq.asList(FetchOptions.Builder.withLimit(10));
+
+ txn.commit();
+ // [END gae_java21_datastore_uses_for_transactions_3]
+
+ assertWithMessage("board.count").that(count).isEqualTo(13L);
+ }
+
+ @Test
+ public void transactionalTaskEnqueuing() throws Exception {
+ // [START gae_java21_datastore_transactional_task_enqueuing]
+ DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
+ Queue queue = QueueFactory.getDefaultQueue();
+ Transaction txn = datastore.beginTransaction();
+ // ...
+
+ queue.add(txn, TaskOptions.Builder.withUrl("/path/to/handler"));
+
+ // ...
+
+ txn.commit();
+ // [END gae_java21_datastore_transactional_task_enqueuing]
+ }
+}