diff --git a/session-handling/pom.xml b/session-handling/pom.xml
new file mode 100644
index 00000000000..3055ffa2b97
--- /dev/null
+++ b/session-handling/pom.xml
@@ -0,0 +1,116 @@
+
+
+
+ 4.0.0
+ war
+
+ com.example.getstarted
+ session-handling
+ 1.0-SNAPSHOT
+
+
+ com.google.cloud.samples
+ shared-configuration
+ 1.0.11
+
+
+
+ MY_PROJECT
+
+ false
+ UTF-8
+ 11
+ 11
+ true
+ true
+ false
+ false
+ 9.4.21.v20190926
+
+
+
+
+
+ com.google.cloud
+ google-cloud-firestore
+ 0.52.0-beta
+
+
+
+ javax.servlet
+ javax.servlet-api
+ 4.0.0
+
+
+
+ com.google.guava
+ guava
+ 23.0
+ compile
+
+
+
+ io.opencensus
+ opencensus-contrib-http-util
+ 0.11.1
+
+
+
+
+ junit
+ junit
+ 4.12
+ test
+
+
+ org.seleniumhq.selenium
+ selenium-server
+ 3.3.1
+ test
+
+
+ org.seleniumhq.selenium
+ selenium-chrome-driver
+ 3.3.1
+ test
+
+
+
+
+ bookshelf-session-handling
+
+ ${project.build.directory}/${project.build.finalName}/WEB-INF/classes
+
+
+
+ org.eclipse.jetty
+ jetty-maven-plugin
+ ${jetty.version}
+
+
+ com.google.cloud.tools
+ jib-maven-plugin
+ 1.7.0
+
+
+ gcr.io/${gcloud.appId}/session-handling
+
+
+
+
+
+
diff --git a/session-handling/src/main/java/com/example/gettingstarted/actions/HelloWorldServlet.java b/session-handling/src/main/java/com/example/gettingstarted/actions/HelloWorldServlet.java
new file mode 100644
index 00000000000..16902e0e470
--- /dev/null
+++ b/session-handling/src/main/java/com/example/gettingstarted/actions/HelloWorldServlet.java
@@ -0,0 +1,62 @@
+/* Copyright 2019 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.gettingstarted.actions;
+
+// [START session_handling_servlet]
+
+import java.io.IOException;
+import java.util.Random;
+import java.util.logging.Logger;
+
+import javax.servlet.annotation.WebServlet;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+@WebServlet(
+ name = "helloworld",
+ urlPatterns = {"/"})
+public class HelloWorldServlet extends HttpServlet {
+ private static String[] greetings = {
+ "Hello World", "Hallo Welt", "Ciao Mondo", "Salut le Monde", "Hola Mundo",
+ };
+ private static final Logger logger = Logger.getLogger(HelloWorldServlet.class.getName());
+
+ @Override
+ public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+ if (!req.getServletPath().equals("/")) {
+ return;
+ }
+ // Get current values for the session.
+ // If any attribute doesn't exist, add it to the session.
+ Integer views = (Integer) req.getSession().getAttribute("views");
+ if (views == null) {
+ views = 0;
+ }
+ views++;
+ req.getSession().setAttribute("views", views);
+
+ String greeting = (String) req.getSession().getAttribute("greeting");
+ if (greeting == null) {
+ greeting = greetings[new Random().nextInt(greetings.length)];
+ req.getSession().setAttribute("greeting", greeting);
+ }
+
+ logger.info("Writing response " + req.toString());
+ resp.getWriter().write(String.format("%d views for %s", views, greeting));
+ }
+}
+// [END session_handling_servlet]
diff --git a/session-handling/src/main/java/com/example/gettingstarted/util/FirestoreSessionFilter.java b/session-handling/src/main/java/com/example/gettingstarted/util/FirestoreSessionFilter.java
new file mode 100644
index 00000000000..1eb95425afe
--- /dev/null
+++ b/session-handling/src/main/java/com/example/gettingstarted/util/FirestoreSessionFilter.java
@@ -0,0 +1,178 @@
+/* Copyright 2019 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.gettingstarted.util;
+
+import com.example.gettingstarted.actions.HelloWorldServlet;
+import com.google.cloud.firestore.CollectionReference;
+import com.google.cloud.firestore.DocumentSnapshot;
+import com.google.cloud.firestore.Firestore;
+import com.google.cloud.firestore.FirestoreOptions;
+import com.google.cloud.firestore.QueryDocumentSnapshot;
+import com.google.cloud.firestore.QuerySnapshot;
+import com.google.common.collect.Maps;
+import java.io.IOException;
+import java.math.BigInteger;
+import java.security.SecureRandom;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.ExecutionException;
+import java.util.logging.Logger;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.annotation.WebFilter;
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+
+@WebFilter(
+ filterName = "FirestoreSessionFilter ",
+ urlPatterns = {""})
+public class FirestoreSessionFilter implements Filter {
+ private static final SimpleDateFormat dtf = new SimpleDateFormat("yyyyMMddHHmmssSSS");
+ private static final Logger logger = Logger.getLogger(HelloWorldServlet.class.getName());
+ private static Firestore firestore;
+ private static CollectionReference sessions;
+
+ // [START sessions_handling_init]
+ @Override
+ public void init(FilterConfig config) throws ServletException {
+ // Initialize local copy of datastore session variables.
+ firestore = FirestoreOptions.getDefaultInstance().getService();
+ sessions = firestore.collection("sessions");
+
+ try {
+ // Delete all sessions unmodified for over two days.
+ Calendar cal = Calendar.getInstance();
+ cal.setTime(new Date());
+ cal.add(Calendar.HOUR, -48);
+ Date twoDaysAgo = Calendar.getInstance().getTime();
+ QuerySnapshot sessionDocs =
+ sessions.whereLessThan("lastModified", dtf.format(twoDaysAgo)).get().get();
+ for (QueryDocumentSnapshot snapshot : sessionDocs.getDocuments()) {
+ snapshot.getReference().delete();
+ }
+ } catch (InterruptedException | ExecutionException e) {
+ throw new ServletException("Exception initializing FirestoreSessionFilter.", e);
+ }
+ }
+ // [END sessions_handling_init]
+
+ // [START sessions_handling_filter]
+ @Override
+ public void doFilter(ServletRequest servletReq, ServletResponse servletResp, FilterChain chain)
+ throws IOException, ServletException {
+ HttpServletRequest req = (HttpServletRequest) servletReq;
+ HttpServletResponse resp = (HttpServletResponse) servletResp;
+
+ // For this app only call Firestore for requests to base path `/`.
+ if (!req.getServletPath().equals("/")) {
+ chain.doFilter(servletReq, servletResp);
+ return;
+ }
+
+ // Check if the session cookie is there, if not there, make a session cookie using a unique
+ // identifier.
+ String sessionId = getCookieValue(req, "bookshelfSessionId");
+ if (sessionId.equals("")) {
+ String sessionNum = new BigInteger(130, new SecureRandom()).toString(32);
+ Cookie session = new Cookie("bookshelfSessionId", sessionNum);
+ session.setPath("/");
+ resp.addCookie(session);
+ }
+
+ // session variables for request
+ Map firestoreMap = null;
+ try {
+ firestoreMap = loadSessionVariables(req);
+ } catch (ExecutionException | InterruptedException e) {
+ throw new ServletException("Exception loading session variables.", e);
+ }
+
+ for (Map.Entry entry : firestoreMap.entrySet()) {
+ servletReq.setAttribute(entry.getKey(), entry.getValue());
+ }
+
+ // Allow the servlet to process request and response
+ chain.doFilter(servletReq, servletResp);
+
+ // Create session map
+ HttpSession session = req.getSession();
+ Map sessionMap = new HashMap<>();
+ Enumeration attrNames = session.getAttributeNames();
+ while (attrNames.hasMoreElements()) {
+ String attrName = attrNames.nextElement();
+ sessionMap.put(attrName, session.getAttribute(attrName));
+ }
+
+
+ logger.info(
+ "Saving data to " + sessionId + " with views: " + session.getAttribute("views"));
+ firestore.runTransaction((ob) -> sessions.document(sessionId).set(sessionMap));
+ }
+ // [END sessions_handling_filter]
+
+ private String getCookieValue(HttpServletRequest req, String cookieName) {
+ Cookie[] cookies = req.getCookies();
+ if (cookies != null) {
+ for (Cookie cookie : cookies) {
+ if (cookie.getName().equals(cookieName)) {
+ return cookie.getValue();
+ }
+ }
+ }
+ return "";
+ }
+
+ // [START sessions_load_session_variables]
+
+ /**
+ * Take an HttpServletRequest, and copy all of the current session variables over to it
+ *
+ * @param req Request from which to extract session.
+ * @return a map of strings containing all the session variables loaded or an empty map.
+ */
+ private Map loadSessionVariables(HttpServletRequest req)
+ throws ExecutionException, InterruptedException {
+ Map datastoreMap = new HashMap<>();
+ String sessionId = getCookieValue(req, "bookshelfSessionId");
+ if (sessionId.equals("")) {
+ return datastoreMap;
+ }
+
+ return firestore
+ .runTransaction(
+ (ob) -> {
+ DocumentSnapshot session = sessions.document(sessionId).get().get();
+ Map data = session.getData();
+ if (data == null) {
+ data = Maps.newHashMap();
+ }
+ return data;
+ })
+ .get();
+ }
+ // [END sessions_load_session_variables]
+}
diff --git a/session-handling/src/main/test/java/com/example/getstarted/actions/UserJourneyTestIT.java b/session-handling/src/main/test/java/com/example/getstarted/actions/UserJourneyTestIT.java
new file mode 100644
index 00000000000..d1038b3c804
--- /dev/null
+++ b/session-handling/src/main/test/java/com/example/getstarted/actions/UserJourneyTestIT.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2019 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.getstarted.actions;
+
+import static org.junit.Assert.assertEquals;
+
+import com.google.cloud.firestore.Firestore;
+import com.google.cloud.firestore.FirestoreOptions;
+import com.google.cloud.firestore.QueryDocumentSnapshot;
+import java.util.concurrent.ExecutionException;
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.chrome.ChromeDriver;
+import org.openqa.selenium.chrome.ChromeDriverService;
+import org.openqa.selenium.remote.service.DriverService;
+
+@RunWith(JUnit4.class)
+@SuppressWarnings("checkstyle:abbreviationaswordinname")
+public class UserJourneyTestIT {
+ private static DriverService service;
+ private WebDriver driver;
+
+ @BeforeClass
+ public static void setupClass() throws Exception {
+ service = ChromeDriverService.createDefaultService();
+ service.start();
+ }
+
+ @AfterClass
+ public static void tearDownClass() throws ExecutionException, InterruptedException {
+ // Clear the firestore sessions data.
+ Firestore firestore = FirestoreOptions.getDefaultInstance().getService();
+ for (QueryDocumentSnapshot docSnapshot :
+ firestore.collection("books").get().get().getDocuments()) {
+ docSnapshot.getReference().delete().get();
+ }
+
+ service.stop();
+ }
+
+ @Before
+ public void setup() {
+ driver = new ChromeDriver();
+ }
+
+ @After
+ public void tearDown() {
+ driver.quit();
+ }
+
+ @Test
+ public void userJourney() {
+ // Do selenium tests on the deployed version, if applicable
+ String endpoint = "http://localhost:8080";
+ System.out.println("Testing endpoint: " + endpoint);
+ driver.get(endpoint);
+
+ WebElement body = driver.findElement(By.cssSelector("body"));
+
+ String responseText = body.getText();
+ // Reload the page to ensure the session data returns the same text except with 1 more view.
+ driver.get(endpoint);
+ body = driver.findElement(By.cssSelector("body"));
+ String response2Text = body.getText();
+ assertEquals(responseText, response2Text);
+ }
+}