diff --git a/appengine/guestbook-cloud-datastore/README.md b/appengine/guestbook-cloud-datastore/README.md new file mode 100644 index 00000000000..94b3edf3de9 --- /dev/null +++ b/appengine/guestbook-cloud-datastore/README.md @@ -0,0 +1,28 @@ +# appengine/guestbook-cloud-datastore + +An App Engine guestbook using Java, Maven, and the Cloud Datastore API via +[google-cloud-java](https://github.com/GoogleCloudPlatform/google-cloud-java). + +Please ask questions on [StackOverflow](http://stackoverflow.com/questions/tagged/google-app-engine). + +## Running Locally + +First, pick a project ID. You can create a project in the [Cloud Console] if you'd like, though this +isn't necessary unless you'd like to deploy the sample. + +Second, modify `Persistence.java`: replace `your-project-id-here` with the project ID you picked. + +Then start the [Cloud Datastore Emulator](https://cloud.google.com/datastore/docs/tools/datastore-emulator): + + gcloud beta emulators datastore start --project=YOUR_PROJECT_ID_HERE + +Finally, in a new shell, [set the Datastore Emulator environmental variables](https://cloud.google.com/datastore/docs/tools/datastore-emulator#setting_environment_variables) +and run + + mvn clean appengine:devserver + +## Deploying + +Modify `appengine-web.xml` to reflect your app ID and version, then: + + mvn clean appengine:update diff --git a/appengine/guestbook-cloud-datastore/pom.xml b/appengine/guestbook-cloud-datastore/pom.xml new file mode 100644 index 00000000000..d40dafdede5 --- /dev/null +++ b/appengine/guestbook-cloud-datastore/pom.xml @@ -0,0 +1,112 @@ + + + + 4.0.0 + war + 1.0-SNAPSHOT + + com.example.appengine + appengine-guestbook-cloud-datastore + + 19.0 + + + com.google.cloud + doc-samples + 1.0.0 + ../.. + + + + + 3.3.9 + + + + + + + com.google.appengine + appengine-api-1.0-sdk + ${appengine.sdk.version} + + + javax.servlet + servlet-api + 2.5 + provided + + + jstl + jstl + 1.2 + + + + com.google.cloud + google-cloud + 0.4.0 + + + + com.google.guava + guava + ${guava.version} + + + + + junit + junit + 4.12 + test + + + org.mockito + mockito-all + 1.10.19 + test + + + com.google.appengine + appengine-testing + ${appengine.sdk.version} + test + + + com.google.appengine + appengine-api-stubs + ${appengine.sdk.version} + test + + + com.google.appengine + appengine-tools-sdk + ${appengine.sdk.version} + test + + + + + + ${project.build.directory}/${project.build.finalName}/WEB-INF/classes + + + com.google.appengine + appengine-maven-plugin + ${appengine.sdk.version} + + false + + + + + + + + + diff --git a/appengine/guestbook-cloud-datastore/src/main/java/com/example/guestbook/Greeting.java b/appengine/guestbook-cloud-datastore/src/main/java/com/example/guestbook/Greeting.java new file mode 100644 index 00000000000..988118174c0 --- /dev/null +++ b/appengine/guestbook-cloud-datastore/src/main/java/com/example/guestbook/Greeting.java @@ -0,0 +1,117 @@ +/** + * 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. + */ + +//[START all] +package com.example.guestbook; + +import static com.example.guestbook.Persistence.getDatastore; + +import com.google.cloud.datastore.DateTime; +import com.google.cloud.datastore.Entity; +import com.google.cloud.datastore.FullEntity; +import com.google.cloud.datastore.FullEntity.Builder; +import com.google.cloud.datastore.IncompleteKey; +import com.google.cloud.datastore.Key; +import java.util.Date; +import java.util.Objects; + +public class Greeting { + private Guestbook book; + + public Key key; + + public String authorEmail; + public String authorId; + public String content; + public Date date; + + public Greeting() { + date = new Date(); + } + + public Greeting(String book, String content) { + this(); + this.book = new Guestbook(book); + this.content = content; + } + + public Greeting(String book, String content, String id, String email) { + this(book, content); + authorEmail = email; + authorId = id; + } + + /** + * Load greeting from Datastore entity + * + * @param entity + */ + public Greeting(Entity entity) { + key = entity.hasKey() ? entity.key() : null; + authorEmail = entity.contains("authorEmail") ? entity.getString("authorEmail") : null; + authorId = entity.contains("authorId") ? entity.getString("authorId") : null; + date = entity.contains("date") ? entity.getDateTime("date").toDate() : null; + content = entity.contains("content") ? entity.getString("content") : null; + } + + public void save() { + if (key == null) { + key = getDatastore().allocateId(makeIncompleteKey()); // Give this greeting a unique ID + } + + Builder builder = FullEntity.builder(key); + + if (authorEmail != null) { + builder.set("authorEmail", authorEmail); + } + + if (authorId != null) { + builder.set("authorId", authorId); + } + + builder.set("content", content); + builder.set("date", DateTime.copyFrom(date)); + + getDatastore().put(builder.build()); + } + + private IncompleteKey makeIncompleteKey() { + // The book is our ancestor key. + return Key.builder(book.getKey(), "Greeting").build(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Greeting greeting = (Greeting) o; + return Objects.equals(key, greeting.key) && + Objects.equals(authorEmail, greeting.authorEmail) && + Objects.equals(authorId, greeting.authorId) && + Objects.equals(content, greeting.content) && + Objects.equals(date, greeting.date); + } + + @Override + public int hashCode() { + return Objects.hash(key, authorEmail, authorId, content, date); + } +} +//[END all] \ No newline at end of file diff --git a/appengine/guestbook-cloud-datastore/src/main/java/com/example/guestbook/Guestbook.java b/appengine/guestbook-cloud-datastore/src/main/java/com/example/guestbook/Guestbook.java new file mode 100644 index 00000000000..694010ab5c5 --- /dev/null +++ b/appengine/guestbook-cloud-datastore/src/main/java/com/example/guestbook/Guestbook.java @@ -0,0 +1,69 @@ +/** + * 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.guestbook; + +import static com.example.guestbook.Persistence.getDatastore; +import static com.example.guestbook.Persistence.getKeyFactory; +import static com.google.cloud.datastore.StructuredQuery.OrderBy.desc; +import static com.google.cloud.datastore.StructuredQuery.PropertyFilter.hasAncestor; + +import com.google.cloud.datastore.Entity; +import com.google.cloud.datastore.EntityQuery; +import com.google.cloud.datastore.Key; +import com.google.cloud.datastore.KeyFactory; +import com.google.cloud.datastore.Query; +import com.google.cloud.datastore.QueryResults; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableList.Builder; +import java.util.List; + +//[START all] +public class Guestbook { + private static final KeyFactory kf = getKeyFactory(Guestbook.class); + + private final Key key; + public final String book; + + public Guestbook(String book) { + this.book = book == null ? "default" : book; + key = kf.newKey(this.book); // There is a 1:1 mapping between Guestbook names and Guestbook objects + } + + public Key getKey() { + return key; + } + + public List getGreetings() { + // This query requires the index defined in index.yaml to work because of the orderBy on date. + EntityQuery query = Query.entityQueryBuilder() + .kind("Greeting") + .filter(hasAncestor(key)) + .orderBy(desc("date")) + .limit(5) + .build(); + + QueryResults results = getDatastore().run(query); + + Builder resultListBuilder = ImmutableList.builder(); + while (results.hasNext()) { + resultListBuilder.add(new Greeting(results.next())); + } + + return resultListBuilder.build(); + } +} +//[END all] \ No newline at end of file diff --git a/appengine/guestbook-cloud-datastore/src/main/java/com/example/guestbook/Persistence.java b/appengine/guestbook-cloud-datastore/src/main/java/com/example/guestbook/Persistence.java new file mode 100644 index 00000000000..3aee65c89cc --- /dev/null +++ b/appengine/guestbook-cloud-datastore/src/main/java/com/example/guestbook/Persistence.java @@ -0,0 +1,45 @@ +/** + * 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.guestbook; + +import com.google.cloud.datastore.Datastore; +import com.google.cloud.datastore.DatastoreOptions; +import com.google.cloud.datastore.KeyFactory; + +import java.util.concurrent.atomic.AtomicReference; + +//[START all] +public class Persistence { + private static AtomicReference datastore = new AtomicReference<>(); + + public static Datastore getDatastore() { + if (datastore.get() == null) { + datastore.set(DatastoreOptions.builder().projectId("your-project-id-here").build().service()); + } + + return datastore.get(); + } + + public static KeyFactory getKeyFactory(Class c) { + return getDatastore().newKeyFactory().kind(c.getSimpleName()); + } + + public static void setDatastore(Datastore datastore) { + Persistence.datastore.set(datastore); + } +} +//[END all] diff --git a/appengine/guestbook-cloud-datastore/src/main/java/com/example/guestbook/SignGuestbookServlet.java b/appengine/guestbook-cloud-datastore/src/main/java/com/example/guestbook/SignGuestbookServlet.java new file mode 100644 index 00000000000..eec1a34c2a8 --- /dev/null +++ b/appengine/guestbook-cloud-datastore/src/main/java/com/example/guestbook/SignGuestbookServlet.java @@ -0,0 +1,52 @@ +/** + * 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. + */ + +//[START all] +package com.example.guestbook; + +import com.google.appengine.api.users.User; +import com.google.appengine.api.users.UserService; +import com.google.appengine.api.users.UserServiceFactory; +import java.io.IOException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +//[START all] +public class SignGuestbookServlet extends HttpServlet { + // Process the HTTP POST of the form + @Override + public void doPost(HttpServletRequest req, HttpServletResponse resp) + throws IOException { + Greeting greeting; + + UserService userService = UserServiceFactory.getUserService(); + User user = userService.getCurrentUser(); // Find out who the user is. + + String guestbookName = req.getParameter("guestbookName"); + String content = req.getParameter("content"); + if (user != null) { + greeting = new Greeting(guestbookName, content, user.getUserId(), user.getEmail()); + } else { + greeting = new Greeting(guestbookName, content); + } + + greeting.save(); + + resp.sendRedirect("/guestbook.jsp?guestbookName=" + guestbookName); + } +} +//[END all] diff --git a/appengine/guestbook-cloud-datastore/src/main/webapp/WEB-INF/appengine-web.xml b/appengine/guestbook-cloud-datastore/src/main/webapp/WEB-INF/appengine-web.xml new file mode 100644 index 00000000000..14ea1140aa7 --- /dev/null +++ b/appengine/guestbook-cloud-datastore/src/main/webapp/WEB-INF/appengine-web.xml @@ -0,0 +1,10 @@ + + + your-app-id-here + your-app-version-here + true + + + + + diff --git a/appengine/guestbook-cloud-datastore/src/main/webapp/WEB-INF/index.yaml b/appengine/guestbook-cloud-datastore/src/main/webapp/WEB-INF/index.yaml new file mode 100644 index 00000000000..03f9df1f728 --- /dev/null +++ b/appengine/guestbook-cloud-datastore/src/main/webapp/WEB-INF/index.yaml @@ -0,0 +1,7 @@ +indexes: + +- kind: Greeting + ancestor: yes + properties: + - name: date + direction: desc \ No newline at end of file diff --git a/appengine/guestbook-cloud-datastore/src/main/webapp/WEB-INF/logging.properties b/appengine/guestbook-cloud-datastore/src/main/webapp/WEB-INF/logging.properties new file mode 100644 index 00000000000..a17206681f0 --- /dev/null +++ b/appengine/guestbook-cloud-datastore/src/main/webapp/WEB-INF/logging.properties @@ -0,0 +1,13 @@ +# A default java.util.logging configuration. +# (All App Engine logging is through java.util.logging by default). +# +# To use this configuration, copy it into your application's WEB-INF +# folder and add the following to your appengine-web.xml: +# +# +# +# +# + +# Set the default logging level for all loggers to WARNING +.level = WARNING diff --git a/appengine/guestbook-cloud-datastore/src/main/webapp/WEB-INF/web.xml b/appengine/guestbook-cloud-datastore/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000000..1ffb6bb5853 --- /dev/null +++ b/appengine/guestbook-cloud-datastore/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,22 @@ + + + + + + sign + com.example.guestbook.SignGuestbookServlet + 1 + + + + sign + /sign + + + + guestbook.jsp + + + diff --git a/appengine/guestbook-cloud-datastore/src/main/webapp/guestbook.jsp b/appengine/guestbook-cloud-datastore/src/main/webapp/guestbook.jsp new file mode 100644 index 00000000000..163fd1a29b3 --- /dev/null +++ b/appengine/guestbook-cloud-datastore/src/main/webapp/guestbook.jsp @@ -0,0 +1,98 @@ +<%-- //[START all]--%> +<%@ page contentType="text/html;charset=UTF-8" language="java" %> +<%@ page import="com.google.appengine.api.users.User" %> +<%@ page import="com.google.appengine.api.users.UserService" %> +<%@ page import="com.google.appengine.api.users.UserServiceFactory" %> + +<%-- //[START imports]--%> +<%@ page import="com.example.guestbook.Greeting" %> +<%@ page import="com.example.guestbook.Guestbook" %> +<%-- //[END imports]--%> + +<%@ page import="java.util.List" %> +<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %> + + + + + + + + +<% + String guestbookName = request.getParameter("guestbookName"); + if (guestbookName == null) { + guestbookName = "default"; + } + pageContext.setAttribute("guestbookName", guestbookName); + UserService userService = UserServiceFactory.getUserService(); + User user = userService.getCurrentUser(); + if (user != null) { + pageContext.setAttribute("user", user); +%> + +

