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); + } +}