From 507f3769eb39df9e24da3155579c887dd0f253b2 Mon Sep 17 00:00:00 2001 From: Kurtis Van Gent Date: Tue, 25 Sep 2018 13:48:54 -0700 Subject: [PATCH 1/4] Added MySQL Servlet connectivity sample. Fix checkstyle violations. --- cloud-sql/mysql/servlet/README.md | 61 ++++++++ cloud-sql/mysql/servlet/pom.xml | 89 +++++++++++ .../ConnectionPoolContextListener.java | 138 +++++++++++++++++ .../com/example/cloudsql/IndexServlet.java | 139 ++++++++++++++++++ .../main/java/com/example/cloudsql/Vote.java | 47 ++++++ .../src/main/webapp/WEB-INF/appengine-web.xml | 26 ++++ .../mysql/servlet/src/main/webapp/index.jsp | 117 +++++++++++++++ pom.xml | 5 +- 8 files changed, 621 insertions(+), 1 deletion(-) create mode 100644 cloud-sql/mysql/servlet/README.md create mode 100644 cloud-sql/mysql/servlet/pom.xml create mode 100644 cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/ConnectionPoolContextListener.java create mode 100644 cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/IndexServlet.java create mode 100644 cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/Vote.java create mode 100644 cloud-sql/mysql/servlet/src/main/webapp/WEB-INF/appengine-web.xml create mode 100644 cloud-sql/mysql/servlet/src/main/webapp/index.jsp diff --git a/cloud-sql/mysql/servlet/README.md b/cloud-sql/mysql/servlet/README.md new file mode 100644 index 00000000000..cd2e22fbbcc --- /dev/null +++ b/cloud-sql/mysql/servlet/README.md @@ -0,0 +1,61 @@ +# Connecting to Cloud SQL - MySQL + +## Before you begin + +1. If you haven't already, set up a Java Development Environment (including google-cloud-sdk and +maven utilities) by following the [java setup guide](https://cloud.google.com/java/docs/setup). + +1. Create a 2nd Gen Cloud SQL Instance by following these +[instructions](https://cloud.google.com/sql/docs/mysql/create-instance). Note the connection string, +database user, and database password that you create. + +1. Create a database for your application by following these +[instructions](https://cloud.google.com/sql/docs/mysql/create-manage-databases). Note the database +name. + +1. Create a service account with the 'Cloud SQL Client' permissions by following these +[instructions](https://cloud.google.com/sql/docs/mysql/connect-external-app#4_if_required_by_your_authentication_method_create_a_service_account). +Download a JSON key to use to authenticate your connection. + +1. Use the information noted in the previous steps: +```bash +export GOOGLE_APPLICATION_CREDENTIALS=/path/to/service/account/key.json +export CLOUD_SQL_CONNECTION_NAME='::' +export DB_USER='my-db-user' +export DB_PASS='my-db-pass' +export DB_NAME='my_db' +``` + +## Deploying locally + +To run this application locally, run the following command inside the project folder: + +```bash +mvn jetty:run +``` + +Navigate towards `http://127.0.0.1:8080` to verify your application is running correctly. + +## Google AppEngine-Standard + +To run on GAE-Standard, create an AppEngine project by following the setup for these +[instructions](https://cloud.google.com/appengine/docs/standard/java/quickstart#before-you-begin) +and verify that `appengine-api-1.0-sdk` is listed as a dependency in the pom.xml. + + +### Development Server + +The following command will run the application locally in the the GAE-development server: +```bash +mvn appengine:run +``` + +### Deploy to Google Cloud + +First, update `src/main/webapp/WEB-INF/appengine-web.xml` with the correct values to pass the +environment variables into the runtime. + +Next, the following command will deploy the application to your Google Cloud project: +```bash +mvn appengine:deploy +``` diff --git a/cloud-sql/mysql/servlet/pom.xml b/cloud-sql/mysql/servlet/pom.xml new file mode 100644 index 00000000000..6b2476b2edb --- /dev/null +++ b/cloud-sql/mysql/servlet/pom.xml @@ -0,0 +1,89 @@ + + + 4.0.0 + war + 1.0-SNAPSHOT + com.example.cloudsql + tabs-vs-spaces + + + + com.google.cloud.samples + shared-configuration + 1.0.10 + + + + 1.8 + 1.8 + false + + + + + + + com.google.appengine + appengine-api-1.0-sdk + 1.9.64 + + + + javax.servlet + javax.servlet-api + 3.1.0 + jar + provided + + + mysql + mysql-connector-java + 8.0.11 + + + com.google.cloud.sql + mysql-socket-factory-connector-j-8 + 1.0.11 + + + com.zaxxer + HikariCP + 3.1.0 + + + + + + + org.eclipse.jetty + jetty-maven-plugin + 9.4.10.v20180503 + + 1 + + + + com.google.cloud.tools + appengine-maven-plugin + 1.3.2 + + + + diff --git a/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/ConnectionPoolContextListener.java b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/ConnectionPoolContextListener.java new file mode 100644 index 00000000000..4835c4c5a50 --- /dev/null +++ b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/ConnectionPoolContextListener.java @@ -0,0 +1,138 @@ +/* + * Copyright 2018 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.cloudsql; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import javax.servlet.ServletContextEvent; +import javax.servlet.ServletContextListener; +import javax.servlet.annotation.WebListener; +import javax.sql.DataSource; + +@WebListener +public class ConnectionPoolContextListener implements ServletContextListener { + + private static final String CLOUD_SQL_INSTANCE_NAME = System.getenv("CLOUD_SQL_INSTANCE_NAME"); + private static final String DB_USER = System.getenv("DB_USER"); + private static final String DB_PASS = System.getenv("DB_PASS"); + private static final String DB_NAME = System.getenv("DB_NAME"); + + private DataSource mysqlConnectionPool() { + // [START cloud_sql_mysql_connection_pool] + // The configuration object specifies behaviors for the connection pool. + HikariConfig config = new HikariConfig(); + + // Configure which instance and what database user to connect with. + config.setJdbcUrl(String.format("jdbc:mysql:///%s", DB_NAME)); + config.setUsername(DB_USER); // e.g. "root", "postgres" + config.setPassword(DB_PASS); // e.g. "my-password" + + // For Java users, the Cloud SQL JDBC Socket Factory can provide authenticated connections. + // See https://github.com/GoogleCloudPlatform/cloud-sql-jdbc-socket-factory for details. + config.addDataSourceProperty("socketFactory", "com.google.cloud.sql.mysql.SocketFactory"); + config.addDataSourceProperty("cloudSqlInstance", CLOUD_SQL_INSTANCE_NAME); + config.addDataSourceProperty("useSSL", "false"); + + // ... Specify additional connection properties here. + + // [START_EXCLUDE] + + // [START cloud_sql_max_connections] + // maximumPoolSize limits the total number of concurrent connections this pool will keep. Ideal + // values for this setting are highly variable on app design, infrastructure, and database. + config.setMaximumPoolSize(5); + // [END cloud_sql_max_connections] + + // [START cloud_sql_connection_timeout] + // setConnectionTimeout is the maximum number of milliseconds to wait for a connection checkout. + // Any attempt to retrieve a connection from this pool that exceeds the set limit will throw an + // SQLException. + config.setConnectionTimeout(10000); // 10 seconds + // [END cloud_sql_connection_timeout] + + // [START cloud_sql_connection_backoff] + // Hikari automatically delays between failed connection attempts, eventually reaching a + // maximum delay of `connectionTimeout / 2` between attempts. + // [END cloud_sql_connection_backoff] + + // [START cloud_sql_connection_lifetime] + // maxLifetime is the maximum possible lifetime of a connection in the pool. Connections that + // live longer than this many milliseconds will be closed and reestablished between uses. + config.setMaxLifetime(1800000); // 30 minutes + // [END cloud_sql_connection_lifetime] + + // [START cloud_sql_idle_connections] + // minimumIdle is the minimum number of idle connections Hikari maintains in the pool. + // Additional connections will be established to meet this value unless the pool is full. + config.setMinimumIdle(5); + // idleTimeout is the maximum amount of time a connection can sit in the pool. Connections that + // sit idle for this many milliseconds are retried if minimumIdle is exceeded. + config.setIdleTimeout(600000); // 10 minutes + // [END cloud_sql_idle_connections] + // [END_EXCLUDE] + + // Initialize the connection pool using the configuration object. + DataSource pool = new HikariDataSource(config); + // [END cloud_sql_mysql_connection_pool] + return pool; + } + + private void createTableSchema(DataSource pool) { + // Safely attempt to create the table schema. + try (Connection conn = pool.getConnection()) { + PreparedStatement createTableStatement = conn.prepareStatement( + "CREATE TABLE IF NOT EXISTS votes ( " + + "vote_id SERIAL NOT NULL, time_cast timestamp NOT NULL, canidate CHAR(6) NOT NULL, " + + "PRIMARY KEY (vote_id) );" + ); + createTableStatement.execute(); + } catch (SQLException e) { + throw new Error( + "Unable to successfully verify table schema. Please double check the steps in the README" + + " and restart the application. \n" + e.toString()); + } + } + + @Override + public void contextDestroyed(ServletContextEvent event) { + // This function is called when the Servlet is destroyed. + DataSource pool = (DataSource) event.getServletContext().getAttribute("my-pool"); + if (pool != null) { + try { + pool.unwrap(HikariDataSource.class).close(); + } catch (SQLException e) { + // Handle exception + System.out.println("Any error occurred while the application was shutting down: " + e); + } + } + } + + @Override + public void contextInitialized(ServletContextEvent event) { + // This function is called when the application starts and will safely create a connection pool + // that can be used to connect to. + DataSource pool = (DataSource) event.getServletContext().getAttribute("my-pool"); + if (pool == null) { + pool = mysqlConnectionPool(); + event.getServletContext().setAttribute("my-pool", pool); + } + createTableSchema(pool); + } +} diff --git a/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/IndexServlet.java b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/IndexServlet.java new file mode 100644 index 00000000000..e677cd5ce4a --- /dev/null +++ b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/IndexServlet.java @@ -0,0 +1,139 @@ +/* + * Copyright 2018 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.cloudsql; + +import java.io.IOException; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.sql.DataSource; + +@WebServlet(name = "Index", value = "") +public class IndexServlet extends HttpServlet { + + @Override + public void doGet(HttpServletRequest req, HttpServletResponse resp) + throws IOException, ServletException { + // Extract the pool from the Servlet Context, reusing the one that was created + // in the ContextListener when the application was started + DataSource pool = (DataSource) req.getServletContext().getAttribute("my-pool"); + + int tabCt; + int voteCt; + List recentVotes = new ArrayList<>(); + try (Connection conn = pool.getConnection()) { + // PreparedStatements are compiled by the database immediately and executed at a later date. + // Most databases cache previously compiled queries, which improves efficiency. + PreparedStatement voteStmt = conn.prepareStatement( + "SELECT canidate, time_cast FROM votes ORDER BY time_cast DESC LIMIT 5"); + // Execute the statement + ResultSet voteResults = voteStmt.executeQuery(); + // Convert a ResultSet into Vote objects + while (voteResults.next()) { + String candidate = voteResults.getString(1); + Timestamp timeCast = voteResults.getTimestamp(2); + recentVotes.add(new Vote(candidate, timeCast)); + } + + // PreparedStatements can also be executed multiple times with different arguments. This can + // improve efficiency, and project a query from being vulnerable to an SQL injection. + PreparedStatement voteCtStmt = conn.prepareStatement( + "SELECT COUNT(vote_id) FROM votes WHERE canidate=?"); + + voteCtStmt.setString(1, "tabs"); + ResultSet tabResult = voteCtStmt.executeQuery(); + tabResult.next(); // Move to the first result + tabCt = tabResult.getInt(1); + + voteCtStmt.setString(1, "spaces"); + ResultSet spacesResult = voteCtStmt.executeQuery(); + spacesResult.next(); // Move to the first result + voteCt = spacesResult.getInt(1); + + } catch (SQLException e) { + // If something goes wrong, the application needs to react appropriately. This might mean + // getting a new connection and executing the query again, or it might mean redirecting the + // user to a different page to let them know something went wrong. + throw new ServletException("Unable to successfully connect to the database. Please check the " + + "steps in the README and try again."); + } + // [END cloud_sql_example_query] + + // Add variables and render the page + req.setAttribute("tabVoteCt", tabCt); + req.setAttribute("spaceVoteCt", voteCt); + req.setAttribute("recentVotes", recentVotes); + req.getRequestDispatcher("/index.jsp").forward(req, resp); + } + + @Override + public void doPost(HttpServletRequest req, HttpServletResponse resp) + throws IOException { + // Get the team from the request and record the time of the vote. + String team = req.getParameter("team"); + if (team != null) { + team = team.toLowerCase(); + } + Timestamp now = new Timestamp(new Date().getTime()); + if (team == null || !team.equals("tabs") && !team.equals("spaces")) { + resp.setStatus(400); + resp.getWriter().append("Invalid team specified."); + return; + } + + // Reuse the pool that was created in the ContextListener when the Servlet started. + DataSource pool = (DataSource) req.getServletContext().getAttribute("my-pool"); + // [START cloud_sql_example_statement] + // Using a try-with-resources statement ensures that the connection is always released back + // into the pool at the end of the statement (even if an error occurs) + try (Connection conn = pool.getConnection()) { + + // PreparedStatements can be more efficient and project against injections. + PreparedStatement voteStmt = conn.prepareStatement( + "INSERT INTO votes (time_cast, canidate) VALUES (?, ?);"); + voteStmt.setTimestamp(1, now); + voteStmt.setString(2, team); + + // Finally, execute the statement. If it fails, an error will be thrown. + voteStmt.execute(); + + } catch (SQLException e) { + // If something goes wrong, handle the error in this section. This might involve retrying or + // adjusting parameters depending on the situation. + // [START_EXCLUDE] + System.out.println("An SQL error occurred during executions: \n" + e.toString()); + resp.getWriter().write("Unable to successfully cast vote! Please contact the application" + + "owner for more details."); + // [END_EXCLUDE] + } + // [END cloud_sql_example_statement] + + + resp.getWriter().printf("Vote successfully cast for '%s' at time %s!\n", team, now); + } + +} diff --git a/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/Vote.java b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/Vote.java new file mode 100644 index 00000000000..2ba1c7ce03f --- /dev/null +++ b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/Vote.java @@ -0,0 +1,47 @@ +/* + * Copyright 2018 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.cloudsql; + +import java.sql.Timestamp; + +public class Vote { + + private String candiate; + private Timestamp timeCast; + + public Vote(String candiate, Timestamp timeCast) { + this.candiate = candiate; + this.timeCast = timeCast; + } + + public String getCandiate() { + return candiate; + } + + public void setCandiate(String candiate) { + this.candiate = candiate; + } + + public Timestamp getTimeCast() { + return timeCast; + } + + public void setTimeCast(Timestamp timeCast) { + this.timeCast = timeCast; + } + +} diff --git a/cloud-sql/mysql/servlet/src/main/webapp/WEB-INF/appengine-web.xml b/cloud-sql/mysql/servlet/src/main/webapp/WEB-INF/appengine-web.xml new file mode 100644 index 00000000000..a5121bd132b --- /dev/null +++ b/cloud-sql/mysql/servlet/src/main/webapp/WEB-INF/appengine-web.xml @@ -0,0 +1,26 @@ + + + + true + java8 + + + + + + + \ No newline at end of file diff --git a/cloud-sql/mysql/servlet/src/main/webapp/index.jsp b/cloud-sql/mysql/servlet/src/main/webapp/index.jsp new file mode 100644 index 00000000000..0d37aa183af --- /dev/null +++ b/cloud-sql/mysql/servlet/src/main/webapp/index.jsp @@ -0,0 +1,117 @@ +<%@ page import="java.util.List" %> +<%@ page import="com.example.cloudsql.Vote" %> + +<%@ page contentType="text/html;charset=UTF-8" language="java" %> + + + Tabs VS Spaces + + + + + + + +<% + Integer tabVoteCt = (Integer) request.getAttribute("tabVoteCt"); + Integer spaceVoteCt = (Integer) request.getAttribute("spaceVoteCt"); + List recentVotes = (List) request.getAttribute("recentVotes"); + int voteDiff = 0; + String leadTeam = ""; + if (!tabVoteCt.equals(spaceVoteCt)) { + if (tabVoteCt > spaceVoteCt) { + leadTeam = "TABS"; + voteDiff = tabVoteCt - spaceVoteCt; + } else { + leadTeam = "SPACES"; + voteDiff = spaceVoteCt - tabVoteCt; + } + } +%> +
+
+

