From c2091ae6a7b5c6daff81827d84cea92df04fd3c7 Mon Sep 17 00:00:00 2001 From: Tim Swast Date: Wed, 20 Apr 2016 17:33:55 -0700 Subject: [PATCH] Add App Engine Datastore raw APIs sample. These samples are pulled from the "projection query" https://cloud.google.com/appengine/docs/java/datastore/projectionqueries and "structuring for strong consistency" https://cloud.google.com/appengine/docs/java/datastore/structuring_for_strong_consistency pages. I will add additional code to this sample as I convert the other pages, as well. --- appengine/datastore/README.md | 19 ++ appengine/datastore/pom.xml | 114 +++++++++++ .../example/appengine/AbstractGuestbook.java | 83 ++++++++ .../appengine/AbstractGuestbookServlet.java | 59 ++++++ .../java/com/example/appengine/Greeting.java | 43 ++++ .../java/com/example/appengine/Guestbook.java | 66 +++++++ .../example/appengine/GuestbookServlet.java | 25 +++ .../example/appengine/GuestbookStrong.java | 74 +++++++ .../appengine/GuestbookStrongServlet.java | 27 +++ .../example/appengine/ProjectionServlet.java | 79 ++++++++ .../src/main/java/com/example/time/Clock.java | 35 ++++ .../java/com/example/time/SystemClock.java | 38 ++++ .../com/example/time/testing/FakeClock.java | 186 ++++++++++++++++++ .../src/main/webapp/WEB-INF/appengine-web.xml | 20 ++ .../main/webapp/WEB-INF/datastore-indexes.xml | 22 +++ .../datastore/src/main/webapp/WEB-INF/web.xml | 58 ++++++ .../datastore/src/main/webapp/guestbook.jsp | 45 +++++ .../appengine/GuestbookStrongTest.java | 105 ++++++++++ .../com/example/appengine/GuestbookTest.java | 127 ++++++++++++ .../appengine/ProjectionServletTest.java | 104 ++++++++++ pom.xml | 1 + 21 files changed, 1330 insertions(+) create mode 100644 appengine/datastore/README.md create mode 100644 appengine/datastore/pom.xml create mode 100644 appengine/datastore/src/main/java/com/example/appengine/AbstractGuestbook.java create mode 100644 appengine/datastore/src/main/java/com/example/appengine/AbstractGuestbookServlet.java create mode 100644 appengine/datastore/src/main/java/com/example/appengine/Greeting.java create mode 100644 appengine/datastore/src/main/java/com/example/appengine/Guestbook.java create mode 100644 appengine/datastore/src/main/java/com/example/appengine/GuestbookServlet.java create mode 100644 appengine/datastore/src/main/java/com/example/appengine/GuestbookStrong.java create mode 100644 appengine/datastore/src/main/java/com/example/appengine/GuestbookStrongServlet.java create mode 100644 appengine/datastore/src/main/java/com/example/appengine/ProjectionServlet.java create mode 100644 appengine/datastore/src/main/java/com/example/time/Clock.java create mode 100644 appengine/datastore/src/main/java/com/example/time/SystemClock.java create mode 100644 appengine/datastore/src/main/java/com/example/time/testing/FakeClock.java create mode 100644 appengine/datastore/src/main/webapp/WEB-INF/appengine-web.xml create mode 100644 appengine/datastore/src/main/webapp/WEB-INF/datastore-indexes.xml create mode 100644 appengine/datastore/src/main/webapp/WEB-INF/web.xml create mode 100644 appengine/datastore/src/main/webapp/guestbook.jsp create mode 100644 appengine/datastore/src/test/java/com/example/appengine/GuestbookStrongTest.java create mode 100644 appengine/datastore/src/test/java/com/example/appengine/GuestbookTest.java create mode 100644 appengine/datastore/src/test/java/com/example/appengine/ProjectionServletTest.java diff --git a/appengine/datastore/README.md b/appengine/datastore/README.md new file mode 100644 index 00000000000..0682e0e9076 --- /dev/null +++ b/appengine/datastore/README.md @@ -0,0 +1,19 @@ +# Google Cloud Datastore Sample + +This sample demonstrates how to use [Google Cloud Datastore][java-datastore] +from [Google App Engine standard environment][ae-docs]. + +[java-datastore]: https://cloud.google.com/appengine/docs/java/datastore/ +[ae-docs]: https://cloud.google.com/appengine/docs/java/ + +## Setup +1. Update the `` tag in `src/main/webapp/WEB-INF/appengine-web.xml` + with your project name. +1. Update the `` tag in `src/main/webapp/WEB-INF/appengine-web.xml` + with your version name. + +## Running locally + $ mvn appengine:devserver + +## Deploying + $ mvn appengine:update diff --git a/appengine/datastore/pom.xml b/appengine/datastore/pom.xml new file mode 100644 index 00000000000..11806455431 --- /dev/null +++ b/appengine/datastore/pom.xml @@ -0,0 +1,114 @@ + + + 4.0.0 + war + 1.0-SNAPSHOT + com.example.appengine + appengine-datastore + + com.google.cloud + doc-samples + 1.0.0 + ../.. + + + + com.google.appengine + appengine-api-1.0-sdk + ${appengine.sdk.version} + + + com.google.auto.value + auto-value + 1.2 + provided + + + com.google.code.findbugs + jsr305 + 3.0.1 + + + + com.google.guava + guava + 19.0 + + + + javax.servlet + servlet-api + jar + provided + + + joda-time + joda-time + 2.9.3 + + + + + junit + junit + 4.10 + test + + + org.mockito + mockito-all + 1.10.19 + test + + + com.google.appengine + appengine-testing + ${appengine.sdk.version} + test + + + com.google.appengine + appengine-api-stubs + ${appengine.sdk.version} + test + + + com.google.appengine + appengine-tools-sdk + ${appengine.sdk.version} + test + + + com.google.truth + truth + 0.28 + test + + + + + ${project.build.directory}/${project.build.finalName}/WEB-INF/classes + + + + com.google.appengine + appengine-maven-plugin + ${appengine.sdk.version} + + + + diff --git a/appengine/datastore/src/main/java/com/example/appengine/AbstractGuestbook.java b/appengine/datastore/src/main/java/com/example/appengine/AbstractGuestbook.java new file mode 100644 index 00000000000..a11707a8819 --- /dev/null +++ b/appengine/datastore/src/main/java/com/example/appengine/AbstractGuestbook.java @@ -0,0 +1,83 @@ +/* + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.appengine; + +import com.example.time.Clock; + +import com.google.appengine.api.datastore.DatastoreService; +import com.google.appengine.api.datastore.DatastoreServiceFactory; +import com.google.appengine.api.datastore.Entity; +import com.google.appengine.api.users.User; +import com.google.appengine.api.users.UserService; +import com.google.appengine.api.users.UserServiceFactory; +import com.google.common.collect.ImmutableList; + +import java.util.Date; +import java.util.List; + +/** + * A log of notes left by users. + * + *