Hello, ${fn:escapeXml(user.nickname)}! (You can + sign out.)

+<% + } else { +%> +

Hello! + Sign in + to include your name with greetings you post.

+<% + } +%> + +<%-- //[START datastore]--%> +<% + // Create the correct Ancestor key + Guestbook theBook = new Guestbook(guestbookName); + + // Run an ancestor query to ensure we see the most up-to-date + // view of the Greetings belonging to the selected Guestbook. + List greetings = theBook.getGreetings(); + + if (greetings.isEmpty()) { +%> +

Guestbook '${fn:escapeXml(guestbookName)}' has no messages.

+<% + } else { +%> +

Messages in Guestbook '${fn:escapeXml(guestbookName)}'.

+<% + // Look at all of our greetings + for (Greeting greeting : greetings) { + pageContext.setAttribute("greeting_content", greeting.content); + String author; + if (greeting.authorEmail == null) { + author = "An anonymous person"; + } else { + author = greeting.authorEmail; + String author_id = greeting.authorId; + if (user != null && user.getUserId().equals(author_id)) { + author += " (You)"; + } + } + pageContext.setAttribute("greeting_user", author); +%> +

${fn:escapeXml(greeting_user)} wrote:

+
${fn:escapeXml(greeting_content)}
+<% + } + } +%> + +
+
+
+ +
+<%-- //[END datastore]--%> +
+
+
+
+ + + +<%-- //[END all]--%> diff --git a/appengine/guestbook-cloud-datastore/src/main/webapp/stylesheets/main.css b/appengine/guestbook-cloud-datastore/src/main/webapp/stylesheets/main.css new file mode 100644 index 00000000000..05d72d5536d --- /dev/null +++ b/appengine/guestbook-cloud-datastore/src/main/webapp/stylesheets/main.css @@ -0,0 +1,4 @@ +body { + font-family: Verdana, Helvetica, sans-serif; + background-color: #FFFFCC; +} diff --git a/appengine/guestbook-cloud-datastore/src/test/java/com/example/guestbook/GreetingTest.java b/appengine/guestbook-cloud-datastore/src/test/java/com/example/guestbook/GreetingTest.java new file mode 100644 index 00000000000..8b3eed2451d --- /dev/null +++ b/appengine/guestbook-cloud-datastore/src/test/java/com/example/guestbook/GreetingTest.java @@ -0,0 +1,51 @@ +/** + * 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.guestbook; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +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; + +@RunWith(JUnit4.class) +public class GreetingTest { + @Before + public void setUp() { + TestUtils.startDatastore(); + } + + @Test + public void testSaveGreeting() throws Exception { + Greeting greeting = new Greeting(null, "Test!"); + greeting.save(); + + Guestbook guestbook = new Guestbook(null); + List greetings = guestbook.getGreetings(); + assertTrue(greetings.size() == 1); + assertEquals(greeting, greetings.get(0)); + } + + @After + public void tearDown() { + TestUtils.stopDatastore(); + } +} diff --git a/appengine/guestbook-cloud-datastore/src/test/java/com/example/guestbook/SignGuestbookServletTest.java b/appengine/guestbook-cloud-datastore/src/test/java/com/example/guestbook/SignGuestbookServletTest.java new file mode 100644 index 00000000000..03aaa73bff6 --- /dev/null +++ b/appengine/guestbook-cloud-datastore/src/test/java/com/example/guestbook/SignGuestbookServletTest.java @@ -0,0 +1,73 @@ +/** + * 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.guestbook; + +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.when; + +import com.google.appengine.tools.development.testing.LocalServiceTestHelper; +import java.util.List; +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; + +@RunWith(JUnit4.class) +public class SignGuestbookServletTest { + private final LocalServiceTestHelper helper = new LocalServiceTestHelper(); + + @Mock private HttpServletRequest mockRequest; + @Mock private HttpServletResponse mockResponse; + + private SignGuestbookServlet signGuestbookServlet; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + // Sets up the UserServiceFactory used in SignGuestbookServlet (but not in this test) + helper.setUp(); + + signGuestbookServlet = new SignGuestbookServlet(); + TestUtils.startDatastore(); + } + + @Test + public void doPost_userNotLoggedIn() throws Exception { + String testBook = "default"; + when(mockRequest.getParameter("guestbookName")).thenReturn(testBook); + String testGreeting = "beep!"; + when(mockRequest.getParameter("content")).thenReturn(testGreeting); + + signGuestbookServlet.doPost(mockRequest, mockResponse); + Guestbook guestbook = new Guestbook(testBook); + List greetings = guestbook.getGreetings(); + + assertTrue(greetings.size() == 1); + assertTrue(greetings.get(0).content.equals(testGreeting)); + } + + @After + public void tearDown() { + TestUtils.stopDatastore(); + } +} diff --git a/appengine/guestbook-cloud-datastore/src/test/java/com/example/guestbook/TestUtils.java b/appengine/guestbook-cloud-datastore/src/test/java/com/example/guestbook/TestUtils.java new file mode 100644 index 00000000000..7a9716e467b --- /dev/null +++ b/appengine/guestbook-cloud-datastore/src/test/java/com/example/guestbook/TestUtils.java @@ -0,0 +1,45 @@ +package com.example.guestbook; + +import static com.example.guestbook.Persistence.getDatastore; + +import com.google.cloud.datastore.Datastore; +import com.google.cloud.datastore.Key; +import com.google.cloud.datastore.Query; +import com.google.cloud.datastore.QueryResults; +import com.google.cloud.datastore.testing.LocalDatastoreHelper; +import com.google.common.collect.Lists; + +import java.io.IOException; +import java.util.ArrayList; + +public class TestUtils { + static LocalDatastoreHelper datastore = LocalDatastoreHelper.create(); + + public static void startDatastore() { + try { + datastore.start(); + Persistence.setDatastore(datastore.options().service()); + } catch (IOException | InterruptedException e) { + throw new RuntimeException(e); + } + } + + public static void stopDatastore() { + try { + datastore.stop(); + Persistence.setDatastore(null); + } catch (IOException | InterruptedException e) { + throw new RuntimeException(e); + } + } + + public static void wipeDatastore() { + Datastore datastore = getDatastore(); + QueryResults guestbooks = datastore.run(Query.keyQueryBuilder().kind("Greeting").build()); + ArrayList keys = Lists.newArrayList(guestbooks); + + if (!keys.isEmpty()) { + datastore.delete(keys.toArray(new Key[keys.size()])); + } + } +}