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 + + +Open in Cloud Shell + +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("

"); + + 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] + } +}