From ad0af31ca92484114b3581e15bfaee7ceb2f68bc Mon Sep 17 00:00:00 2001 From: Tim Swast Date: Wed, 27 Apr 2016 16:34:50 -0700 Subject: [PATCH] Add Query Cursor Datastore sample. (#207) Moved from https://cloud.google.com/appengine/docs/java/datastore/query-cursors#query_cursor_example --- .../example/appengine/ListPeopleServlet.java | 91 ++++++++++ .../com/example/appengine/StartupServlet.java | 120 +++++++++++++ .../datastore/src/main/webapp/WEB-INF/web.xml | 20 ++- .../appengine/ListPeopleServletTest.java | 163 ++++++++++++++++++ .../example/appengine/StartupServletTest.java | 106 ++++++++++++ 5 files changed, 499 insertions(+), 1 deletion(-) create mode 100644 appengine/datastore/src/main/java/com/example/appengine/ListPeopleServlet.java create mode 100644 appengine/datastore/src/main/java/com/example/appengine/StartupServlet.java create mode 100644 appengine/datastore/src/test/java/com/example/appengine/ListPeopleServletTest.java create mode 100644 appengine/datastore/src/test/java/com/example/appengine/StartupServletTest.java diff --git a/appengine/datastore/src/main/java/com/example/appengine/ListPeopleServlet.java b/appengine/datastore/src/main/java/com/example/appengine/ListPeopleServlet.java new file mode 100644 index 00000000000..2da95898980 --- /dev/null +++ b/appengine/datastore/src/main/java/com/example/appengine/ListPeopleServlet.java @@ -0,0 +1,91 @@ +/* + * 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; + +// [START cursors] +import com.google.appengine.api.datastore.Cursor; +import com.google.appengine.api.datastore.DatastoreService; +import com.google.appengine.api.datastore.DatastoreServiceFactory; +import com.google.appengine.api.datastore.Entity; +import com.google.appengine.api.datastore.FetchOptions; +import com.google.appengine.api.datastore.PreparedQuery; +import com.google.appengine.api.datastore.Query; +import com.google.appengine.api.datastore.Query.SortDirection; +import com.google.appengine.api.datastore.QueryResultList; + +import java.io.IOException; +import java.io.PrintWriter; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public class ListPeopleServlet extends HttpServlet { + static final int PAGE_SIZE = 15; + private final DatastoreService datastore; + + public ListPeopleServlet() { + datastore = DatastoreServiceFactory.getDatastoreService(); + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + FetchOptions fetchOptions = FetchOptions.Builder.withLimit(PAGE_SIZE); + + // If this servlet is passed a cursor parameter, let's use it. + String startCursor = req.getParameter("cursor"); + if (startCursor != null) { + fetchOptions.startCursor(Cursor.fromWebSafeString(startCursor)); + } + + Query q = new Query("Person").addSort("name", SortDirection.ASCENDING); + PreparedQuery pq = datastore.prepare(q); + + QueryResultList results; + try { + results = pq.asQueryResultList(fetchOptions); + } catch (IllegalArgumentException e) { + // IllegalArgumentException happens when an invalid cursor is used. + // A user could have manually entered a bad cursor in the URL or there + // may have been an internal implementation detail change in App Engine. + // Redirect to the page without the cursor parameter to show something + // rather than an error. + resp.sendRedirect("/people"); + return; + } + + resp.setContentType("text/html"); + resp.setCharacterEncoding("UTF-8"); + PrintWriter w = resp.getWriter(); + w.println(""); + w.println(""); + w.println("Cloud Datastore Cursor Sample"); + w.println(""); + + String cursorString = results.getCursor().toWebSafeString(); + + // This servlet lives at '/people'. + w.println("Next page"); + } +} +// [END cursors] diff --git a/appengine/datastore/src/main/java/com/example/appengine/StartupServlet.java b/appengine/datastore/src/main/java/com/example/appengine/StartupServlet.java new file mode 100644 index 00000000000..03fe99b3ea9 --- /dev/null +++ b/appengine/datastore/src/main/java/com/example/appengine/StartupServlet.java @@ -0,0 +1,120 @@ +/* + * 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.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/datastore/src/main/webapp/WEB-INF/web.xml b/appengine/datastore/src/main/webapp/WEB-INF/web.xml index 2ff1ccf54ae..9cb58bff05a 100644 --- a/appengine/datastore/src/main/webapp/WEB-INF/web.xml +++ b/appengine/datastore/src/main/webapp/WEB-INF/web.xml @@ -24,7 +24,7 @@ guestbook-strong - /guestbook-strong + / guestbook @@ -34,6 +34,14 @@ guestbook /guestbook + + people + com.example.appengine.ListPeopleServlet + + + people + /people + projection com.example.appengine.ProjectionServlet @@ -43,6 +51,16 @@ /projection + + + startup + com.example.appengine.StartupServlet + + + startup + /_ah/start + + profile diff --git a/appengine/datastore/src/test/java/com/example/appengine/ListPeopleServletTest.java b/appengine/datastore/src/test/java/com/example/appengine/ListPeopleServletTest.java new file mode 100644 index 00000000000..b1f33bbf3f9 --- /dev/null +++ b/appengine/datastore/src/test/java/com/example/appengine/ListPeopleServletTest.java @@ -0,0 +1,163 @@ +/* + * 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.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 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 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(); + + 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 ListPeopleServlet servletUnderTest; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(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++) { + assertThat(response).named("ListPeopleServlet 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) { + assertThat(response) + .named("ListPeopleServlet 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/datastore/src/test/java/com/example/appengine/StartupServletTest.java b/appengine/datastore/src/test/java/com/example/appengine/StartupServletTest.java new file mode 100644 index 00000000000..cf54f983097 --- /dev/null +++ b/appengine/datastore/src/test/java/com/example/appengine/StartupServletTest.java @@ -0,0 +1,106 @@ +/* + * 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.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 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 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.initMocks(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); + assertThat(responseWriter.toString()).named("StartupServlet response").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(); + assertThat(result.getProperty("name")).named("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); + assertThat(responseWriter.toString()).named("StartupServlet response").isEqualTo("ok\n"); + } +}