This is meant to be subclassed to demonstrate different storage structures in Datastore. + */ +abstract class AbstractGuestbook { + private final DatastoreService datastore; + private final UserService userService; + private final Clock clock; + + AbstractGuestbook(Clock clock) { + this.datastore = DatastoreServiceFactory.getDatastoreService(); + this.userService = UserServiceFactory.getUserService(); + this.clock = clock; + } + + /** + * Appends a new greeting to the guestbook and returns the {@link Entity} that was created. + */ + public Greeting appendGreeting(String content) { + Greeting greeting = + Greeting.create( + createGreeting( + datastore, + userService.getCurrentUser(), + clock.now().toDate(), + content)); + return greeting; + } + + /** + * Write a greeting to Datastore. + */ + protected abstract Entity createGreeting( + DatastoreService datastore, User user, Date date, String content); + + /** + * Return a list of the most recent greetings. + */ + public List listGreetings() { + ImmutableList.Builder greetings = ImmutableList.builder(); + for (Entity entity : listGreetingEntities(datastore)) { + greetings.add(Greeting.create(entity)); + } + return greetings.build(); + } + + /** + * Return a list of the most recent greetings. + */ + protected abstract List listGreetingEntities(DatastoreService datastore); +} diff --git a/appengine/datastore/src/main/java/com/example/appengine/AbstractGuestbookServlet.java b/appengine/datastore/src/main/java/com/example/appengine/AbstractGuestbookServlet.java new file mode 100644 index 00000000000..ba6f8c95ce9 --- /dev/null +++ b/appengine/datastore/src/main/java/com/example/appengine/AbstractGuestbookServlet.java @@ -0,0 +1,59 @@ +/* + * Copyright 2016 Google Inc. All Rights Reserved. + * + * 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/datastore/src/main/java/com/example/appengine/Greeting.java b/appengine/datastore/src/main/java/com/example/appengine/Greeting.java new file mode 100644 index 00000000000..ad083267f05 --- /dev/null +++ b/appengine/datastore/src/main/java/com/example/appengine/Greeting.java @@ -0,0 +1,43 @@ +/* + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.appengine; + +import com.google.appengine.api.datastore.Entity; +import com.google.appengine.api.users.User; +import com.google.auto.value.AutoValue; +import org.joda.time.Instant; + +import java.util.Date; + +import javax.annotation.Nullable; + +@AutoValue +public abstract class Greeting { + static Greeting create(Entity entity) { + User user = (User) entity.getProperty("user"); + Instant date = new Instant((Date) entity.getProperty("date")); + String content = (String) entity.getProperty("content"); + return new AutoValue_Greeting(user, date, content); + } + + @Nullable + public abstract User getUser(); + + public abstract Instant getDate(); + + public abstract String getContent(); +} diff --git a/appengine/datastore/src/main/java/com/example/appengine/Guestbook.java b/appengine/datastore/src/main/java/com/example/appengine/Guestbook.java new file mode 100644 index 00000000000..36649c958fc --- /dev/null +++ b/appengine/datastore/src/main/java/com/example/appengine/Guestbook.java @@ -0,0 +1,66 @@ +/* + * Copyright 2016 Google Inc. All Rights Reserved. + * + * 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/datastore/src/main/java/com/example/appengine/GuestbookServlet.java b/appengine/datastore/src/main/java/com/example/appengine/GuestbookServlet.java new file mode 100644 index 00000000000..8c01d83cb09 --- /dev/null +++ b/appengine/datastore/src/main/java/com/example/appengine/GuestbookServlet.java @@ -0,0 +1,25 @@ +/* + * Copyright 2016 Google Inc. All Rights Reserved. + * + * 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/datastore/src/main/java/com/example/appengine/GuestbookStrong.java b/appengine/datastore/src/main/java/com/example/appengine/GuestbookStrong.java new file mode 100644 index 00000000000..7c481556a81 --- /dev/null +++ b/appengine/datastore/src/main/java/com/example/appengine/GuestbookStrong.java @@ -0,0 +1,74 @@ +/* + * Copyright 2016 Google Inc. All Rights Reserved. + * + * 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/datastore/src/main/java/com/example/appengine/GuestbookStrongServlet.java b/appengine/datastore/src/main/java/com/example/appengine/GuestbookStrongServlet.java new file mode 100644 index 00000000000..004584119fc --- /dev/null +++ b/appengine/datastore/src/main/java/com/example/appengine/GuestbookStrongServlet.java @@ -0,0 +1,27 @@ +/* + * Copyright 2016 Google Inc. All Rights Reserved. + * + * 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/datastore/src/main/java/com/example/appengine/ProjectionServlet.java b/appengine/datastore/src/main/java/com/example/appengine/ProjectionServlet.java new file mode 100644 index 00000000000..ffeff7d2d22 --- /dev/null +++ b/appengine/datastore/src/main/java/com/example/appengine/ProjectionServlet.java @@ -0,0 +1,79 @@ +/* + * Copyright 2016 Google Inc. All Rights Reserved. + * + * 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/datastore/src/main/java/com/example/time/Clock.java b/appengine/datastore/src/main/java/com/example/time/Clock.java new file mode 100644 index 00000000000..60ee9a49d0c --- /dev/null +++ b/appengine/datastore/src/main/java/com/example/time/Clock.java @@ -0,0 +1,35 @@ +/* + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.time; + +import org.joda.time.Instant; + +/** + * Provides the current value of "now." To preserve testability, avoid all other libraries that + * access the system clock (whether {@linkplain System#currentTimeMillis directly} or {@linkplain + * org.joda.time.DateTime#DateTime() indirectly}). + * + *

In production, use the {@link SystemClock} implementation to return the "real" system time. In + * tests, either use {@link com.example.time.testing.FakeClock}, or get an instance from a mocking + * framework such as Mockito. + */ +public interface Clock { + /** + * Returns the current, absolute time according to this clock. + */ + Instant now(); +} diff --git a/appengine/datastore/src/main/java/com/example/time/SystemClock.java b/appengine/datastore/src/main/java/com/example/time/SystemClock.java new file mode 100644 index 00000000000..ecf15c34207 --- /dev/null +++ b/appengine/datastore/src/main/java/com/example/time/SystemClock.java @@ -0,0 +1,38 @@ +/* + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.time; + +import org.joda.time.Instant; + +/** + * Clock implementation that returns the "real" system time. + * + *

This class exists so that we can use a fake implementation for unit + * testing classes that need the current time value. See {@link Clock} for + * general information about clocks. + */ +public class SystemClock implements Clock { + /** + * Creates a new instance. All {@code SystemClock} instances function identically. + */ + public SystemClock() {} + + @Override + public Instant now() { + return new Instant(); + } +} diff --git a/appengine/datastore/src/main/java/com/example/time/testing/FakeClock.java b/appengine/datastore/src/main/java/com/example/time/testing/FakeClock.java new file mode 100644 index 00000000000..5b05841e439 --- /dev/null +++ b/appengine/datastore/src/main/java/com/example/time/testing/FakeClock.java @@ -0,0 +1,186 @@ +/* + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.time.testing; + +import com.example.time.Clock; + +import org.joda.time.Instant; +import org.joda.time.ReadableDuration; +import org.joda.time.ReadableInstant; + +import java.util.concurrent.atomic.AtomicLong; + +/** + * A Clock that returns a fixed Instant value as the current clock time. The + * fixed Instant is settable for testing. Test code should hold a reference to + * the FakeClock, while code under test should hold a Clock reference. + * + *

The clock time can be incremented/decremented manually, with + * {@link #incrementTime} and {@link #decrementTime} respectively. + * + *

The clock can also be configured so that the time is incremented whenever + * {@link #now()} is called: see {@link #setAutoIncrementStep}. + */ +public class FakeClock implements Clock { + private static final Instant DEFAULT_TIME = new Instant(1000000000L); + private final long baseTimeMs; + private final AtomicLong fakeNowMs; + private volatile long autoIncrementStepMs; + + /** + * Creates a FakeClock instance initialized to an arbitrary constant. + */ + public FakeClock() { + this(DEFAULT_TIME); + } + + /** + * Creates a FakeClock instance initialized to the given time. + */ + public FakeClock(ReadableInstant now) { + baseTimeMs = now.getMillis(); + fakeNowMs = new AtomicLong(baseTimeMs); + } + + /** + * Sets the value of the underlying instance for testing purposes. + * + * @return this + */ + public FakeClock setNow(ReadableInstant now) { + fakeNowMs.set(now.getMillis()); + return this; + } + + @Override + public Instant now() { + return getAndAdd(autoIncrementStepMs); + } + + /** + * Returns the current time without applying an auto increment, if configured. + * The default behavior of {@link #now()} is the same as this method. + */ + public Instant peek() { + return new Instant(fakeNowMs.get()); + } + + /** + * Reset the given clock back to the base time with which the FakeClock was + * initially constructed. + * + * @return this + */ + public FakeClock resetTime() { + fakeNowMs.set(baseTimeMs); + return this; + } + + /** + * Increments the clock time by the given duration. + * + * @param duration the duration to increment the clock time by + * @return this + */ + public FakeClock incrementTime(ReadableDuration duration) { + incrementTime(duration.getMillis()); + return this; + } + + /** + * Increments the clock time by the given duration. + * + * @param durationMs the duration to increment the clock time by, + * in milliseconds + * @return this + */ + public FakeClock incrementTime(long durationMs) { + fakeNowMs.addAndGet(durationMs); + return this; + } + + /** + * Decrements the clock time by the given duration. + * + * @param duration the duration to decrement the clock time by + * @return this + */ + public FakeClock decrementTime(ReadableDuration duration) { + incrementTime(-duration.getMillis()); + return this; + } + + /** + * Decrements the clock time by the given duration. + * + * @param durationMs the duration to decrement the clock time by, + * in milliseconds + * @return this + */ + public FakeClock decrementTime(long durationMs) { + incrementTime(-durationMs); + return this; + } + + /** + * Sets the increment applied to the clock whenever it is queried. + * The increment is zero by default: the clock is left unchanged when queried. + * + * @param autoIncrementStep the new auto increment duration + * @return this + */ + public FakeClock setAutoIncrementStep(ReadableDuration autoIncrementStep) { + setAutoIncrementStep(autoIncrementStep.getMillis()); + return this; + } + + /** + * Sets the increment applied to the clock whenever it is queried. + * The increment is zero by default: the clock is left unchanged when queried. + * + * @param autoIncrementStepMs the new auto increment duration, in milliseconds + * @return this + */ + public FakeClock setAutoIncrementStep(long autoIncrementStepMs) { + this.autoIncrementStepMs = autoIncrementStepMs; + return this; + } + + /** + * Atomically adds the given value to the current time. + * + * @see AtomicLong#addAndGet + * + * @param durationMs the duration to add, in milliseconds + * @return the updated current time + */ + protected final Instant addAndGet(long durationMs) { + return new Instant(fakeNowMs.addAndGet(durationMs)); + } + + /** + * Atomically adds the given value to the current time. + * + * @see AtomicLong#getAndAdd + * + * @param durationMs the duration to add, in milliseconds + * @return the previous time + */ + protected final Instant getAndAdd(long durationMs) { + return new Instant(fakeNowMs.getAndAdd(durationMs)); + } +} diff --git a/appengine/datastore/src/main/webapp/WEB-INF/appengine-web.xml b/appengine/datastore/src/main/webapp/WEB-INF/appengine-web.xml new file mode 100644 index 00000000000..c322e7d016e --- /dev/null +++ b/appengine/datastore/src/main/webapp/WEB-INF/appengine-web.xml @@ -0,0 +1,20 @@ + + + + + + YOUR-PROJECT-ID + YOUR-VERSION-ID + true + diff --git a/appengine/datastore/src/main/webapp/WEB-INF/datastore-indexes.xml b/appengine/datastore/src/main/webapp/WEB-INF/datastore-indexes.xml new file mode 100644 index 00000000000..662ebb0c35e --- /dev/null +++ b/appengine/datastore/src/main/webapp/WEB-INF/datastore-indexes.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + diff --git a/appengine/datastore/src/main/webapp/WEB-INF/web.xml b/appengine/datastore/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000000..2ff1ccf54ae --- /dev/null +++ b/appengine/datastore/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,58 @@ + + + + + guestbook-strong + com.example.appengine.GuestbookStrongServlet + + + guestbook-strong + /guestbook-strong + + + guestbook + com.example.appengine.GuestbookServlet + + + guestbook + /guestbook + + + projection + com.example.appengine.ProjectionServlet + + + projection + /projection + + + + + profile + /* + + + CONFIDENTIAL + + + * + + + diff --git a/appengine/datastore/src/main/webapp/guestbook.jsp b/appengine/datastore/src/main/webapp/guestbook.jsp new file mode 100644 index 00000000000..0be736e697e --- /dev/null +++ b/appengine/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/datastore/src/test/java/com/example/appengine/GuestbookStrongTest.java b/appengine/datastore/src/test/java/com/example/appengine/GuestbookStrongTest.java new file mode 100644 index 00000000000..9237cb4a46a --- /dev/null +++ b/appengine/datastore/src/test/java/com/example/appengine/GuestbookStrongTest.java @@ -0,0 +1,105 @@ +/* + * Copyright 2016 Google Inc. All Rights Reserved. + * + * 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.example.time.testing.FakeClock; + +import com.google.appengine.api.datastore.DatastoreService; +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 org.joda.time.Instant; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.util.List; + +/** + * Unit tests for {@link GuestbookStrong}. + */ +@RunWith(JUnit4.class) +public class GuestbookStrongTest { + private static final Instant FAKE_NOW = new Instant(1234567890L); + private static final String GUESTBOOK_ID = "my guestbook"; + + private final LocalServiceTestHelper helper = + new LocalServiceTestHelper( + // Set maximum eventual consistency. + // https://cloud.google.com/appengine/docs/java/tools/localunittesting#Java_Writing_High_Replication_Datastore_tests + 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 DatastoreService datastore; + 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!"); + + assertThat(got.getContent()) + .named("content property") + .isEqualTo("Hello, Datastore!"); + } + + @Test + public void appendGreeting_normalData_setsDateProperty() { + Greeting got = guestbookUnderTest.appendGreeting("Hello, Datastore!"); + + assertThat(got.getDate()) + .named("date property") + .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/datastore/src/test/java/com/example/appengine/GuestbookTest.java b/appengine/datastore/src/test/java/com/example/appengine/GuestbookTest.java new file mode 100644 index 00000000000..403f15eb6a1 --- /dev/null +++ b/appengine/datastore/src/test/java/com/example/appengine/GuestbookTest.java @@ -0,0 +1,127 @@ +/* + * Copyright 2016 Google Inc. All Rights Reserved. + * + * 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.example.time.testing.FakeClock; + +import com.google.appengine.api.datastore.DatastoreService; +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 org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.util.List; + +/** + * 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; + } + } + + private final LocalServiceTestHelper helper = + new LocalServiceTestHelper( + // Set custom, deterministic, eventual consistency. + // https://cloud.google.com/appengine/docs/java/tools/localunittesting#Java_Writing_High_Replication_Datastore_tests + 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 DatastoreService datastore; + 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!"); + + assertThat(got.getContent()) + .named("content property") + .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/datastore/src/test/java/com/example/appengine/ProjectionServletTest.java b/appengine/datastore/src/test/java/com/example/appengine/ProjectionServletTest.java new file mode 100644 index 00000000000..7e71cfee7a5 --- /dev/null +++ b/appengine/datastore/src/test/java/com/example/appengine/ProjectionServletTest.java @@ -0,0 +1,104 @@ +/* + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.appengine; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.when; + +import com.example.time.testing.FakeClock; + +import com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig; +import com.google.appengine.tools.development.testing.LocalServiceTestHelper; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.io.PrintWriter; +import java.io.StringWriter; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * Unit tests for {@link 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.initMocks(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); + + assertThat(responseWriter.toString()) + .named("ProjectionServlet response") + .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(); + + assertThat(output) + .named("ProjectionServlet response") + .contains("Message Hello."); + assertThat(output) + .named("ProjectionServlet response") + .contains("Message Güten Tag!"); + assertThat(output) + .named("ProjectionServlet response") + .contains("Message Hola."); + } +} diff --git a/pom.xml b/pom.xml index bd747812da5..cf0cb626aab 100644 --- a/pom.xml +++ b/pom.xml @@ -45,6 +45,7 @@ appengine/analytics appengine/appidentity + appengine/datastore appengine/guestbook-objectify appengine/helloworld appengine/logs