+ <% if(voteDiff != 0) { %> + <%= leadTeam %> are winning by <%= voteDiff %> <%= voteDiff > 1 ? "votes" : "vote" %>. + <% } else { %> + TABS and SPACES are tied! + <% } %> +

+
+
+
+
"> + keyboard_tab +

<%= tabVoteCt %> votes

+ +
+
+
+
"> + space_bar +

<%= spaceVoteCt %> votes

+ +
+
+
+

Recent Votes

+
    + <% for(Vote v : recentVotes) { %> +
  • + <% if(v.getCandiate().equals("tabs")) { %> + keyboard_tab + <% } else { %> + space_bar + <% } %> + A vote for <%= v.getCandiate().toUpperCase() %> +

    was cast at <%= v.getTimeCast() %>.

    +
  • + <% } %> +
+
+ +
+ +
+ diff --git a/pom.xml b/pom.xml index cf263b57a82..a17ca332614 100644 --- a/pom.xml +++ b/pom.xml @@ -1,5 +1,5 @@ - - com.google.appengine - appengine-api-1.0-sdk - 1.9.64 - - javax.servlet javax.servlet-api @@ -79,6 +71,7 @@ 1 + com.google.cloud.tools appengine-maven-plugin diff --git a/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/ConnectionPoolContextListener.java b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/ConnectionPoolContextListener.java index 4835c4c5a50..40b88735d49 100644 --- a/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/ConnectionPoolContextListener.java +++ b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/ConnectionPoolContextListener.java @@ -26,7 +26,7 @@ import javax.servlet.annotation.WebListener; import javax.sql.DataSource; -@WebListener +@WebListener("Creates a connection pool that is stored in the Servlet's context for later use.") public class ConnectionPoolContextListener implements ServletContextListener { private static final String CLOUD_SQL_INSTANCE_NAME = System.getenv("CLOUD_SQL_INSTANCE_NAME"); @@ -74,7 +74,9 @@ private DataSource mysqlConnectionPool() { // [START cloud_sql_connection_lifetime] // maxLifetime is the maximum possible lifetime of a connection in the pool. Connections that - // live longer than this many milliseconds will be closed and reestablished between uses. + // live longer than this many milliseconds will be closed and reestablished between uses. This + // value should be several minutes shorter than the database's timeout value to avoid unexpected + // terminations. config.setMaxLifetime(1800000); // 30 minutes // [END cloud_sql_connection_lifetime] diff --git a/cloud-sql/mysql/servlet/src/main/webapp/WEB-INF/appengine-web.xml b/cloud-sql/mysql/servlet/src/main/webapp/WEB-INF/appengine-web.xml index a5121bd132b..5ce3ec0199f 100644 --- a/cloud-sql/mysql/servlet/src/main/webapp/WEB-INF/appengine-web.xml +++ b/cloud-sql/mysql/servlet/src/main/webapp/WEB-INF/appengine-web.xml @@ -23,4 +23,4 @@ - \ No newline at end of file + From 2437011680bf36b1e1fef9bc5c378fbf4fb16d2d Mon Sep 17 00:00:00 2001 From: Kurtis Van Gent Date: Tue, 16 Oct 2018 14:55:42 -0700 Subject: [PATCH 3/4] Address rest of lesv's feedback. --- cloud-sql/mysql/servlet/README.md | 9 +++++++-- .../example/cloudsql/ConnectionPoolContextListener.java | 2 ++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/cloud-sql/mysql/servlet/README.md b/cloud-sql/mysql/servlet/README.md index cd2e22fbbcc..e49b8c5481a 100644 --- a/cloud-sql/mysql/servlet/README.md +++ b/cloud-sql/mysql/servlet/README.md @@ -3,7 +3,8 @@ ## Before you begin 1. If you haven't already, set up a Java Development Environment (including google-cloud-sdk and -maven utilities) by following the [java setup guide](https://cloud.google.com/java/docs/setup). +maven utilities) by following the [java setup guide](https://cloud.google.com/java/docs/setup) and +[create a project](https://cloud.google.com/resource-manager/docs/creating-managing-projects#creating_a_project). 1. Create a 2nd Gen Cloud SQL Instance by following these [instructions](https://cloud.google.com/sql/docs/mysql/create-instance). Note the connection string, @@ -25,6 +26,8 @@ export DB_USER='my-db-user' export DB_PASS='my-db-pass' export DB_NAME='my_db' ``` +Note: Saving credentials in environment variables is convenient, but not secure - consider a more +secure solution such as [Cloud KMS](https://cloud.google.com/kms/) to help keep secrets safe. ## Deploying locally @@ -40,7 +43,9 @@ Navigate towards `http://127.0.0.1:8080` to verify your application is running c To run on GAE-Standard, create an AppEngine project by following the setup for these [instructions](https://cloud.google.com/appengine/docs/standard/java/quickstart#before-you-begin) -and verify that `appengine-api-1.0-sdk` is listed as a dependency in the pom.xml. +and verify that +[appengine-maven-plugin](https://cloud.google.com/java/docs/setup#optional_install_maven_or_gradle_plugin_for_app_engine) + has been added in your build section as a plugin. ### Development Server diff --git a/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/ConnectionPoolContextListener.java b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/ConnectionPoolContextListener.java index 40b88735d49..0dfd2ac56c2 100644 --- a/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/ConnectionPoolContextListener.java +++ b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/ConnectionPoolContextListener.java @@ -29,6 +29,8 @@ @WebListener("Creates a connection pool that is stored in the Servlet's context for later use.") public class ConnectionPoolContextListener implements ServletContextListener { + // Saving credentials in environment variables is convenient, but not secure - consider a more + // secure solution such as https://cloud.google.com/kms/ to help keep secrets safe. private static final String CLOUD_SQL_INSTANCE_NAME = System.getenv("CLOUD_SQL_INSTANCE_NAME"); private static final String DB_USER = System.getenv("DB_USER"); private static final String DB_PASS = System.getenv("DB_PASS"); From 878479046ab36c4be1686baa5b6e719c0741240e Mon Sep 17 00:00:00 2001 From: Kurtis Van Gent Date: Fri, 2 Nov 2018 19:20:07 -0700 Subject: [PATCH 4/4] Address additional feedback. --- cloud-sql/mysql/servlet/README.md | 2 +- cloud-sql/mysql/servlet/pom.xml | 5 + .../ConnectionPoolContextListener.java | 51 ++++---- .../com/example/cloudsql/IndexServlet.java | 53 ++++---- .../main/java/com/example/cloudsql/Vote.java | 14 +- .../mysql/servlet/src/main/webapp/index.jsp | 120 +++++++++--------- 6 files changed, 125 insertions(+), 120 deletions(-) diff --git a/cloud-sql/mysql/servlet/README.md b/cloud-sql/mysql/servlet/README.md index e49b8c5481a..c5c23cca993 100644 --- a/cloud-sql/mysql/servlet/README.md +++ b/cloud-sql/mysql/servlet/README.md @@ -39,7 +39,7 @@ mvn jetty:run Navigate towards `http://127.0.0.1:8080` to verify your application is running correctly. -## Google AppEngine-Standard +## Google App Engine Standard To run on GAE-Standard, create an AppEngine project by following the setup for these [instructions](https://cloud.google.com/appengine/docs/standard/java/quickstart#before-you-begin) diff --git a/cloud-sql/mysql/servlet/pom.xml b/cloud-sql/mysql/servlet/pom.xml index 3002831901a..32ecccbd230 100644 --- a/cloud-sql/mysql/servlet/pom.xml +++ b/cloud-sql/mysql/servlet/pom.xml @@ -44,6 +44,11 @@ jar provided + + javax.servlet + jstl + 1.2 + mysql mysql-connector-java diff --git a/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/ConnectionPoolContextListener.java b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/ConnectionPoolContextListener.java index 0dfd2ac56c2..b49849125c3 100644 --- a/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/ConnectionPoolContextListener.java +++ b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/ConnectionPoolContextListener.java @@ -21,6 +21,7 @@ import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; +import java.util.logging.Logger; import javax.servlet.ServletContextEvent; import javax.servlet.ServletContextListener; import javax.servlet.annotation.WebListener; @@ -29,6 +30,8 @@ @WebListener("Creates a connection pool that is stored in the Servlet's context for later use.") public class ConnectionPoolContextListener implements ServletContextListener { + private static final Logger LOGGER = Logger.getLogger(IndexServlet.class.getName()); + // Saving credentials in environment variables is convenient, but not secure - consider a more // secure solution such as https://cloud.google.com/kms/ to help keep secrets safe. private static final String CLOUD_SQL_INSTANCE_NAME = System.getenv("CLOUD_SQL_INSTANCE_NAME"); @@ -36,7 +39,7 @@ public class ConnectionPoolContextListener implements ServletContextListener { private static final String DB_PASS = System.getenv("DB_PASS"); private static final String DB_NAME = System.getenv("DB_NAME"); - private DataSource mysqlConnectionPool() { + private DataSource createConnectionPool() { // [START cloud_sql_mysql_connection_pool] // The configuration object specifies behaviors for the connection pool. HikariConfig config = new HikariConfig(); @@ -56,17 +59,23 @@ private DataSource mysqlConnectionPool() { // [START_EXCLUDE] - // [START cloud_sql_max_connections] + // [START cloud_sql_limit_connections] // maximumPoolSize limits the total number of concurrent connections this pool will keep. Ideal // values for this setting are highly variable on app design, infrastructure, and database. config.setMaximumPoolSize(5); - // [END cloud_sql_max_connections] + // minimumIdle is the minimum number of idle connections Hikari maintains in the pool. + // Additional connections will be established to meet this value unless the pool is full. + config.setMinimumIdle(5); + // [END cloud_sql_limit_connections] // [START cloud_sql_connection_timeout] // setConnectionTimeout is the maximum number of milliseconds to wait for a connection checkout. // Any attempt to retrieve a connection from this pool that exceeds the set limit will throw an // SQLException. config.setConnectionTimeout(10000); // 10 seconds + // idleTimeout is the maximum amount of time a connection can sit in the pool. Connections that + // sit idle for this many milliseconds are retried if minimumIdle is exceeded. + config.setIdleTimeout(600000); // 10 minutes // [END cloud_sql_connection_timeout] // [START cloud_sql_connection_backoff] @@ -82,14 +91,6 @@ private DataSource mysqlConnectionPool() { config.setMaxLifetime(1800000); // 30 minutes // [END cloud_sql_connection_lifetime] - // [START cloud_sql_idle_connections] - // minimumIdle is the minimum number of idle connections Hikari maintains in the pool. - // Additional connections will be established to meet this value unless the pool is full. - config.setMinimumIdle(5); - // idleTimeout is the maximum amount of time a connection can sit in the pool. Connections that - // sit idle for this many milliseconds are retried if minimumIdle is exceeded. - config.setIdleTimeout(600000); // 10 minutes - // [END cloud_sql_idle_connections] // [END_EXCLUDE] // Initialize the connection pool using the configuration object. @@ -98,33 +99,24 @@ private DataSource mysqlConnectionPool() { return pool; } - private void createTableSchema(DataSource pool) { + private void createTable(DataSource pool) throws SQLException { // Safely attempt to create the table schema. try (Connection conn = pool.getConnection()) { PreparedStatement createTableStatement = conn.prepareStatement( "CREATE TABLE IF NOT EXISTS votes ( " - + "vote_id SERIAL NOT NULL, time_cast timestamp NOT NULL, canidate CHAR(6) NOT NULL, " - + "PRIMARY KEY (vote_id) );" + + "vote_id SERIAL NOT NULL, time_cast timestamp NOT NULL, candidate CHAR(6) NOT NULL," + + " PRIMARY KEY (vote_id) );" ); createTableStatement.execute(); - } catch (SQLException e) { - throw new Error( - "Unable to successfully verify table schema. Please double check the steps in the README" - + " and restart the application. \n" + e.toString()); } } @Override public void contextDestroyed(ServletContextEvent event) { // This function is called when the Servlet is destroyed. - DataSource pool = (DataSource) event.getServletContext().getAttribute("my-pool"); + HikariDataSource pool = (HikariDataSource) event.getServletContext().getAttribute("my-pool"); if (pool != null) { - try { - pool.unwrap(HikariDataSource.class).close(); - } catch (SQLException e) { - // Handle exception - System.out.println("Any error occurred while the application was shutting down: " + e); - } + pool.close(); } } @@ -134,9 +126,14 @@ public void contextInitialized(ServletContextEvent event) { // that can be used to connect to. DataSource pool = (DataSource) event.getServletContext().getAttribute("my-pool"); if (pool == null) { - pool = mysqlConnectionPool(); + pool = createConnectionPool(); event.getServletContext().setAttribute("my-pool", pool); } - createTableSchema(pool); + try { + createTable(pool); + } catch (SQLException ex) { + throw new RuntimeException("Unable to verify table schema. Please double check the steps" + + "in the README and try again.", ex); + } } } diff --git a/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/IndexServlet.java b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/IndexServlet.java index e677cd5ce4a..a235bdc6d45 100644 --- a/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/IndexServlet.java +++ b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/IndexServlet.java @@ -25,6 +25,8 @@ import java.util.ArrayList; import java.util.Date; import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; @@ -35,6 +37,8 @@ @WebServlet(name = "Index", value = "") public class IndexServlet extends HttpServlet { + private static final Logger LOGGER = Logger.getLogger(IndexServlet.class.getName()); + @Override public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { @@ -42,14 +46,14 @@ public void doGet(HttpServletRequest req, HttpServletResponse resp) // in the ContextListener when the application was started DataSource pool = (DataSource) req.getServletContext().getAttribute("my-pool"); - int tabCt; - int voteCt; + int tabCount; + int spaceCount; List recentVotes = new ArrayList<>(); try (Connection conn = pool.getConnection()) { // PreparedStatements are compiled by the database immediately and executed at a later date. // Most databases cache previously compiled queries, which improves efficiency. PreparedStatement voteStmt = conn.prepareStatement( - "SELECT canidate, time_cast FROM votes ORDER BY time_cast DESC LIMIT 5"); + "SELECT candidate, time_cast FROM votes ORDER BY time_cast DESC LIMIT 5"); // Execute the statement ResultSet voteResults = voteStmt.executeQuery(); // Convert a ResultSet into Vote objects @@ -61,31 +65,31 @@ public void doGet(HttpServletRequest req, HttpServletResponse resp) // PreparedStatements can also be executed multiple times with different arguments. This can // improve efficiency, and project a query from being vulnerable to an SQL injection. - PreparedStatement voteCtStmt = conn.prepareStatement( - "SELECT COUNT(vote_id) FROM votes WHERE canidate=?"); + PreparedStatement voteCountStmt = conn.prepareStatement( + "SELECT COUNT(vote_id) FROM votes WHERE candidate=?"); - voteCtStmt.setString(1, "tabs"); - ResultSet tabResult = voteCtStmt.executeQuery(); + voteCountStmt.setString(1, "TABS"); + ResultSet tabResult = voteCountStmt.executeQuery(); tabResult.next(); // Move to the first result - tabCt = tabResult.getInt(1); + tabCount = tabResult.getInt(1); - voteCtStmt.setString(1, "spaces"); - ResultSet spacesResult = voteCtStmt.executeQuery(); - spacesResult.next(); // Move to the first result - voteCt = spacesResult.getInt(1); + voteCountStmt.setString(1, "SPACES"); + ResultSet spaceResult = voteCountStmt.executeQuery(); + spaceResult.next(); // Move to the first result + spaceCount = spaceResult.getInt(1); - } catch (SQLException e) { + } catch (SQLException ex) { // If something goes wrong, the application needs to react appropriately. This might mean // getting a new connection and executing the query again, or it might mean redirecting the // user to a different page to let them know something went wrong. throw new ServletException("Unable to successfully connect to the database. Please check the " - + "steps in the README and try again."); + + "steps in the README and try again.", ex); } // [END cloud_sql_example_query] // Add variables and render the page - req.setAttribute("tabVoteCt", tabCt); - req.setAttribute("spaceVoteCt", voteCt); + req.setAttribute("tabCount", tabCount); + req.setAttribute("spaceCount", spaceCount); req.setAttribute("recentVotes", recentVotes); req.getRequestDispatcher("/index.jsp").forward(req, resp); } @@ -96,10 +100,10 @@ public void doPost(HttpServletRequest req, HttpServletResponse resp) // Get the team from the request and record the time of the vote. String team = req.getParameter("team"); if (team != null) { - team = team.toLowerCase(); + team = team.toUpperCase(); } Timestamp now = new Timestamp(new Date().getTime()); - if (team == null || !team.equals("tabs") && !team.equals("spaces")) { + if (team == null || (!team.equals("TABS") && !team.equals("SPACES"))) { resp.setStatus(400); resp.getWriter().append("Invalid team specified."); return; @@ -114,25 +118,26 @@ public void doPost(HttpServletRequest req, HttpServletResponse resp) // PreparedStatements can be more efficient and project against injections. PreparedStatement voteStmt = conn.prepareStatement( - "INSERT INTO votes (time_cast, canidate) VALUES (?, ?);"); + "INSERT INTO votes (time_cast, candidate) VALUES (?, ?);"); voteStmt.setTimestamp(1, now); voteStmt.setString(2, team); // Finally, execute the statement. If it fails, an error will be thrown. voteStmt.execute(); - } catch (SQLException e) { + } catch (SQLException ex) { // If something goes wrong, handle the error in this section. This might involve retrying or // adjusting parameters depending on the situation. // [START_EXCLUDE] - System.out.println("An SQL error occurred during executions: \n" + e.toString()); - resp.getWriter().write("Unable to successfully cast vote! Please contact the application" - + "owner for more details."); + LOGGER.log(Level.WARNING, "Error while attempting to submit vote.", ex); + resp.setStatus(500); + resp.getWriter().write("Unable to successfully cast vote! Please check the application " + + "logs for more details."); // [END_EXCLUDE] } // [END cloud_sql_example_statement] - + resp.setStatus(200); resp.getWriter().printf("Vote successfully cast for '%s' at time %s!\n", team, now); } diff --git a/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/Vote.java b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/Vote.java index 2ba1c7ce03f..455c99175ea 100644 --- a/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/Vote.java +++ b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/Vote.java @@ -20,20 +20,20 @@ public class Vote { - private String candiate; + private String candidate; private Timestamp timeCast; - public Vote(String candiate, Timestamp timeCast) { - this.candiate = candiate; + public Vote(String candidate, Timestamp timeCast) { + this.candidate = candidate.toUpperCase(); this.timeCast = timeCast; } - public String getCandiate() { - return candiate; + public String getCandidate() { + return candidate; } - public void setCandiate(String candiate) { - this.candiate = candiate; + public void setCandidate(String candidate) { + this.candidate = candidate.toUpperCase(); } public Timestamp getTimeCast() { diff --git a/cloud-sql/mysql/servlet/src/main/webapp/index.jsp b/cloud-sql/mysql/servlet/src/main/webapp/index.jsp index 0d37aa183af..d0f61b96ed2 100644 --- a/cloud-sql/mysql/servlet/src/main/webapp/index.jsp +++ b/cloud-sql/mysql/servlet/src/main/webapp/index.jsp @@ -1,5 +1,3 @@ -<%@ page import="java.util.List" %> -<%@ page import="com.example.cloudsql.Vote" %> <%@ page contentType="text/html;charset=UTF-8" language="java" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> Tabs VS Spaces @@ -30,88 +29,87 @@ limitations under the License. - -<% - Integer tabVoteCt = (Integer) request.getAttribute("tabVoteCt"); - Integer spaceVoteCt = (Integer) request.getAttribute("spaceVoteCt"); - List recentVotes = (List) request.getAttribute("recentVotes"); - int voteDiff = 0; - String leadTeam = ""; - if (!tabVoteCt.equals(spaceVoteCt)) { - if (tabVoteCt > spaceVoteCt) { - leadTeam = "TABS"; - voteDiff = tabVoteCt - spaceVoteCt; - } else { - leadTeam = "SPACES"; - voteDiff = spaceVoteCt - tabVoteCt; - } - } -%>

- <% if(voteDiff != 0) { %> - <%= leadTeam %> are winning by <%= voteDiff %> <%= voteDiff > 1 ? "votes" : "vote" %>. - <% } else { %> - TABS and SPACES are tied! - <% } %> + + + TABS and SPACES are evenly matched! + + + TABS are winning by + ! + + + SPACES are winning by + !! + +

-
"> + + + +
keyboard_tab -

<%= tabVoteCt %> votes

+

votes

-
"> + + + +
space_bar -

<%= spaceVoteCt %> votes

+

votes

Recent Votes

    - <% for(Vote v : recentVotes) { %> +
  • - <% if(v.getCandiate().equals("tabs")) { %> - keyboard_tab - <% } else { %> - space_bar - <% } %> - A vote for <%= v.getCandiate().toUpperCase() %> -

    was cast at <%= v.getTimeCast() %>.

    + + + keyboard_tab + + + space_bar + + + + A vote for + +

    was cast at .

  • - <% } %> +
- - + document.getElementById("voteTabs").addEventListener("click", function () { + vote("TABS"); + }); + document.getElementById("voteSpaces").addEventListener("click", function () { + vote("SPACES"); + }); + +