diff --git a/appengine/multitenancy/README.md b/appengine/multitenancy/README.md new file mode 100644 index 00000000000..dbededfd393 --- /dev/null +++ b/appengine/multitenancy/README.md @@ -0,0 +1,19 @@ +# Multitenancy Java sample + +Shows the usage of the Namespaces API. + +An App Engine guestbook using Java, Maven, and Objectify. + +Data access using [Objectify](https://github.com/objectify/objectify) + +Please ask questions on [Stackoverflow](http://stackoverflow.com/questions/tagged/google-app-engine) + +## Running Locally + +How do I, as a developer, start working on the project? + +1. `mvn clean appengine:devserver` + +## Deploying + +1. `mvn clean appengine:update -Dappengine.appId=PROJECT -Dappengine.version=VERSION` diff --git a/appengine/multitenancy/pom.xml b/appengine/multitenancy/pom.xml new file mode 100644 index 00000000000..2892507d8ed --- /dev/null +++ b/appengine/multitenancy/pom.xml @@ -0,0 +1,122 @@ + + + + 4.0.0 + war + 1.0-SNAPSHOT + + com.example.appengine + appengine-multitenancy + + + 5.1.5 + 18.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.guava + guava + ${guava.version} + + + com.googlecode.objectify + objectify + ${objectify.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 + + + 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} + + false + + + + + + + + + diff --git a/appengine/multitenancy/src/main/java/com/example/appengine/Greeting.java b/appengine/multitenancy/src/main/java/com/example/appengine/Greeting.java new file mode 100644 index 00000000000..bc4a8252b57 --- /dev/null +++ b/appengine/multitenancy/src/main/java/com/example/appengine/Greeting.java @@ -0,0 +1,76 @@ +/** + * Copyright 2014-2015 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.appengine; + +import com.googlecode.objectify.Key; +import com.googlecode.objectify.annotation.Entity; +import com.googlecode.objectify.annotation.Id; +import com.googlecode.objectify.annotation.Index; +import com.googlecode.objectify.annotation.Parent; + +import java.util.Date; + +/** + * The @Entity tells Objectify about our entity. We also register it in {@link OfyHelper} + * Our primary key @Id is set automatically by the Google Datastore for us. + * + * We add a @Parent to tell the object about its ancestor. We are doing this to support many + * guestbooks. Objectify, unlike the AppEngine library requires that you specify the fields you + * want to index using @Index. Only indexing the fields you need can lead to substantial gains in + * performance -- though if not indexing your data from the start will require indexing it later. + * + * NOTE - all the properties are PUBLIC so that can keep the code simple. + **/ +@Entity +public class Greeting { + @Parent Key theBook; + @Id public Long id; + + public String authorEmail; + public String authorId; + public String content; + @Index public Date date; + + /** + * Simple constructor just sets the date. + **/ + public Greeting() { + date = new Date(); + } + + /** + * A convenience constructor. + **/ + public Greeting(String book, String content) { + this(); + if ( book != null ) { + theBook = Key.create(Guestbook.class, book); // Creating the Ancestor key + } else { + theBook = Key.create(Guestbook.class, "default"); + } + this.content = content; + } + + public Greeting(String book, String content, String id, String email) { + this(book, content); + authorEmail = email; + authorId = id; + } + +} +//[END all] diff --git a/appengine/multitenancy/src/main/java/com/example/appengine/Guestbook.java b/appengine/multitenancy/src/main/java/com/example/appengine/Guestbook.java new file mode 100644 index 00000000000..bc22587b99f --- /dev/null +++ b/appengine/multitenancy/src/main/java/com/example/appengine/Guestbook.java @@ -0,0 +1,33 @@ +/** + * Copyright 2014-2015 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.appengine; + +import com.googlecode.objectify.annotation.Entity; +import com.googlecode.objectify.annotation.Id; + +/** + * The @Entity tells Objectify about our entity. We also register it in + * OfyHelper.java -- very important. + * + * This is never actually created, but gives a hint to Objectify about our Ancestor key. + */ +@Entity +public class Guestbook { + @Id public String book; +} +//[END all] diff --git a/appengine/multitenancy/src/main/java/com/example/appengine/MultitenancyServlet.java b/appengine/multitenancy/src/main/java/com/example/appengine/MultitenancyServlet.java new file mode 100644 index 00000000000..78aaf1ca7f2 --- /dev/null +++ b/appengine/multitenancy/src/main/java/com/example/appengine/MultitenancyServlet.java @@ -0,0 +1,114 @@ +/* + * 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.NamespaceManager; +import com.google.appengine.api.memcache.MemcacheService; +import com.google.appengine.api.memcache.MemcacheServiceFactory; +import com.google.appengine.api.search.Index; +import com.google.appengine.api.search.IndexSpec; +import com.google.appengine.api.search.SearchService; +import com.google.appengine.api.search.SearchServiceConfig; +import com.google.appengine.api.search.SearchServiceFactory; +import com.google.appengine.api.users.UserServiceFactory; + +import java.io.IOException; +import java.io.PrintWriter; + +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +// [START example] +@SuppressWarnings("serial") +public class MultitenancyServlet extends HttpServlet { + + @Override + public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { + String namespace; + + PrintWriter out = resp.getWriter(); + out.println("Code Snippets -- not yet fully runnable as an app"); + + // [START temp_namespace] +// Set the namepace temporarily to "abc" + String oldNamespace = NamespaceManager.get(); + NamespaceManager.set("abc"); + try { +// ... perform operation using current namespace ... + } finally { + NamespaceManager.set(oldNamespace); + } +// [END temp_namespace] + + // [START per_user_namespace] + if (com.google.appengine.api.NamespaceManager.get() == null) { + // Assuming there is a logged in user. + namespace = UserServiceFactory.getUserService().getCurrentUser().getUserId(); + NamespaceManager.set(namespace); + } +// [END per_user_namespace] + String value = "something here"; + + // [START ns_memcache] + // Create a MemcacheService that uses the current namespace by + // calling NamespaceManager.get() for every access. + MemcacheService current = MemcacheServiceFactory.getMemcacheService(); + + // stores value in namespace "abc" + oldNamespace = NamespaceManager.get(); + NamespaceManager.set("abc"); + try { + current.put("key", value); // stores value in namespace “abc” + } finally { + NamespaceManager.set(oldNamespace); + } +// [END ns_memcache] + + // [START specific_memcache] + // Create a MemcacheService that uses the namespace "abc". + MemcacheService explicit = MemcacheServiceFactory.getMemcacheService("abc"); + explicit.put("key", value); // stores value in namespace "abc" + // [END specific_memcache] + + //[START searchns] + // Set the current namespace to "aSpace" + NamespaceManager.set("aSpace"); + // Create a SearchService with the namespace "aSpace" + SearchService searchService = SearchServiceFactory.getSearchService(); + // Create an IndexSpec + IndexSpec indexSpec = IndexSpec.newBuilder().setName("myIndex").build(); + // Create an Index with the namespace "aSpace" + Index index = searchService.getIndex(indexSpec); + // [END searchns] + + // [START searchns_2] + // Create a SearchServiceConfig, specifying the namespace "anotherSpace" + SearchServiceConfig config = SearchServiceConfig.newBuilder() + .setNamespace("anotherSpace").build(); + // Create a SearchService with the namespace "anotherSpace" + searchService = SearchServiceFactory.getSearchService(config); + // Create an IndexSpec + indexSpec = IndexSpec.newBuilder().setName("myindex").build(); + // Create an Index with the namespace "anotherSpace" + index = searchService.getIndex(indexSpec); + // [END searchns_2] + + } + + + +} +// [END example] diff --git a/appengine/multitenancy/src/main/java/com/example/appengine/NamespaceFilter.java b/appengine/multitenancy/src/main/java/com/example/appengine/NamespaceFilter.java new file mode 100644 index 00000000000..b67802c8339 --- /dev/null +++ b/appengine/multitenancy/src/main/java/com/example/appengine/NamespaceFilter.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. + */ + +package com.example.appengine; + +import com.google.appengine.api.NamespaceManager; + +import java.io.IOException; +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; + +// [START nsfilter] +// Filter to set the Google Apps domain as the namespace. +public class NamespaceFilter implements Filter { + @Override + public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) + throws IOException, ServletException { + // Make sure set() is only called if the current namespace is not already set. + if (NamespaceManager.get() == null) { + // If your app is hosted on appspot, this will be empty. Otherwise it will be the domain + // the app is hosted on. + NamespaceManager.set(NamespaceManager.getGoogleAppsNamespace()); + } + chain.doFilter(req, res); // Pass request back down the filter chain + } +// [END nsfilter] + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + + } + + @Override + public void destroy() { + + } +} diff --git a/appengine/multitenancy/src/main/java/com/example/appengine/OfyHelper.java b/appengine/multitenancy/src/main/java/com/example/appengine/OfyHelper.java new file mode 100644 index 00000000000..641c9c4f8ab --- /dev/null +++ b/appengine/multitenancy/src/main/java/com/example/appengine/OfyHelper.java @@ -0,0 +1,40 @@ +/** + * Copyright 2014-2015 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.appengine; + +import com.googlecode.objectify.ObjectifyService; + +import javax.servlet.ServletContextEvent; +import javax.servlet.ServletContextListener; + +/** + * OfyHelper, a ServletContextListener, is setup in web.xml to run before a JSP is run. This is + * required to let JSP's access Ofy. + **/ +public class OfyHelper implements ServletContextListener { + public void contextInitialized(ServletContextEvent event) { + // This will be invoked as part of a warmup request, or the first user request if no warmup + // request. + ObjectifyService.register(Guestbook.class); + ObjectifyService.register(Greeting.class); + } + + public void contextDestroyed(ServletContextEvent event) { + // App Engine does not currently invoke this method. + } +} +//[END all] diff --git a/appengine/multitenancy/src/main/java/com/example/appengine/SignGuestbookServlet.java b/appengine/multitenancy/src/main/java/com/example/appengine/SignGuestbookServlet.java new file mode 100644 index 00000000000..44dcb4151e7 --- /dev/null +++ b/appengine/multitenancy/src/main/java/com/example/appengine/SignGuestbookServlet.java @@ -0,0 +1,61 @@ +/** + * Copyright 2014-2015 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.appengine; + +import com.google.appengine.api.users.User; +import com.google.appengine.api.users.UserService; +import com.google.appengine.api.users.UserServiceFactory; + +import com.googlecode.objectify.ObjectifyService; + +import java.io.IOException; + +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * Form Handling Servlet - most of the action for this sample is in webapp/guestbook.jsp. + * It displays {@link Greeting}'s. + */ +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); + } + + // Use Objectify to save the greeting and now() is used to make the call synchronously as we + // will immediately get a new page using redirect and we want the data to be present. + ObjectifyService.ofy().save().entity(greeting).now(); + + resp.sendRedirect("/guestbook.jsp?guestbookName=" + guestbookName); + } +} +//[END all] diff --git a/appengine/multitenancy/src/main/java/com/example/appengine/SomeRequestServlet.java b/appengine/multitenancy/src/main/java/com/example/appengine/SomeRequestServlet.java new file mode 100644 index 00000000000..265f60f3f17 --- /dev/null +++ b/appengine/multitenancy/src/main/java/com/example/appengine/SomeRequestServlet.java @@ -0,0 +1,55 @@ +/* + * 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.NamespaceManager; +import com.google.appengine.api.taskqueue.QueueFactory; +import com.google.appengine.api.taskqueue.TaskOptions; + +import java.io.IOException; + +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +// [START tq_3] +public class SomeRequestServlet extends HttpServlet { + // Handler for URL get requests. + @Override + protected void doGet(HttpServletRequest req, + HttpServletResponse resp) + throws IOException { + + // Increment the count for the current namespace asynchronously. + QueueFactory.getDefaultQueue().add( + TaskOptions.Builder.withUrl("/_ah/update_count") + .param("countName", "SomeRequest")); + // Increment the global count and set the + // namespace locally. The namespace is + // transferred to the invoked request and + // executed asynchronously. + String namespace = NamespaceManager.get(); + try { + NamespaceManager.set("-global-"); + QueueFactory.getDefaultQueue().add( + TaskOptions.Builder.withUrl("/_ah/update_count") + .param("countName", "SomeRequest")); + } finally { + NamespaceManager.set(namespace); + } + resp.setContentType("text/plain"); + resp.getWriter().println("Counts are being updated."); + } +} +// [END tq_3] \ No newline at end of file diff --git a/appengine/multitenancy/src/main/java/com/example/appengine/UpdateCountsServlet.java b/appengine/multitenancy/src/main/java/com/example/appengine/UpdateCountsServlet.java new file mode 100644 index 00000000000..b65b40ee92a --- /dev/null +++ b/appengine/multitenancy/src/main/java/com/example/appengine/UpdateCountsServlet.java @@ -0,0 +1,99 @@ +/* + * 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.googlecode.objectify.ObjectifyService.ofy; + +import com.google.appengine.api.NamespaceManager; + +import com.googlecode.objectify.annotation.Entity; +import com.googlecode.objectify.annotation.Id; +import com.googlecode.objectify.annotation.Index; + +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +// [START datastore] +// [START tq_1] +public class UpdateCountsServlet extends HttpServlet { + private static final int NUM_RETRIES = 10; + + @Entity public class CounterPojo { + @Id public Long id; + @Index public String name; + public Long count; + + public CounterPojo() { + this.count = 0L; + } + + public CounterPojo(String name) { + this.name = name; + this.count = 0L; + } + + public void increment() { + count++; + } + } + + // Increment the count in a Counter datastore entity. + public long updateCount(String countName) { + + CounterPojo cp = ofy().load().type(CounterPojo.class).filter("name", countName).first().now(); + if (cp == null) { + cp = new CounterPojo(countName); + } + cp.increment(); + ofy().save().entity(cp).now(); + + return cp.count; + } +// [END tq_1] + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws java.io.IOException { + + // Update the count for the current namespace. + updateCount("request"); + + // Update the count for the "-global-" namespace. + String namespace = NamespaceManager.get(); + try { + // "-global-" is namespace reserved by the application. + NamespaceManager.set("-global-"); + updateCount("request"); + } finally { + NamespaceManager.set(namespace); + } + resp.setContentType("text/plain"); + resp.getWriter().println("Counts are now updated."); + } + // [END datastore] + + // [START tq_2] + // called from Task Queue + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) { + String[] countName = req.getParameterValues("countName"); + if (countName.length != 1) { + resp.setStatus(HttpServletResponse.SC_BAD_REQUEST); + return; + } + updateCount(countName[0]); + } + // [END tq_2] +} diff --git a/appengine/multitenancy/src/main/webapp/WEB-INF/appengine-web.xml b/appengine/multitenancy/src/main/webapp/WEB-INF/appengine-web.xml new file mode 100644 index 00000000000..8bf645d1013 --- /dev/null +++ b/appengine/multitenancy/src/main/webapp/WEB-INF/appengine-web.xml @@ -0,0 +1,10 @@ + + + your-app-id + ${appengine.app.version} + true + + + + + diff --git a/appengine/multitenancy/src/main/webapp/WEB-INF/logging.properties b/appengine/multitenancy/src/main/webapp/WEB-INF/logging.properties new file mode 100644 index 00000000000..a17206681f0 --- /dev/null +++ b/appengine/multitenancy/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/multitenancy/src/main/webapp/WEB-INF/web.xml b/appengine/multitenancy/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000000..3b8fd031806 --- /dev/null +++ b/appengine/multitenancy/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,49 @@ + + + + + + sign + com.example.appengine.SignGuestbookServlet + + + + sign + /sign + + + + guestbook.jsp + + + + + + ObjectifyFilter + com.googlecode.objectify.ObjectifyFilter + + + ObjectifyFilter + /* + + + com.example.appengine.OfyHelper + + + + + + + NamespaceFilter + com.example.appengine.NamespaceFilter + + + + NamespaceFilter + /sign + + + + diff --git a/appengine/multitenancy/src/main/webapp/guestbook.jsp b/appengine/multitenancy/src/main/webapp/guestbook.jsp new file mode 100644 index 00000000000..317ba765ddc --- /dev/null +++ b/appengine/multitenancy/src/main/webapp/guestbook.jsp @@ -0,0 +1,106 @@ +<%-- //[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.appengine.Greeting" %> +<%@ page import="com.example.appengine.Guestbook" %> +<%@ page import="com.googlecode.objectify.Key" %> +<%@ page import="com.googlecode.objectify.ObjectifyService" %> +<%-- //[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 + Key theBook = Key.create(Guestbook.class, 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 = ObjectifyService.ofy() + .load() + .type(Greeting.class) // We want only Greetings + .ancestor(theBook) // Anyone in this book + .order("-date") // Most recent first - date is indexed. + .limit(5) // Only show 5 of them. + .list(); + + 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/multitenancy/src/main/webapp/stylesheets/main.css b/appengine/multitenancy/src/main/webapp/stylesheets/main.css new file mode 100644 index 00000000000..05d72d5536d --- /dev/null +++ b/appengine/multitenancy/src/main/webapp/stylesheets/main.css @@ -0,0 +1,4 @@ +body { + font-family: Verdana, Helvetica, sans-serif; + background-color: #FFFFCC; +} diff --git a/appengine/multitenancy/src/test/java/com/example/appengine/GreetingTest.java b/appengine/multitenancy/src/test/java/com/example/appengine/GreetingTest.java new file mode 100644 index 00000000000..a93bfe788ab --- /dev/null +++ b/appengine/multitenancy/src/test/java/com/example/appengine/GreetingTest.java @@ -0,0 +1,84 @@ +/* + * 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.example.appengine.GuestbookTestUtilities.cleanDatastore; +import static org.junit.Assert.assertEquals; + +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.KeyFactory; +import com.google.appengine.api.datastore.PreparedQuery; +import com.google.appengine.api.datastore.Query; +import com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig; +import com.google.appengine.tools.development.testing.LocalServiceTestHelper; + +import com.googlecode.objectify.ObjectifyService; +import com.googlecode.objectify.util.Closeable; +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 { + private static final String TEST_CONTENT = "The world is Blue today"; + + 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 Closeable closeable; + private DatastoreService ds; + + @Before + public void setUp() throws Exception { + + helper.setUp(); + ds = DatastoreServiceFactory.getDatastoreService(); + + ObjectifyService.register(Guestbook.class); + ObjectifyService.register(Greeting.class); + + closeable = ObjectifyService.begin(); + + cleanDatastore(ds, "default"); + } + + @After + public void tearDown() { + cleanDatastore(ds, "default"); + helper.tearDown(); + closeable.close(); + } + + @Test + public void createSaveObject() throws Exception { + + Greeting g = new Greeting("default", TEST_CONTENT); + ObjectifyService.ofy().save().entity(g).now(); + + Query query = new Query("Greeting") + .setAncestor(new KeyFactory.Builder("Guestbook", "default").getKey()); + PreparedQuery pq = ds.prepare(query); + Entity greeting = pq.asSingleEntity(); // Should only be one at this point. + assertEquals(greeting.getProperty("content"), TEST_CONTENT); + } +} diff --git a/appengine/multitenancy/src/test/java/com/example/appengine/GuestbookTestUtilities.java b/appengine/multitenancy/src/test/java/com/example/appengine/GuestbookTestUtilities.java new file mode 100644 index 00000000000..99a40b5fe49 --- /dev/null +++ b/appengine/multitenancy/src/test/java/com/example/appengine/GuestbookTestUtilities.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.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.PreparedQuery; +import com.google.appengine.api.datastore.Query; + +import java.util.ArrayList; +import java.util.List; + +public class GuestbookTestUtilities { + + public static void cleanDatastore(DatastoreService ds, String book) { + Query query = new Query("Greeting") + .setAncestor(new KeyFactory.Builder("Guestbook", book) + .getKey()).setKeysOnly(); + PreparedQuery pq = ds.prepare(query); + List entities = pq.asList(FetchOptions.Builder.withDefaults()); + ArrayList keys = new ArrayList<>(entities.size()); + + for (Entity e : entities) { + keys.add(e.getKey()); + } + ds.delete(keys); + } + +} diff --git a/appengine/multitenancy/src/test/java/com/example/appengine/SignGuestbookServletTest.java b/appengine/multitenancy/src/test/java/com/example/appengine/SignGuestbookServletTest.java new file mode 100644 index 00000000000..e7beb16514d --- /dev/null +++ b/appengine/multitenancy/src/test/java/com/example/appengine/SignGuestbookServletTest.java @@ -0,0 +1,118 @@ +/* + * Copyright 2015 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.example.appengine.GuestbookTestUtilities.cleanDatastore; +import static org.junit.Assert.assertEquals; +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.KeyFactory; +import com.google.appengine.api.datastore.PreparedQuery; +import com.google.appengine.api.datastore.Query; +import com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig; +import com.google.appengine.tools.development.testing.LocalServiceTestHelper; + +import com.googlecode.objectify.ObjectifyService; +import com.googlecode.objectify.util.Closeable; +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 com.example.appengine.SignGuestbookServlet}. + */ +@RunWith(JUnit4.class) +public class SignGuestbookServletTest { + private static final String FAKE_URL = "fakey.org/sign"; + private static final String FAKE_NAME = "Fake"; + + 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 final String testPhrase = "Noew is the time"; + + @Mock private HttpServletRequest mockRequest; + + @Mock + private HttpServletResponse mockResponse; + + private StringWriter stringWriter; + private SignGuestbookServlet servletUnderTest; + private Closeable closeable; + private DatastoreService ds; + + @Before + public void setUp() throws Exception { + + MockitoAnnotations.initMocks(this); + helper.setUp(); + ds = DatastoreServiceFactory.getDatastoreService(); + + // Set up some fake HTTP requests + when(mockRequest.getRequestURI()).thenReturn(FAKE_URL); + when(mockRequest.getParameter("guestbookName")).thenReturn( "default" ); + when(mockRequest.getParameter("content")).thenReturn( testPhrase ); + + stringWriter = new StringWriter(); + when(mockResponse.getWriter()).thenReturn(new PrintWriter(stringWriter)); + + servletUnderTest = new SignGuestbookServlet(); + + ObjectifyService.register(Guestbook.class); + ObjectifyService.register(Greeting.class); + + closeable = ObjectifyService.begin(); + + cleanDatastore(ds, "default"); + } + + @After public void tearDown() { + cleanDatastore(ds, "default"); + helper.tearDown(); + closeable.close(); + } + + @Test + public void doPost_userNotLoggedIn() throws Exception { + servletUnderTest.doPost(mockRequest, mockResponse); + + Query query = new Query("Greeting") + .setAncestor(new KeyFactory.Builder("Guestbook", "default").getKey()); + PreparedQuery pq = ds.prepare(query); + + Entity greeting = pq.asSingleEntity(); // Should only be one at this point. + assertEquals(greeting.getProperty("content"), testPhrase); + } + +} diff --git a/pom.xml b/pom.xml index be2f3bcb449..f0800083dab 100644 --- a/pom.xml +++ b/pom.xml @@ -53,6 +53,7 @@ appengine/mailgun appengine/mailjet appengine/memcache + appengine/multitenancy appengine/oauth2 appengine/requests appengine/search