From bd79d413d4cb2a6d89128a0c05ab4e2b1054caac Mon Sep 17 00:00:00 2001 From: Shubha Rajan Date: Fri, 22 Apr 2022 15:31:48 -0700 Subject: [PATCH 01/25] update mysql sample to include new region tags --- cloud-sql/mysql/servlet/README.md | 48 +++++ .../ConnectionPoolContextListener.java | 84 +-------- .../cloudsql/ConnectionPoolFactory.java | 173 ++++++++++++++++++ 3 files changed, 222 insertions(+), 83 deletions(-) create mode 100644 cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/ConnectionPoolFactory.java diff --git a/cloud-sql/mysql/servlet/README.md b/cloud-sql/mysql/servlet/README.md index 8b510b28d14..e9ac4ad1ada 100644 --- a/cloud-sql/mysql/servlet/README.md +++ b/cloud-sql/mysql/servlet/README.md @@ -29,6 +29,54 @@ 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/) or [Secret Manager](https://cloud.google.com/secret-manager/) to help keep secrets safe. +## Configure SSL Certificates +For deployments that connect directly to a Cloud SQL instance with TCP, without using the Cloud SQL Proxy, configuring SSL certificates will ensure the connection is encrypted. +1. Use the gcloud CLI to [download the server certificate](https://cloud.google.com/sql/docs/mysql/configure-ssl-instance#server-certs) for your Cloud SQL instance. + - Get information about the service certificate: + ``` + gcloud beta sql ssl server-ca-certs list --instance=INSTANCE_NAME + ``` + - Create a server certificate: + ``` + gcloud beta sql ssl server-ca-certs create --instance=INSTANCE_NAME + ``` + - Download the certificate information to a local PEM file + ``` + gcloud beta sql ssl server-ca-certs list \ + --format="value(cert)" \ + --instance=INSTANCE_NAME > \ + server-ca.pem + ``` + +1. Use the gcloud CLI to [create and download a client public key certificate and client private key](https://cloud.google.com/sql/docs/mysql/configure-ssl-instance#client-certs) + - Create a client certificate using the ssl client-certs create command: + ``` + gcloud sql ssl client-certs create CERT_NAME client-key.pem --instance=INSTANCE_NAME + ``` + - Retrieve the public key for the certificate you just created and copy it into the client-cert.pem file with the ssl client-certs describe command: + ``` + gcloud sql ssl client-certs describe CERT_NAME \ + --instance=INSTANCE_NAME \ + --format="value(cert)" > client-cert.pem + ``` +1. [Import the server certificate into a custom Java truststore](https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-reference-using-ssl.html) using `keytool`: + ``` + keytool -importcert -alias MySQLCACert -file server-ca.pem \ + -keystore -storepass + ``` +1. Set the `TRUST_CERT_KEYSTORE_PATH` and `TRUST_CERT_KEYSTORE_PASSWD` environment variables to the values used in the previous step. +1. [Import the client certificate and key into a custom Java keystore](https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-reference-using-ssl.html) using `openssl` and `keytool`: + - Convert the client key and certificate files to a PKCS #12 archive: + ``` + openssl pkcs12 -export -in client-cert.pem -inkey client-key.pem \ + -name "mysqlclient" -passout pass:mypassword -out client-keystore.p12 + ``` + - Import the client key and certificate into a Java keystore: + ``` + keytool -importkeystore -srckeystore client-keystore.p12 -srcstoretype pkcs12 \ + -srcstorepass -destkeystore -deststoretype JKS -deststorepass + ``` +1. Set the `CLIENT_CERT_KEYSTORE_PATH` and `CLIENT_CERT_KEYSTORE_PASSWD` environment variables to the values used in the previous step. ## Deploying locally To run this application locally, run the following command inside the project folder: 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 aa994574207..6020c51018e 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 @@ -16,7 +16,6 @@ package com.example.cloudsql; -import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import java.sql.Connection; @@ -34,87 +33,6 @@ @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 INSTANCE_CONNECTION_NAME = - System.getenv("INSTANCE_CONNECTION_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"); - - @SuppressFBWarnings( - value = "USBR_UNNECESSARY_STORE_BEFORE_RETURN", - justification = "Necessary for sample region tag.") - private DataSource createConnectionPool() { - // [START cloud_sql_mysql_servlet_create] - // Note: For Java users, the Cloud SQL JDBC Socket Factory can provide authenticated connections - // which is preferred to using the Cloud SQL Proxy with Unix sockets. - // See https://github.com/GoogleCloudPlatform/cloud-sql-jdbc-socket-factory for details. - - // The configuration object specifies behaviors for the connection pool. - HikariConfig config = new HikariConfig(); - - // The following URL is equivalent to setting the config options below: - // jdbc:mysql:///?cloudSqlInstance=& - // socketFactory=com.google.cloud.sql.mysql.SocketFactory&user=&password= - // See the link below for more info on building a JDBC URL for the Cloud SQL JDBC Socket Factory - // https://github.com/GoogleCloudPlatform/cloud-sql-jdbc-socket-factory#creating-the-jdbc-url - - // 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", "mysql" - config.setPassword(DB_PASS); // e.g. "my-password" - - config.addDataSourceProperty("socketFactory", "com.google.cloud.sql.mysql.SocketFactory"); - config.addDataSourceProperty("cloudSqlInstance", INSTANCE_CONNECTION_NAME); - - // The ipTypes argument can be used to specify a comma delimited list of preferred IP types - // for connecting to a Cloud SQL instance. The argument ipTypes=PRIVATE will force the - // SocketFactory to connect with an instance's associated private IP. - config.addDataSourceProperty("ipTypes", "PUBLIC,PRIVATE"); - - // ... Specify additional connection properties here. - // [START_EXCLUDE] - - // [START cloud_sql_mysql_servlet_limit] - // 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); - // 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_mysql_servlet_limit] - - // [START cloud_sql_mysql_servlet_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_mysql_servlet_timeout] - - // [START cloud_sql_mysql_servlet_backoff] - // Hikari automatically delays between failed connection attempts, eventually reaching a - // maximum delay of `connectionTimeout / 2` between attempts. - // [END cloud_sql_mysql_servlet_backoff] - - // [START cloud_sql_mysql_servlet_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. 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_mysql_servlet_lifetime] - - // [END_EXCLUDE] - - // Initialize the connection pool using the configuration object. - DataSource pool = new HikariDataSource(config); - // [END cloud_sql_mysql_servlet_create] - return pool; - } private void createTable(DataSource pool) throws SQLException { // Safely attempt to create the table schema. @@ -145,7 +63,7 @@ public void contextInitialized(ServletContextEvent event) { ServletContext servletContext = event.getServletContext(); DataSource pool = (DataSource) servletContext.getAttribute("my-pool"); if (pool == null) { - pool = createConnectionPool(); + pool = ConnectionPoolFactory.createConnectionPool(); servletContext.setAttribute("my-pool", pool); } try { diff --git a/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/ConnectionPoolFactory.java b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/ConnectionPoolFactory.java new file mode 100644 index 00000000000..c6de2203846 --- /dev/null +++ b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/ConnectionPoolFactory.java @@ -0,0 +1,173 @@ +package com.example.cloudsql; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import javax.sql.DataSource; + +public class ConnectionPoolFactory { + + // 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 INSTANCE_CONNECTION_NAME = + System.getenv("INSTANCE_CONNECTION_NAME"); + private static final String INSTANCE_HOST = System.getenv("INSTANCE_HOST"); + private static final String INSTANCE_UNIX_SOCKET = System.getenv("INSTANCE_UNIX_SOCKET"); + private static final String DB_PORT = System.getenv("DB_PORT"); + 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 static final String TRUST_CERT_KEYSTORE_PATH = System.getenv("TRUST_CERT_KEYSTORE_PATH"); + private static final String TRUST_CERT_KEYSTORE_PASSWD = System.getenv( + "TRUST_CERT_KEYSTORE_PASSWD"); + private static final String CLIENT_CERT_KEYSTORE_PATH = System.getenv( + "CLIENT_CERT_KEYSTORE_PATH"); + private static final String CLIENT_CERT_KEYSTORE_PASSWD = System.getenv( + "CLIENT_CERT_KEYSTORE_PASSWD"); + + @SuppressFBWarnings( + value = "USBR_UNNECESSARY_STORE_BEFORE_RETURN", + justification = "Necessary for sample region tag.") + private static HikariConfig configureConnectionPool(HikariConfig config) { + // [START cloud_sql_mysql_servlet_limit] + // 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); + // 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_mysql_servlet_limit] + + // [START cloud_sql_mysql_servlet_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_mysql_servlet_timeout] + + // [START cloud_sql_mysql_servlet_backoff] + // Hikari automatically delays between failed connection attempts, eventually reaching a + // maximum delay of `connectionTimeout / 2` between attempts. + // [END cloud_sql_mysql_servlet_backoff] + + // [START cloud_sql_mysql_servlet_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. 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_mysql_servlet_lifetime] + return config; + } + + public static DataSource createConnectionPool() { + if (INSTANCE_HOST != null) { + return createConnectionPoolWithTCP(); + } else { + return createConnectionPoolWithConnector(); + } + } + + private static DataSource createConnectionPoolWithConnector() { + // [START cloud_sql_mysql_servlet_connect_connector] + // [START cloud_sql_mysql_servlet_connect_unix] + // The configuration object specifies behaviors for the connection pool. + HikariConfig config = new HikariConfig(); + + // The following URL is equivalent to setting the config options below: + // jdbc:mysql:///?cloudSqlInstance=& + // socketFactory=com.google.cloud.sql.mysql.SocketFactory&user=&password= + // See the link below for more info on building a JDBC URL for the Cloud SQL JDBC Socket Factory + // https://github.com/GoogleCloudPlatform/cloud-sql-jdbc-socket-factory#creating-the-jdbc-url + + // 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", "mysql" + config.setPassword(DB_PASS); // e.g. "my-password" + + config.addDataSourceProperty("socketFactory", "com.google.cloud.sql.mysql.SocketFactory"); + config.addDataSourceProperty("cloudSqlInstance", INSTANCE_CONNECTION_NAME); + + // [END cloud_sql_mysql_servlet_connect_connector] + // Unix sockets are not natively supported in Java, so it is necessary to use the Cloud SQL JDBC + // Socket Factory to connect. + // Note: For Java users, the Cloud SQL JDBC Socket Factory can provide authenticated connections + // which is preferred to using the Cloud SQL Proxy with Unix sockets. + // See https://github.com/GoogleCloudPlatform/cloud-sql-jdbc-socket-factory for details. + if (INSTANCE_UNIX_SOCKET != null) { + config.addDataSourceProperty("unixSocketPath", INSTANCE_UNIX_SOCKET); + } + // [START cloud_sql_mysql_servlet_connect_connector] + + + // The ipTypes argument can be used to specify a comma delimited list of preferred IP types + // for connecting to a Cloud SQL instance. The argument ipTypes=PRIVATE will force the + // SocketFactory to connect with an instance's associated private IP. + config.addDataSourceProperty("ipTypes", "PUBLIC,PRIVATE"); + + // ... Specify additional connection properties here. + // [START_EXCLUDE] + configureConnectionPool(config); + // [END_EXCLUDE] + + // Initialize the connection pool using the configuration object. + DataSource pool = new HikariDataSource(config); + // [END cloud_sql_mysql_servlet_connect_connector] + // [END cloud_sql_mysql_servlet_connect_unix] + return pool; + } + + @SuppressFBWarnings( + value = "USBR_UNNECESSARY_STORE_BEFORE_RETURN", + justification = "Necessary for sample region tag.") + private static DataSource createConnectionPoolWithTCP() { + // [START cloud_sql_mysql_servlet_connect_tcp] + // [START cloud_sql_mysql_servlet_connect_tcp_sslcerts] + // The configuration object specifies behaviors for the connection pool. + HikariConfig config = new HikariConfig(); + + // The following URL is equivalent to setting the config options below: + // jdbc:mysql://:/?user=&password= + // See the link below for more info on building a JDBC URL for the Cloud SQL JDBC Socket Factory + // https://github.com/GoogleCloudPlatform/cloud-sql-jdbc-socket-factory#creating-the-jdbc-url + + // Configure which instance and what database user to connect with. + config.setJdbcUrl(String.format("jdbc:mysql://%s:%s/%s", INSTANCE_HOST, DB_PORT, DB_NAME)); + config.setUsername(DB_USER); // e.g. "root", "mysql" + config.setPassword(DB_PASS); // e.g. "my-password" + + // [END cloud_sql_mysql_servlet_connect_tcp] + // (OPTIONAL) Configure SSL certificates + // For deployments that connect directly to a Cloud SQL instance without + // using the Cloud SQL Proxy, configuring SSL certificates will ensure the + // connection is encrypted. + // See the link below for more information on how to configure SSL Certificates for use with + // MySQL Connector/J + // + if (CLIENT_CERT_KEYSTORE_PATH != null && TRUST_CERT_KEYSTORE_PATH != null) { + config.addDataSourceProperty("trustCertificateKeyStoreUrl", + String.format("file:%s", TRUST_CERT_KEYSTORE_PATH)); + config.addDataSourceProperty("trustCertificateKeyStorePassword", TRUST_CERT_KEYSTORE_PASSWD); + config.addDataSourceProperty("clientCertificateKeyStoreUrl", + String.format("file:%s", CLIENT_CERT_KEYSTORE_PATH)); + config.addDataSourceProperty("clientCertificateKeyStorePassword", + CLIENT_CERT_KEYSTORE_PASSWD); + } + // [START cloud_sql_mysql_servlet_connect_tcp] + + // ... Specify additional connection properties here. + // [START_EXCLUDE] + configureConnectionPool(config); + // [END_EXCLUDE] + + // Initialize the connection pool using the configuration object. + DataSource pool = new HikariDataSource(config); + // [END cloud_sql_mysql_servlet_connect_tcp] + // [END cloud_sql_mysql_servlet_connect_tcp_sslcerts] + return pool; + } +} From 65b4ef7405e1e2f0db7f0f2f7247b9d7f31f2e43 Mon Sep 17 00:00:00 2001 From: Shubha Rajan Date: Fri, 22 Apr 2022 15:40:00 -0700 Subject: [PATCH 02/25] add license header --- .../cloudsql/ConnectionPoolFactory.java | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/ConnectionPoolFactory.java b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/ConnectionPoolFactory.java index c6de2203846..b5eab071af8 100644 --- a/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/ConnectionPoolFactory.java +++ b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/ConnectionPoolFactory.java @@ -1,3 +1,19 @@ +/* + * Copyright 2022 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; @@ -66,7 +82,7 @@ private static HikariConfig configureConnectionPool(HikariConfig config) { public static DataSource createConnectionPool() { if (INSTANCE_HOST != null) { - return createConnectionPoolWithTCP(); + return createConnectionPoolWithTcp(); } else { return createConnectionPoolWithConnector(); } @@ -103,7 +119,6 @@ private static DataSource createConnectionPoolWithConnector() { } // [START cloud_sql_mysql_servlet_connect_connector] - // The ipTypes argument can be used to specify a comma delimited list of preferred IP types // for connecting to a Cloud SQL instance. The argument ipTypes=PRIVATE will force the // SocketFactory to connect with an instance's associated private IP. @@ -124,7 +139,7 @@ private static DataSource createConnectionPoolWithConnector() { @SuppressFBWarnings( value = "USBR_UNNECESSARY_STORE_BEFORE_RETURN", justification = "Necessary for sample region tag.") - private static DataSource createConnectionPoolWithTCP() { + private static DataSource createConnectionPoolWithTcp() { // [START cloud_sql_mysql_servlet_connect_tcp] // [START cloud_sql_mysql_servlet_connect_tcp_sslcerts] // The configuration object specifies behaviors for the connection pool. From aa506d7e15ba82c01f7a9a2c0a4745aef1e43048 Mon Sep 17 00:00:00 2001 From: Shubha Rajan Date: Mon, 25 Apr 2022 18:14:29 -0700 Subject: [PATCH 03/25] move connection code for TCP and connector to separate files --- .../ConnectionPoolContextListener.java | 7 +- .../cloudsql/ConnectionPoolFactory.java | 127 +----------------- .../ConnectorConnectionPoolFactory.java | 87 ++++++++++++ .../cloudsql/TcpConnectionPoolFactory.java | 96 +++++++++++++ 4 files changed, 190 insertions(+), 127 deletions(-) create mode 100644 cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/ConnectorConnectionPoolFactory.java create mode 100644 cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/TcpConnectionPoolFactory.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 6020c51018e..ab0a6637faa 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 @@ -63,7 +63,12 @@ public void contextInitialized(ServletContextEvent event) { ServletContext servletContext = event.getServletContext(); DataSource pool = (DataSource) servletContext.getAttribute("my-pool"); if (pool == null) { - pool = ConnectionPoolFactory.createConnectionPool(); + if (System.getenv("INSTANCE_HOST") != null) { + pool = TcpConnectionPoolFactory.createConnectionPool(); + } else { + pool = ConnectorConnectionPoolFactory.createConnectionPool(); + } + servletContext.setAttribute("my-pool", pool); } try { diff --git a/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/ConnectionPoolFactory.java b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/ConnectionPoolFactory.java index b5eab071af8..9ec4732a7b4 100644 --- a/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/ConnectionPoolFactory.java +++ b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/ConnectionPoolFactory.java @@ -23,29 +23,10 @@ public class ConnectionPoolFactory { - // 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 INSTANCE_CONNECTION_NAME = - System.getenv("INSTANCE_CONNECTION_NAME"); - private static final String INSTANCE_HOST = System.getenv("INSTANCE_HOST"); - private static final String INSTANCE_UNIX_SOCKET = System.getenv("INSTANCE_UNIX_SOCKET"); - private static final String DB_PORT = System.getenv("DB_PORT"); - 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 static final String TRUST_CERT_KEYSTORE_PATH = System.getenv("TRUST_CERT_KEYSTORE_PATH"); - private static final String TRUST_CERT_KEYSTORE_PASSWD = System.getenv( - "TRUST_CERT_KEYSTORE_PASSWD"); - private static final String CLIENT_CERT_KEYSTORE_PATH = System.getenv( - "CLIENT_CERT_KEYSTORE_PATH"); - private static final String CLIENT_CERT_KEYSTORE_PASSWD = System.getenv( - "CLIENT_CERT_KEYSTORE_PASSWD"); - @SuppressFBWarnings( value = "USBR_UNNECESSARY_STORE_BEFORE_RETURN", justification = "Necessary for sample region tag.") - private static HikariConfig configureConnectionPool(HikariConfig config) { + public static HikariConfig configureConnectionPool(HikariConfig config) { // [START cloud_sql_mysql_servlet_limit] // 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. @@ -79,110 +60,4 @@ private static HikariConfig configureConnectionPool(HikariConfig config) { // [END cloud_sql_mysql_servlet_lifetime] return config; } - - public static DataSource createConnectionPool() { - if (INSTANCE_HOST != null) { - return createConnectionPoolWithTcp(); - } else { - return createConnectionPoolWithConnector(); - } - } - - private static DataSource createConnectionPoolWithConnector() { - // [START cloud_sql_mysql_servlet_connect_connector] - // [START cloud_sql_mysql_servlet_connect_unix] - // The configuration object specifies behaviors for the connection pool. - HikariConfig config = new HikariConfig(); - - // The following URL is equivalent to setting the config options below: - // jdbc:mysql:///?cloudSqlInstance=& - // socketFactory=com.google.cloud.sql.mysql.SocketFactory&user=&password= - // See the link below for more info on building a JDBC URL for the Cloud SQL JDBC Socket Factory - // https://github.com/GoogleCloudPlatform/cloud-sql-jdbc-socket-factory#creating-the-jdbc-url - - // 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", "mysql" - config.setPassword(DB_PASS); // e.g. "my-password" - - config.addDataSourceProperty("socketFactory", "com.google.cloud.sql.mysql.SocketFactory"); - config.addDataSourceProperty("cloudSqlInstance", INSTANCE_CONNECTION_NAME); - - // [END cloud_sql_mysql_servlet_connect_connector] - // Unix sockets are not natively supported in Java, so it is necessary to use the Cloud SQL JDBC - // Socket Factory to connect. - // Note: For Java users, the Cloud SQL JDBC Socket Factory can provide authenticated connections - // which is preferred to using the Cloud SQL Proxy with Unix sockets. - // See https://github.com/GoogleCloudPlatform/cloud-sql-jdbc-socket-factory for details. - if (INSTANCE_UNIX_SOCKET != null) { - config.addDataSourceProperty("unixSocketPath", INSTANCE_UNIX_SOCKET); - } - // [START cloud_sql_mysql_servlet_connect_connector] - - // The ipTypes argument can be used to specify a comma delimited list of preferred IP types - // for connecting to a Cloud SQL instance. The argument ipTypes=PRIVATE will force the - // SocketFactory to connect with an instance's associated private IP. - config.addDataSourceProperty("ipTypes", "PUBLIC,PRIVATE"); - - // ... Specify additional connection properties here. - // [START_EXCLUDE] - configureConnectionPool(config); - // [END_EXCLUDE] - - // Initialize the connection pool using the configuration object. - DataSource pool = new HikariDataSource(config); - // [END cloud_sql_mysql_servlet_connect_connector] - // [END cloud_sql_mysql_servlet_connect_unix] - return pool; - } - - @SuppressFBWarnings( - value = "USBR_UNNECESSARY_STORE_BEFORE_RETURN", - justification = "Necessary for sample region tag.") - private static DataSource createConnectionPoolWithTcp() { - // [START cloud_sql_mysql_servlet_connect_tcp] - // [START cloud_sql_mysql_servlet_connect_tcp_sslcerts] - // The configuration object specifies behaviors for the connection pool. - HikariConfig config = new HikariConfig(); - - // The following URL is equivalent to setting the config options below: - // jdbc:mysql://:/?user=&password= - // See the link below for more info on building a JDBC URL for the Cloud SQL JDBC Socket Factory - // https://github.com/GoogleCloudPlatform/cloud-sql-jdbc-socket-factory#creating-the-jdbc-url - - // Configure which instance and what database user to connect with. - config.setJdbcUrl(String.format("jdbc:mysql://%s:%s/%s", INSTANCE_HOST, DB_PORT, DB_NAME)); - config.setUsername(DB_USER); // e.g. "root", "mysql" - config.setPassword(DB_PASS); // e.g. "my-password" - - // [END cloud_sql_mysql_servlet_connect_tcp] - // (OPTIONAL) Configure SSL certificates - // For deployments that connect directly to a Cloud SQL instance without - // using the Cloud SQL Proxy, configuring SSL certificates will ensure the - // connection is encrypted. - // See the link below for more information on how to configure SSL Certificates for use with - // MySQL Connector/J - // - if (CLIENT_CERT_KEYSTORE_PATH != null && TRUST_CERT_KEYSTORE_PATH != null) { - config.addDataSourceProperty("trustCertificateKeyStoreUrl", - String.format("file:%s", TRUST_CERT_KEYSTORE_PATH)); - config.addDataSourceProperty("trustCertificateKeyStorePassword", TRUST_CERT_KEYSTORE_PASSWD); - config.addDataSourceProperty("clientCertificateKeyStoreUrl", - String.format("file:%s", CLIENT_CERT_KEYSTORE_PATH)); - config.addDataSourceProperty("clientCertificateKeyStorePassword", - CLIENT_CERT_KEYSTORE_PASSWD); - } - // [START cloud_sql_mysql_servlet_connect_tcp] - - // ... Specify additional connection properties here. - // [START_EXCLUDE] - configureConnectionPool(config); - // [END_EXCLUDE] - - // Initialize the connection pool using the configuration object. - DataSource pool = new HikariDataSource(config); - // [END cloud_sql_mysql_servlet_connect_tcp] - // [END cloud_sql_mysql_servlet_connect_tcp_sslcerts] - return pool; - } } diff --git a/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/ConnectorConnectionPoolFactory.java b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/ConnectorConnectionPoolFactory.java new file mode 100644 index 00000000000..425b389a707 --- /dev/null +++ b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/ConnectorConnectionPoolFactory.java @@ -0,0 +1,87 @@ +/* + * 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 edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import javax.sql.DataSource; + + +public class ConnectorConnectionPoolFactory extends ConnectionPoolFactory { + + // 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 INSTANCE_CONNECTION_NAME = + System.getenv("INSTANCE_CONNECTION_NAME"); + private static final String INSTANCE_UNIX_SOCKET = System.getenv("INSTANCE_UNIX_SOCKET"); + 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"); + + @SuppressFBWarnings( + value = "USBR_UNNECESSARY_STORE_BEFORE_RETURN", + justification = "Necessary for sample region tag.") + public static DataSource createConnectionPool() { + // [START cloud_sql_mysql_servlet_connect_connector] + // [START cloud_sql_mysql_servlet_connect_unix] + // The configuration object specifies behaviors for the connection pool. + HikariConfig config = new HikariConfig(); + + // The following URL is equivalent to setting the config options below: + // jdbc:mysql:///?cloudSqlInstance=& + // socketFactory=com.google.cloud.sql.mysql.SocketFactory&user=&password= + // See the link below for more info on building a JDBC URL for the Cloud SQL JDBC Socket Factory + // https://github.com/GoogleCloudPlatform/cloud-sql-jdbc-socket-factory#creating-the-jdbc-url + + // 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", "mysql" + config.setPassword(DB_PASS); // e.g. "my-password" + + config.addDataSourceProperty("socketFactory", "com.google.cloud.sql.mysql.SocketFactory"); + config.addDataSourceProperty("cloudSqlInstance", INSTANCE_CONNECTION_NAME); + + // [END cloud_sql_mysql_servlet_connect_connector] + // Unix sockets are not natively supported in Java, so it is necessary to use the Cloud SQL JDBC + // Socket Factory to connect. + // Note: For Java users, the Cloud SQL JDBC Socket Factory can provide authenticated connections + // which is preferred to using the Cloud SQL Proxy with Unix sockets. + // See https://github.com/GoogleCloudPlatform/cloud-sql-jdbc-socket-factory for details. + if (INSTANCE_UNIX_SOCKET != null) { + config.addDataSourceProperty("unixSocketPath", INSTANCE_UNIX_SOCKET); + } + // [START cloud_sql_mysql_servlet_connect_connector] + + // The ipTypes argument can be used to specify a comma delimited list of preferred IP types + // for connecting to a Cloud SQL instance. The argument ipTypes=PRIVATE will force the + // SocketFactory to connect with an instance's associated private IP. + config.addDataSourceProperty("ipTypes", "PUBLIC,PRIVATE"); + + // ... Specify additional connection properties here. + // [START_EXCLUDE] + configureConnectionPool(config); + // [END_EXCLUDE] + + // Initialize the connection pool using the configuration object. + DataSource pool = new HikariDataSource(config); + // [END cloud_sql_mysql_servlet_connect_connector] + // [END cloud_sql_mysql_servlet_connect_unix] + return pool; + } + +} diff --git a/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/TcpConnectionPoolFactory.java b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/TcpConnectionPoolFactory.java new file mode 100644 index 00000000000..b8fdfd29619 --- /dev/null +++ b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/TcpConnectionPoolFactory.java @@ -0,0 +1,96 @@ +/* + * 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 edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import javax.sql.DataSource; + + +public class TcpConnectionPoolFactory extends ConnectionPoolFactory { + + @SuppressFBWarnings( + value = "USBR_UNNECESSARY_STORE_BEFORE_RETURN", + justification = "Necessary for sample region tag.") + + // 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 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 static final String INSTANCE_HOST = System.getenv("INSTANCE_HOST"); + private static final String DB_PORT = System.getenv("DB_PORT"); + + private static final String TRUST_CERT_KEYSTORE_PATH = System.getenv( + "TRUST_CERT_KEYSTORE_PATH"); + private static final String TRUST_CERT_KEYSTORE_PASSWD = System.getenv( + "TRUST_CERT_KEYSTORE_PASSWD"); + private static final String CLIENT_CERT_KEYSTORE_PATH = System.getenv( + "CLIENT_CERT_KEYSTORE_PATH"); + private static final String CLIENT_CERT_KEYSTORE_PASSWD = System.getenv( + "CLIENT_CERT_KEYSTORE_PASSWD"); + + public static DataSource createConnectionPool() { + // [START cloud_sql_mysql_servlet_connect_tcp] + // [START cloud_sql_mysql_servlet_connect_tcp_sslcerts] + // The configuration object specifies behaviors for the connection pool. + HikariConfig config = new HikariConfig(); + + // The following URL is equivalent to setting the config options below: + // jdbc:mysql://:/?user=&password= + // See the link below for more info on building a JDBC URL for the Cloud SQL JDBC Socket Factory + // https://github.com/GoogleCloudPlatform/cloud-sql-jdbc-socket-factory#creating-the-jdbc-url + + // Configure which instance and what database user to connect with. + config.setJdbcUrl(String.format("jdbc:mysql://%s:%s/%s", INSTANCE_HOST, DB_PORT, DB_NAME)); + config.setUsername(DB_USER); // e.g. "root", "mysql" + config.setPassword(DB_PASS); // e.g. "my-password" + + // [END cloud_sql_mysql_servlet_connect_tcp] + // (OPTIONAL) Configure SSL certificates + // For deployments that connect directly to a Cloud SQL instance without + // using the Cloud SQL Proxy, configuring SSL certificates will ensure the + // connection is encrypted. + // See the link below for more information on how to configure SSL Certificates for use with + // MySQL Connector/J + // + if (CLIENT_CERT_KEYSTORE_PATH != null && TRUST_CERT_KEYSTORE_PATH != null) { + config.addDataSourceProperty("trustCertificateKeyStoreUrl", + String.format("file:%s", TRUST_CERT_KEYSTORE_PATH)); + config.addDataSourceProperty("trustCertificateKeyStorePassword", TRUST_CERT_KEYSTORE_PASSWD); + config.addDataSourceProperty("clientCertificateKeyStoreUrl", + String.format("file:%s", CLIENT_CERT_KEYSTORE_PATH)); + config.addDataSourceProperty("clientCertificateKeyStorePassword", + CLIENT_CERT_KEYSTORE_PASSWD); + } + // [START cloud_sql_mysql_servlet_connect_tcp] + + // ... Specify additional connection properties here. + // [START_EXCLUDE] + configureConnectionPool(config); + // [END_EXCLUDE] + + // Initialize the connection pool using the configuration object. + DataSource pool = new HikariDataSource(config); + // [END cloud_sql_mysql_servlet_connect_tcp] + // [END cloud_sql_mysql_servlet_connect_tcp_sslcerts] + return pool; + } + +} From d8f9198721e0838f8b5d94ea7da4744392120387 Mon Sep 17 00:00:00 2001 From: Shubha Rajan Date: Mon, 25 Apr 2022 19:08:58 -0700 Subject: [PATCH 04/25] add functions sample --- cloud-sql/mysql/servlet/pom.xml | 5 + .../ConnectionPoolContextListener.java | 1 - .../com/example/cloudsql/IndexServlet.java | 63 +-------- .../com/example/cloudsql/TemplateData.java | 90 ++++++++++++ .../com/example/cloudsql/functions/Main.java | 131 ++++++++++++++++++ .../com/example/cloudsql/functions/pom.xml | 45 ++++++ .../test/java/com/TestIndexServletMysql.java | 1 - 7 files changed, 275 insertions(+), 61 deletions(-) create mode 100644 cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/TemplateData.java create mode 100644 cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/functions/Main.java create mode 100644 cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/functions/pom.xml diff --git a/cloud-sql/mysql/servlet/pom.xml b/cloud-sql/mysql/servlet/pom.xml index 0ffc5709b88..11fd9f02808 100644 --- a/cloud-sql/mysql/servlet/pom.xml +++ b/cloud-sql/mysql/servlet/pom.xml @@ -92,6 +92,11 @@ 1.1.3 test + + com.google.cloud.functions.invoker + java-function-invoker + 1.0.1 + 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 ab0a6637faa..fb83fafb65a 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 @@ -68,7 +68,6 @@ public void contextInitialized(ServletContextEvent event) { } else { pool = ConnectorConnectionPoolFactory.createConnectionPool(); } - servletContext.setAttribute("my-pool", pool); } try { 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 7f43bf811cd..686d7e7ff6e 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 @@ -47,67 +47,12 @@ public class IndexServlet extends HttpServlet { private static final Logger LOGGER = Logger.getLogger(IndexServlet.class.getName()); - class TemplateData { - - public int tabCount; - public int spaceCount; - public List recentVotes; - - public TemplateData(int tabCount, int spaceCount, List recentVotes) { - this.tabCount = tabCount; - this.spaceCount = spaceCount; - this.recentVotes = recentVotes; - } - } - - public TemplateData getTemplateData(DataSource pool) throws ServletException { - - int tabCount = 0; - int spaceCount = 0; - 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. - String stmt1 = "SELECT candidate, time_cast FROM votes ORDER BY time_cast DESC LIMIT 5"; - try (PreparedStatement voteStmt = conn.prepareStatement(stmt1);) { - // 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. - String stmt2 = "SELECT COUNT(vote_id) FROM votes WHERE candidate=?"; - try (PreparedStatement voteCountStmt = conn.prepareStatement(stmt2);) { - voteCountStmt.setString(1, "TABS"); - ResultSet tabResult = voteCountStmt.executeQuery(); - if (tabResult.next()) { // Move to the first result - tabCount = tabResult.getInt(1); - } - - voteCountStmt.setString(1, "SPACES"); - ResultSet spaceResult = voteCountStmt.executeQuery(); - if (spaceResult.next()) { // Move to the first result - spaceCount = spaceResult.getInt(1); - } - } + TemplateData getTemplateData(DataSource pool) throws ServletException { + try { + return TemplateData.getTemplateData(pool); } 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.", - ex); + throw new ServletException(ex); } - TemplateData templateData = new TemplateData(tabCount, spaceCount, recentVotes); - - return templateData; } @Override diff --git a/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/TemplateData.java b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/TemplateData.java new file mode 100644 index 00000000000..e86aa815029 --- /dev/null +++ b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/TemplateData.java @@ -0,0 +1,90 @@ +/* + * 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.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.List; +import javax.servlet.ServletException; +import javax.sql.DataSource; + +public class TemplateData { + + public int tabCount; + public int spaceCount; + public List recentVotes; + + public TemplateData(int tabCount, int spaceCount, List recentVotes) { + this.tabCount = tabCount; + this.spaceCount = spaceCount; + this.recentVotes = recentVotes; + } + + public static TemplateData getTemplateData(DataSource pool) throws SQLException { + + int tabCount = 0; + int spaceCount = 0; + 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. + String stmt1 = "SELECT candidate, time_cast FROM votes ORDER BY time_cast DESC LIMIT 5"; + try (PreparedStatement voteStmt = conn.prepareStatement(stmt1);) { + // 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. + String stmt2 = "SELECT COUNT(vote_id) FROM votes WHERE candidate=?"; + try (PreparedStatement voteCountStmt = conn.prepareStatement(stmt2);) { + voteCountStmt.setString(1, "TABS"); + ResultSet tabResult = voteCountStmt.executeQuery(); + if (tabResult.next()) { // Move to the first result + tabCount = tabResult.getInt(1); + } + + voteCountStmt.setString(1, "SPACES"); + ResultSet spaceResult = voteCountStmt.executeQuery(); + if (spaceResult.next()) { // Move to the first result + spaceCount = spaceResult.getInt(1); + } + } + } 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 SQLException( + "Unable to successfully connect to the database. Please check the " + + "steps in the README and try again.", + ex); + } + TemplateData templateData = new TemplateData(tabCount, spaceCount, recentVotes); + + return templateData; + } +} diff --git a/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/functions/Main.java b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/functions/Main.java new file mode 100644 index 00000000000..5f9e46e4f62 --- /dev/null +++ b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/functions/Main.java @@ -0,0 +1,131 @@ +package com.example.cloudsql.functions; + +import com.example.cloudsql.ConnectorConnectionPoolFactory; +import com.example.cloudsql.TcpConnectionPoolFactory; +import com.example.cloudsql.TemplateData; +import com.google.cloud.functions.HttpFunction; +import com.google.cloud.functions.HttpRequest; +import com.google.cloud.functions.HttpResponse; +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import java.io.IOException; +import java.net.HttpURLConnection; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.util.Date; +import java.util.Locale; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.annotation.Nullable; +import javax.sql.DataSource; + +public class Main implements HttpFunction { + + private DataSource pool; + private Logger logger = Logger.getLogger(Main.class.getName()); + private static final Gson gson = new Gson(); + + private void createTable(DataSource pool) throws SQLException { + // Safely attempt to create the table schema. + try (Connection conn = pool.getConnection()) { + String stmt = + "CREATE TABLE IF NOT EXISTS votes ( " + + "vote_id SERIAL NOT NULL, time_cast timestamp NOT NULL, candidate CHAR(6) NOT NULL," + + " PRIMARY KEY (vote_id) );"; + try (PreparedStatement createTableStatement = conn.prepareStatement(stmt);) { + createTableStatement.execute(); + } + } + } + + // Used to validate user input. All user provided data should be validated and sanitized before + // being used something like a SQL query. Returns null if invalid. + @Nullable + private String validateTeam(String input) { + if (input != null) { + input = input.toUpperCase(Locale.ENGLISH); + // Must be either "TABS" or "SPACES" + if (!"TABS".equals(input) && !"SPACES".equals(input)) { + return null; + } + } + return input; + } + + + private void submitVote(HttpRequest req, HttpResponse resp) throws IOException { + Timestamp now = new Timestamp(new Date().getTime()); + String team = validateTeam(req.getFirstQueryParameter("team").get()); + if (team == null) { + resp.setStatusCode(400); + resp.getWriter().append("Invalid team specified."); + return; + } + try (Connection conn = pool.getConnection()) { + // PreparedStatements can be more efficient and project against injections. + String stmt = "INSERT INTO votes (time_cast, candidate) VALUES (?, ?);"; + try (PreparedStatement voteStmt = conn.prepareStatement(stmt);) { + voteStmt.setTimestamp(1, now); + voteStmt.setString(2, team); + + // Finally, execute the statement. If it fails, an error will be thrown. + voteStmt.execute(); + } + } catch (SQLException ex) { + // If something goes wrong, handle the error in this section. This might involve retrying or + // adjusting parameters depending on the situation. + logger.log(Level.WARNING, "Error while attempting to submit vote.", ex); + resp.setStatusCode(500); + resp.getWriter() + .write( + "Unable to successfully cast vote! Please check the application " + + "logs for more details."); + resp.setStatusCode(HttpURLConnection.HTTP_CREATED); + } + } + + @Override + public void service(HttpRequest req, HttpResponse resp) throws IOException, SQLException { + // lazily initialize pool and create table + if (pool == null) { + if (System.getenv("INSTANCE_HOST") != null) { + pool = TcpConnectionPoolFactory.createConnectionPool(); + } else { + pool = ConnectorConnectionPoolFactory.createConnectionPool(); + } + 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); + } + } + + String method = req.getMethod(); + switch (method) { + case "GET": + TemplateData templateData = TemplateData.getTemplateData(pool); + JsonObject respContent = new JsonObject(); + + // Return JSON Data + respContent.addProperty("tabCount", templateData.tabCount); + respContent.addProperty("spaceCount", templateData.spaceCount); + respContent.addProperty("recentVotes", gson.toJson(templateData.recentVotes)); + resp.getWriter().write(respContent.getAsString()); + resp.setStatusCode(HttpURLConnection.HTTP_OK); + break; + case "POST": + submitVote(req, resp); + break; + default: + resp.setStatusCode(HttpURLConnection.HTTP_BAD_METHOD); + resp.getWriter().write(String.format("HTTP Method %s is not supported", method)); + break; + } + + } +} diff --git a/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/functions/pom.xml b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/functions/pom.xml new file mode 100644 index 00000000000..36bc97f6f5a --- /dev/null +++ b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/functions/pom.xml @@ -0,0 +1,45 @@ + + 4.0.0 + + com.example.cloud.functions + functions-cloud-sql-mysql + 1.0.0-SNAPSHOT + + 11 + 11 + + + + + + com.google.cloud.functions + functions-framework-api + 1.0.4 + provided + + + + + + + + com.google.cloud.functions + function-maven-plugin + 0.10.0 + + com.example.cloudsql.functions.Main + + + + + diff --git a/cloud-sql/mysql/servlet/src/test/java/com/TestIndexServletMysql.java b/cloud-sql/mysql/servlet/src/test/java/com/TestIndexServletMysql.java index abc81dfc142..57b624ba526 100644 --- a/cloud-sql/mysql/servlet/src/test/java/com/TestIndexServletMysql.java +++ b/cloud-sql/mysql/servlet/src/test/java/com/TestIndexServletMysql.java @@ -22,7 +22,6 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -import com.example.cloudsql.IndexServlet.TemplateData; import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; import java.io.PrintWriter; From 6e1ead52955d50dc101de4486de772e8d2112473 Mon Sep 17 00:00:00 2001 From: Shubha Rajan Date: Mon, 25 Apr 2022 19:15:00 -0700 Subject: [PATCH 05/25] add missing license headers --- .../cloudsql/ConnectorConnectionPoolFactory.java | 2 +- .../cloudsql/TcpConnectionPoolFactory.java | 2 +- .../java/com/example/cloudsql/TemplateData.java | 2 +- .../com/example/cloudsql/functions/Main.java | 16 ++++++++++++++++ .../java/com/example/cloudsql/functions/pom.xml | 15 +++++++++++++++ 5 files changed, 34 insertions(+), 3 deletions(-) diff --git a/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/ConnectorConnectionPoolFactory.java b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/ConnectorConnectionPoolFactory.java index 425b389a707..d8d23c592cb 100644 --- a/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/ConnectorConnectionPoolFactory.java +++ b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/ConnectorConnectionPoolFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC + * Copyright 2022 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/TcpConnectionPoolFactory.java b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/TcpConnectionPoolFactory.java index b8fdfd29619..94ca58fa9af 100644 --- a/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/TcpConnectionPoolFactory.java +++ b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/TcpConnectionPoolFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC + * Copyright 2022 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/TemplateData.java b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/TemplateData.java index e86aa815029..6190996042c 100644 --- a/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/TemplateData.java +++ b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/TemplateData.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC + * Copyright 2022 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/functions/Main.java b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/functions/Main.java index 5f9e46e4f62..1e0f743e59a 100644 --- a/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/functions/Main.java +++ b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/functions/Main.java @@ -1,3 +1,19 @@ +/* + * Copyright 2022 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.functions; import com.example.cloudsql.ConnectorConnectionPoolFactory; diff --git a/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/functions/pom.xml b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/functions/pom.xml index 36bc97f6f5a..5e994325432 100644 --- a/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/functions/pom.xml +++ b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/functions/pom.xml @@ -1,3 +1,18 @@ + From 09b85808db4b8cf029bc938d73c024cca7cbdafb Mon Sep 17 00:00:00 2001 From: Shubha Rajan Date: Mon, 25 Apr 2022 19:17:31 -0700 Subject: [PATCH 06/25] Update region tag --- .../com/example/cloudsql/ConnectorConnectionPoolFactory.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/ConnectorConnectionPoolFactory.java b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/ConnectorConnectionPoolFactory.java index d8d23c592cb..11a72c4340b 100644 --- a/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/ConnectorConnectionPoolFactory.java +++ b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/ConnectorConnectionPoolFactory.java @@ -67,10 +67,12 @@ public static DataSource createConnectionPool() { } // [START cloud_sql_mysql_servlet_connect_connector] + // [END cloud_sql_mysql_servlet_connect_unix] // The ipTypes argument can be used to specify a comma delimited list of preferred IP types // for connecting to a Cloud SQL instance. The argument ipTypes=PRIVATE will force the // SocketFactory to connect with an instance's associated private IP. config.addDataSourceProperty("ipTypes", "PUBLIC,PRIVATE"); + // [START cloud_sql_mysql_servlet_connect_unix] // ... Specify additional connection properties here. // [START_EXCLUDE] From b1915ee651f602ffdbdbb750de48d8fd553498ec Mon Sep 17 00:00:00 2001 From: Shubha Rajan Date: Tue, 26 Apr 2022 09:55:03 -0700 Subject: [PATCH 07/25] reformat code --- .../src/main/java/com/example/cloudsql/IndexServlet.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 686d7e7ff6e..9d253add13d 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 @@ -38,7 +38,6 @@ import javax.sql.DataSource; - @SuppressFBWarnings( value = {"SE_NO_SERIALVERSIONID", "WEM_WEAK_EXCEPTION_MESSAGING"}, justification = "Not needed for IndexServlet, Exception adds context") @@ -51,7 +50,7 @@ TemplateData getTemplateData(DataSource pool) throws ServletException { try { return TemplateData.getTemplateData(pool); } catch (SQLException ex) { - throw new ServletException(ex); + throw new ServletException(ex); } } From a111d1558c9418efb3825e97eb734d22dd845163 Mon Sep 17 00:00:00 2001 From: Shubha Rajan Date: Tue, 26 Apr 2022 09:58:06 -0700 Subject: [PATCH 08/25] fix function sample --- .../src/main/java/com/example/cloudsql/functions/Main.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/functions/Main.java b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/functions/Main.java index 1e0f743e59a..245826132db 100644 --- a/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/functions/Main.java +++ b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/functions/Main.java @@ -98,7 +98,6 @@ private void submitVote(HttpRequest req, HttpResponse resp) throws IOException { .write( "Unable to successfully cast vote! Please check the application " + "logs for more details."); - resp.setStatusCode(HttpURLConnection.HTTP_CREATED); } } @@ -119,6 +118,7 @@ public void service(HttpRequest req, HttpResponse resp) throws IOException, SQLE + "in the README and try again.", ex); } + resp.setStatusCode(HttpURLConnection.HTTP_CREATED); } String method = req.getMethod(); From 34b7b4d9a600b5f56f3a04dd833a64a901634224 Mon Sep 17 00:00:00 2001 From: Shubha Rajan Date: Tue, 26 Apr 2022 15:45:35 -0700 Subject: [PATCH 09/25] get function working --- cloud-sql/mysql/servlet/pom.xml | 23 +++++++ .../com/example/cloudsql/functions/Main.java | 5 +- .../com/example/cloudsql/functions/pom.xml | 60 ------------------- 3 files changed, 26 insertions(+), 62 deletions(-) delete mode 100644 cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/functions/pom.xml diff --git a/cloud-sql/mysql/servlet/pom.xml b/cloud-sql/mysql/servlet/pom.xml index 11fd9f02808..f71710a4bd4 100644 --- a/cloud-sql/mysql/servlet/pom.xml +++ b/cloud-sql/mysql/servlet/pom.xml @@ -97,6 +97,12 @@ java-function-invoker 1.0.1 + + com.google.cloud.functions + functions-framework-api + 1.0.4 + provided + @@ -119,6 +125,23 @@ GCLOUD_CONFIG + + + com.google.cloud.functions + function-maven-plugin + 0.10.0 + + com.example.cloudsql.functions.Main + + diff --git a/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/functions/Main.java b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/functions/Main.java index 245826132db..6a850719491 100644 --- a/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/functions/Main.java +++ b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/functions/Main.java @@ -73,7 +73,8 @@ private String validateTeam(String input) { private void submitVote(HttpRequest req, HttpResponse resp) throws IOException { Timestamp now = new Timestamp(new Date().getTime()); - String team = validateTeam(req.getFirstQueryParameter("team").get()); + JsonObject body = gson.fromJson(req.getReader(), JsonObject.class); + String team = validateTeam(body.get("team").getAsString()); if (team == null) { resp.setStatusCode(400); resp.getWriter().append("Invalid team specified."); @@ -131,7 +132,7 @@ public void service(HttpRequest req, HttpResponse resp) throws IOException, SQLE respContent.addProperty("tabCount", templateData.tabCount); respContent.addProperty("spaceCount", templateData.spaceCount); respContent.addProperty("recentVotes", gson.toJson(templateData.recentVotes)); - resp.getWriter().write(respContent.getAsString()); + resp.getWriter().write(respContent.toString()); resp.setStatusCode(HttpURLConnection.HTTP_OK); break; case "POST": diff --git a/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/functions/pom.xml b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/functions/pom.xml deleted file mode 100644 index 5e994325432..00000000000 --- a/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/functions/pom.xml +++ /dev/null @@ -1,60 +0,0 @@ - - - 4.0.0 - - com.example.cloud.functions - functions-cloud-sql-mysql - 1.0.0-SNAPSHOT - - 11 - 11 - - - - - - com.google.cloud.functions - functions-framework-api - 1.0.4 - provided - - - - - - - - com.google.cloud.functions - function-maven-plugin - 0.10.0 - - com.example.cloudsql.functions.Main - - - - - From 0c2ef0fa84b6f20a3c7e82618af86a5834748e93 Mon Sep 17 00:00:00 2001 From: Shubha Rajan Date: Tue, 26 Apr 2022 16:00:20 -0700 Subject: [PATCH 10/25] update instructions for deploying to Functions --- cloud-sql/mysql/servlet/.env.yaml | 7 +++++++ cloud-sql/mysql/servlet/README.md | 19 ++++++++++++++++++- .../cloudsql/ConnectionPoolFactory.java | 2 -- .../com/example/cloudsql/IndexServlet.java | 3 --- .../com/example/cloudsql/TemplateData.java | 1 - 5 files changed, 25 insertions(+), 7 deletions(-) create mode 100644 cloud-sql/mysql/servlet/.env.yaml diff --git a/cloud-sql/mysql/servlet/.env.yaml b/cloud-sql/mysql/servlet/.env.yaml new file mode 100644 index 00000000000..e6db43078f2 --- /dev/null +++ b/cloud-sql/mysql/servlet/.env.yaml @@ -0,0 +1,7 @@ +INSTANCE_CONNECTION_NAME: +INSTANCE_UNIX_SOCKET: +DB_USER: +DB_PASS: +DB_NAME: +INSTANCE_HOST: +INSTANCE_PORT: diff --git a/cloud-sql/mysql/servlet/README.md b/cloud-sql/mysql/servlet/README.md index e9ac4ad1ada..40a5756b496 100644 --- a/cloud-sql/mysql/servlet/README.md +++ b/cloud-sql/mysql/servlet/README.md @@ -96,13 +96,19 @@ and verify that has been added in your build section as a plugin. -### Development Server +### App Engine Development Server The following command will run the application locally in the the GAE-development server: ```bash mvn appengine:run ``` +### Cloud Functions Development Server +To run the application locally as a Cloud Function, run the following command: +``` +mvn function:run -Drun.functionTarget=com.example.cloudsql.functions.Main +``` + ### Deploy to Google App Engine First, update `src/main/webapp/WEB-INF/appengine-web.xml` with the correct values to pass the @@ -168,3 +174,14 @@ mvn clean package com.google.cloud.tools:jib-maven-plugin:2.8.0:build \ For more details about using Cloud Run see http://cloud.run. Review other [Java on Cloud Run samples](../../../run/). + +### Deploy to Google Cloud Functions + +To deploy the application to Cloud Functions, first fill in the values for required environment variables in `.env.yaml`. Then run the following command +``` +gcloud functions deploy mysql-sample \ + --trigger-http \ + --entry-point com.example.cloudsql.functions.Main \ + --runtime java11 \ + --env-vars-file .env.yaml +``` diff --git a/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/ConnectionPoolFactory.java b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/ConnectionPoolFactory.java index 9ec4732a7b4..dbd2f749c86 100644 --- a/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/ConnectionPoolFactory.java +++ b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/ConnectionPoolFactory.java @@ -17,9 +17,7 @@ package com.example.cloudsql; import com.zaxxer.hikari.HikariConfig; -import com.zaxxer.hikari.HikariDataSource; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; -import javax.sql.DataSource; public class ConnectionPoolFactory { 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 9d253add13d..e581d7677d3 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 @@ -20,12 +20,9 @@ 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 java.util.Locale; import java.util.logging.Level; import java.util.logging.Logger; diff --git a/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/TemplateData.java b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/TemplateData.java index 6190996042c..32cd9b92e76 100644 --- a/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/TemplateData.java +++ b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/TemplateData.java @@ -23,7 +23,6 @@ import java.sql.Timestamp; import java.util.ArrayList; import java.util.List; -import javax.servlet.ServletException; import javax.sql.DataSource; public class TemplateData { From dbf82e31e62ead1c70a0ef11fc9296d49e5c1aef Mon Sep 17 00:00:00 2001 From: Shubha Rajan Date: Wed, 27 Apr 2022 11:51:41 -0700 Subject: [PATCH 11/25] Update cloud-sql/mysql/servlet/.env.yaml Co-authored-by: Jack Wotherspoon --- cloud-sql/mysql/servlet/.env.yaml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/cloud-sql/mysql/servlet/.env.yaml b/cloud-sql/mysql/servlet/.env.yaml index e6db43078f2..7978d8d71b7 100644 --- a/cloud-sql/mysql/servlet/.env.yaml +++ b/cloud-sql/mysql/servlet/.env.yaml @@ -1,7 +1,7 @@ -INSTANCE_CONNECTION_NAME: -INSTANCE_UNIX_SOCKET: -DB_USER: -DB_PASS: -DB_NAME: -INSTANCE_HOST: -INSTANCE_PORT: +INSTANCE_CONNECTION_NAME: ::INSTANCE-NAME> +INSTANCE_UNIX_SOCKET: /cloudsql/::INSTANCE-NAME> +INSTANCE_HOST: '127.0.0.1' +DB_PORT: 3306 +DB_USER: +DB_PASS: +DB_NAME: From 2367b8eedb712b3022e2f6db4e8c752ce77bbf77 Mon Sep 17 00:00:00 2001 From: Shubha Rajan Date: Thu, 28 Apr 2022 13:29:35 -0700 Subject: [PATCH 12/25] add imports and environment variables to sample --- .../cloudsql/ConnectionPoolFactory.java | 4 ---- .../ConnectorConnectionPoolFactory.java | 19 +++++++---------- .../cloudsql/TcpConnectionPoolFactory.java | 21 +++++++------------ 3 files changed, 15 insertions(+), 29 deletions(-) diff --git a/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/ConnectionPoolFactory.java b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/ConnectionPoolFactory.java index dbd2f749c86..a9f51330483 100644 --- a/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/ConnectionPoolFactory.java +++ b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/ConnectionPoolFactory.java @@ -17,13 +17,9 @@ package com.example.cloudsql; import com.zaxxer.hikari.HikariConfig; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; public class ConnectionPoolFactory { - @SuppressFBWarnings( - value = "USBR_UNNECESSARY_STORE_BEFORE_RETURN", - justification = "Necessary for sample region tag.") public static HikariConfig configureConnectionPool(HikariConfig config) { // [START cloud_sql_mysql_servlet_limit] // maximumPoolSize limits the total number of concurrent connections this pool will keep. Ideal diff --git a/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/ConnectorConnectionPoolFactory.java b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/ConnectorConnectionPoolFactory.java index 11a72c4340b..f1067252573 100644 --- a/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/ConnectorConnectionPoolFactory.java +++ b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/ConnectorConnectionPoolFactory.java @@ -16,12 +16,13 @@ package com.example.cloudsql; +// [START cloud_sql_mysql_servlet_connect_connector] +// [START cloud_sql_mysql_servlet_connect_unix] + import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import javax.sql.DataSource; - public class ConnectorConnectionPoolFactory extends ConnectionPoolFactory { // Saving credentials in environment variables is convenient, but not secure - consider a more @@ -33,12 +34,8 @@ public class ConnectorConnectionPoolFactory extends ConnectionPoolFactory { private static final String DB_PASS = System.getenv("DB_PASS"); private static final String DB_NAME = System.getenv("DB_NAME"); - @SuppressFBWarnings( - value = "USBR_UNNECESSARY_STORE_BEFORE_RETURN", - justification = "Necessary for sample region tag.") public static DataSource createConnectionPool() { - // [START cloud_sql_mysql_servlet_connect_connector] - // [START cloud_sql_mysql_servlet_connect_unix] + // The configuration object specifies behaviors for the connection pool. HikariConfig config = new HikariConfig(); @@ -80,10 +77,8 @@ public static DataSource createConnectionPool() { // [END_EXCLUDE] // Initialize the connection pool using the configuration object. - DataSource pool = new HikariDataSource(config); - // [END cloud_sql_mysql_servlet_connect_connector] - // [END cloud_sql_mysql_servlet_connect_unix] - return pool; + return new HikariDataSource(config); } - } +// [END cloud_sql_mysql_servlet_connect_connector] +// [END cloud_sql_mysql_servlet_connect_unix] diff --git a/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/TcpConnectionPoolFactory.java b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/TcpConnectionPoolFactory.java index 94ca58fa9af..53e4775126b 100644 --- a/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/TcpConnectionPoolFactory.java +++ b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/TcpConnectionPoolFactory.java @@ -16,18 +16,15 @@ package com.example.cloudsql; +// [START cloud_sql_mysql_servlet_connect_tcp] +// [START cloud_sql_mysql_servlet_connect_tcp_sslcerts] + import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import javax.sql.DataSource; - public class TcpConnectionPoolFactory extends ConnectionPoolFactory { - @SuppressFBWarnings( - value = "USBR_UNNECESSARY_STORE_BEFORE_RETURN", - justification = "Necessary for sample region tag.") - // 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 DB_USER = System.getenv("DB_USER"); @@ -37,6 +34,7 @@ public class TcpConnectionPoolFactory extends ConnectionPoolFactory { private static final String INSTANCE_HOST = System.getenv("INSTANCE_HOST"); private static final String DB_PORT = System.getenv("DB_PORT"); + // [END cloud_sql_mysql_servlet_connect_tcp] private static final String TRUST_CERT_KEYSTORE_PATH = System.getenv( "TRUST_CERT_KEYSTORE_PATH"); private static final String TRUST_CERT_KEYSTORE_PASSWD = System.getenv( @@ -45,10 +43,9 @@ public class TcpConnectionPoolFactory extends ConnectionPoolFactory { "CLIENT_CERT_KEYSTORE_PATH"); private static final String CLIENT_CERT_KEYSTORE_PASSWD = System.getenv( "CLIENT_CERT_KEYSTORE_PASSWD"); + // [START cloud_sql_mysql_servlet_connect_tcp] public static DataSource createConnectionPool() { - // [START cloud_sql_mysql_servlet_connect_tcp] - // [START cloud_sql_mysql_servlet_connect_tcp_sslcerts] // The configuration object specifies behaviors for the connection pool. HikariConfig config = new HikariConfig(); @@ -87,10 +84,8 @@ public static DataSource createConnectionPool() { // [END_EXCLUDE] // Initialize the connection pool using the configuration object. - DataSource pool = new HikariDataSource(config); - // [END cloud_sql_mysql_servlet_connect_tcp] - // [END cloud_sql_mysql_servlet_connect_tcp_sslcerts] - return pool; + return new HikariDataSource(config); } - } +// [END cloud_sql_mysql_servlet_connect_tcp] +// [END cloud_sql_mysql_servlet_connect_tcp_sslcerts] From b51e07f128934116a2d60941908ed29af7417dbf Mon Sep 17 00:00:00 2001 From: Shubha Rajan Date: Thu, 28 Apr 2022 14:10:33 -0700 Subject: [PATCH 13/25] Update README.md --- cloud-sql/mysql/servlet/README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cloud-sql/mysql/servlet/README.md b/cloud-sql/mysql/servlet/README.md index 40a5756b496..915f0e13fdf 100644 --- a/cloud-sql/mysql/servlet/README.md +++ b/cloud-sql/mysql/servlet/README.md @@ -30,7 +30,9 @@ Note: Saving credentials in environment variables is convenient, but not secure secure solution such as [Cloud KMS](https://cloud.google.com/kms/) or [Secret Manager](https://cloud.google.com/secret-manager/) to help keep secrets safe. ## Configure SSL Certificates -For deployments that connect directly to a Cloud SQL instance with TCP, without using the Cloud SQL Proxy, configuring SSL certificates will ensure the connection is encrypted. +For deployments that connect directly to a Cloud SQL instance with TCP, +without using the Cloud SQL Proxy, +configuring SSL certificates will ensure the connection is encrypted. 1. Use the gcloud CLI to [download the server certificate](https://cloud.google.com/sql/docs/mysql/configure-ssl-instance#server-certs) for your Cloud SQL instance. - Get information about the service certificate: ``` From 0134a350fc1b33ff0ffc4dac1d6c6d026a846a80 Mon Sep 17 00:00:00 2001 From: Shubha Rajan Date: Thu, 28 Apr 2022 14:13:07 -0700 Subject: [PATCH 14/25] Update ConnectorConnectionPoolFactory.java --- .../example/cloudsql/ConnectorConnectionPoolFactory.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/ConnectorConnectionPoolFactory.java b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/ConnectorConnectionPoolFactory.java index f1067252573..c2b168a8ef8 100644 --- a/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/ConnectorConnectionPoolFactory.java +++ b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/ConnectorConnectionPoolFactory.java @@ -25,8 +25,10 @@ public class ConnectorConnectionPoolFactory extends ConnectionPoolFactory { - // 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. + // Note: Saving credentials in environment variables is convenient, but not + // secure - consider a more secure solution such as + // Cloud Secret Manager (https://cloud.google.com/secret-manager) to help + // keep secrets safe. private static final String INSTANCE_CONNECTION_NAME = System.getenv("INSTANCE_CONNECTION_NAME"); private static final String INSTANCE_UNIX_SOCKET = System.getenv("INSTANCE_UNIX_SOCKET"); From 9d992b304b42343951d3f83bf3f875781fd241d1 Mon Sep 17 00:00:00 2001 From: Shubha Rajan Date: Thu, 28 Apr 2022 17:01:56 -0700 Subject: [PATCH 15/25] formatting --- .../example/cloudsql/ConnectorConnectionPoolFactory.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/ConnectorConnectionPoolFactory.java b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/ConnectorConnectionPoolFactory.java index c2b168a8ef8..e239aa47e30 100644 --- a/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/ConnectorConnectionPoolFactory.java +++ b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/ConnectorConnectionPoolFactory.java @@ -26,9 +26,9 @@ public class ConnectorConnectionPoolFactory extends ConnectionPoolFactory { // Note: Saving credentials in environment variables is convenient, but not - // secure - consider a more secure solution such as - // Cloud Secret Manager (https://cloud.google.com/secret-manager) to help - // keep secrets safe. + // secure - consider a more secure solution such as + // Cloud Secret Manager (https://cloud.google.com/secret-manager) to help + // keep secrets safe. private static final String INSTANCE_CONNECTION_NAME = System.getenv("INSTANCE_CONNECTION_NAME"); private static final String INSTANCE_UNIX_SOCKET = System.getenv("INSTANCE_UNIX_SOCKET"); From 90016c28224fb32a729ba7d5415e0909d1d66013 Mon Sep 17 00:00:00 2001 From: Shubha Rajan Date: Fri, 29 Apr 2022 10:05:23 -0700 Subject: [PATCH 16/25] Update cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/ConnectorConnectionPoolFactory.java Co-authored-by: Kurtis Van Gent <31518063+kurtisvg@users.noreply.github.com> --- .../com/example/cloudsql/ConnectorConnectionPoolFactory.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/ConnectorConnectionPoolFactory.java b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/ConnectorConnectionPoolFactory.java index e239aa47e30..58aadcafe72 100644 --- a/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/ConnectorConnectionPoolFactory.java +++ b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/ConnectorConnectionPoolFactory.java @@ -59,7 +59,7 @@ public static DataSource createConnectionPool() { // Unix sockets are not natively supported in Java, so it is necessary to use the Cloud SQL JDBC // Socket Factory to connect. // Note: For Java users, the Cloud SQL JDBC Socket Factory can provide authenticated connections - // which is preferred to using the Cloud SQL Proxy with Unix sockets. + // which is usually preferable to using the Cloud SQL Proxy with Unix sockets. // See https://github.com/GoogleCloudPlatform/cloud-sql-jdbc-socket-factory for details. if (INSTANCE_UNIX_SOCKET != null) { config.addDataSourceProperty("unixSocketPath", INSTANCE_UNIX_SOCKET); From 8fc722f1ee2f35d3020b44663b3223d73319d5bb Mon Sep 17 00:00:00 2001 From: Shubha Rajan Date: Fri, 29 Apr 2022 10:08:33 -0700 Subject: [PATCH 17/25] Update TcpConnectionPoolFactory.java --- .../java/com/example/cloudsql/TcpConnectionPoolFactory.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/TcpConnectionPoolFactory.java b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/TcpConnectionPoolFactory.java index 53e4775126b..9b92c575207 100644 --- a/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/TcpConnectionPoolFactory.java +++ b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/TcpConnectionPoolFactory.java @@ -66,7 +66,7 @@ public static DataSource createConnectionPool() { // connection is encrypted. // See the link below for more information on how to configure SSL Certificates for use with // MySQL Connector/J - // + // https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-reference-using-ssl.html if (CLIENT_CERT_KEYSTORE_PATH != null && TRUST_CERT_KEYSTORE_PATH != null) { config.addDataSourceProperty("trustCertificateKeyStoreUrl", String.format("file:%s", TRUST_CERT_KEYSTORE_PATH)); From d78947c919a599f8ca54082109c26f0ca31e6a18 Mon Sep 17 00:00:00 2001 From: Shubha Rajan Date: Fri, 29 Apr 2022 10:08:47 -0700 Subject: [PATCH 18/25] Update cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/TemplateData.java Co-authored-by: Kurtis Van Gent <31518063+kurtisvg@users.noreply.github.com> --- .../servlet/src/main/java/com/example/cloudsql/TemplateData.java | 1 - 1 file changed, 1 deletion(-) diff --git a/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/TemplateData.java b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/TemplateData.java index 32cd9b92e76..693da207de4 100644 --- a/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/TemplateData.java +++ b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/TemplateData.java @@ -38,7 +38,6 @@ public TemplateData(int tabCount, int spaceCount, List recentVotes) { } public static TemplateData getTemplateData(DataSource pool) throws SQLException { - int tabCount = 0; int spaceCount = 0; List recentVotes = new ArrayList<>(); From dc809d5121b0fa62263f7d39634c94ae7b5d1a17 Mon Sep 17 00:00:00 2001 From: Shubha Rajan Date: Fri, 29 Apr 2022 10:49:33 -0700 Subject: [PATCH 19/25] Factor out common methods in servlet and cloud function --- .../ConnectionPoolContextListener.java | 16 +----- .../com/example/cloudsql/DatabaseSetup.java | 38 ++++++++++++++ .../com/example/cloudsql/IndexServlet.java | 16 +----- .../com/example/cloudsql/InputValidator.java | 37 +++++++++++++ .../com/example/cloudsql/functions/Main.java | 52 ++++++------------- 5 files changed, 92 insertions(+), 67 deletions(-) create mode 100644 cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/DatabaseSetup.java create mode 100644 cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/InputValidator.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 fb83fafb65a..21c385fef06 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 @@ -33,20 +33,6 @@ @WebListener("Creates a connection pool that is stored in the Servlet's context for later use.") public class ConnectionPoolContextListener implements ServletContextListener { - - private void createTable(DataSource pool) throws SQLException { - // Safely attempt to create the table schema. - try (Connection conn = pool.getConnection()) { - String stmt = - "CREATE TABLE IF NOT EXISTS votes ( " - + "vote_id SERIAL NOT NULL, time_cast timestamp NOT NULL, candidate CHAR(6) NOT NULL," - + " PRIMARY KEY (vote_id) );"; - try (PreparedStatement createTableStatement = conn.prepareStatement(stmt);) { - createTableStatement.execute(); - } - } - } - @Override public void contextDestroyed(ServletContextEvent event) { // This function is called when the Servlet is destroyed. @@ -71,7 +57,7 @@ public void contextInitialized(ServletContextEvent event) { servletContext.setAttribute("my-pool", pool); } try { - createTable(pool); + DatabaseSetup.createTable(pool); } catch (SQLException ex) { throw new RuntimeException( "Unable to verify table schema. Please double check the steps" diff --git a/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/DatabaseSetup.java b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/DatabaseSetup.java new file mode 100644 index 00000000000..3b58dd5379c --- /dev/null +++ b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/DatabaseSetup.java @@ -0,0 +1,38 @@ +/* + * Copyright 2022 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.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import javax.sql.DataSource; + +public class DatabaseSetup { + + public static void createTable(DataSource pool) throws SQLException { + // Safely attempt to create the table schema. + try (Connection conn = pool.getConnection()) { + String stmt = + "CREATE TABLE IF NOT EXISTS votes ( " + + "vote_id SERIAL NOT NULL, time_cast timestamp NOT NULL, candidate CHAR(6) NOT NULL," + + " PRIMARY KEY (vote_id) );"; + try (PreparedStatement createTableStatement = conn.prepareStatement(stmt);) { + createTableStatement.execute(); + } + } + } +} 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 e581d7677d3..68ee3962ad5 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 @@ -67,27 +67,13 @@ public void doGet(HttpServletRequest req, HttpServletResponse resp) req.getRequestDispatcher("/index.jsp").forward(req, resp); } - // Used to validate user input. All user provided data should be validated and sanitized before - // being used something like a SQL query. Returns null if invalid. - @Nullable - private String validateTeam(String input) { - if (input != null) { - input = input.toUpperCase(Locale.ENGLISH); - // Must be either "TABS" or "SPACES" - if (!"TABS".equals(input) && !"SPACES".equals(input)) { - return null; - } - } - return input; - } - @SuppressFBWarnings( value = {"SERVLET_PARAMETER", "XSS_SERVLET"}, justification = "Input is validated and sanitized.") @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 = validateTeam(req.getParameter("team")); + String team = InputValidator.validateTeam(req.getParameter("team")); Timestamp now = new Timestamp(new Date().getTime()); if (team == null) { resp.setStatus(400); diff --git a/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/InputValidator.java b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/InputValidator.java new file mode 100644 index 00000000000..065ee7b71d9 --- /dev/null +++ b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/InputValidator.java @@ -0,0 +1,37 @@ +/* + * Copyright 2022 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.util.Locale; +import javax.annotation.Nullable; + +public class InputValidator { + + // Used to validate user input. All user provided data should be validated and sanitized before + // being used something like a SQL query. Returns null if invalid. + @Nullable + public static String validateTeam(String input) { + if (input != null) { + input = input.toUpperCase(Locale.ENGLISH); + // Must be either "TABS" or "SPACES" + if (!"TABS".equals(input) && !"SPACES".equals(input)) { + return null; + } + } + return input; + } +} diff --git a/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/functions/Main.java b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/functions/Main.java index 6a850719491..f0d8c847da0 100644 --- a/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/functions/Main.java +++ b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/functions/Main.java @@ -17,6 +17,8 @@ package com.example.cloudsql.functions; import com.example.cloudsql.ConnectorConnectionPoolFactory; +import com.example.cloudsql.DatabaseSetup; +import com.example.cloudsql.InputValidator; import com.example.cloudsql.TcpConnectionPoolFactory; import com.example.cloudsql.TemplateData; import com.google.cloud.functions.HttpFunction; @@ -43,38 +45,23 @@ public class Main implements HttpFunction { private Logger logger = Logger.getLogger(Main.class.getName()); private static final Gson gson = new Gson(); - private void createTable(DataSource pool) throws SQLException { - // Safely attempt to create the table schema. - try (Connection conn = pool.getConnection()) { - String stmt = - "CREATE TABLE IF NOT EXISTS votes ( " - + "vote_id SERIAL NOT NULL, time_cast timestamp NOT NULL, candidate CHAR(6) NOT NULL," - + " PRIMARY KEY (vote_id) );"; - try (PreparedStatement createTableStatement = conn.prepareStatement(stmt);) { - createTableStatement.execute(); - } - } - } + private void returnVoteCounts(HttpRequest req, HttpResponse resp) + throws SQLException, IOException { + TemplateData templateData = TemplateData.getTemplateData(pool); + JsonObject respContent = new JsonObject(); - // Used to validate user input. All user provided data should be validated and sanitized before - // being used something like a SQL query. Returns null if invalid. - @Nullable - private String validateTeam(String input) { - if (input != null) { - input = input.toUpperCase(Locale.ENGLISH); - // Must be either "TABS" or "SPACES" - if (!"TABS".equals(input) && !"SPACES".equals(input)) { - return null; - } - } - return input; + // Return JSON Data + respContent.addProperty("tabCount", templateData.tabCount); + respContent.addProperty("spaceCount", templateData.spaceCount); + respContent.addProperty("recentVotes", gson.toJson(templateData.recentVotes)); + resp.getWriter().write(respContent.toString()); + resp.setStatusCode(HttpURLConnection.HTTP_OK); } - private void submitVote(HttpRequest req, HttpResponse resp) throws IOException { Timestamp now = new Timestamp(new Date().getTime()); JsonObject body = gson.fromJson(req.getReader(), JsonObject.class); - String team = validateTeam(body.get("team").getAsString()); + String team = InputValidator.validateTeam(body.get("team").getAsString()); if (team == null) { resp.setStatusCode(400); resp.getWriter().append("Invalid team specified."); @@ -112,7 +99,7 @@ public void service(HttpRequest req, HttpResponse resp) throws IOException, SQLE pool = ConnectorConnectionPoolFactory.createConnectionPool(); } try { - createTable(pool); + DatabaseSetup.createTable(pool); } catch (SQLException ex) { throw new RuntimeException( "Unable to verify table schema. Please double check the steps" @@ -125,15 +112,7 @@ public void service(HttpRequest req, HttpResponse resp) throws IOException, SQLE String method = req.getMethod(); switch (method) { case "GET": - TemplateData templateData = TemplateData.getTemplateData(pool); - JsonObject respContent = new JsonObject(); - - // Return JSON Data - respContent.addProperty("tabCount", templateData.tabCount); - respContent.addProperty("spaceCount", templateData.spaceCount); - respContent.addProperty("recentVotes", gson.toJson(templateData.recentVotes)); - resp.getWriter().write(respContent.toString()); - resp.setStatusCode(HttpURLConnection.HTTP_OK); + returnVoteCounts(req, resp); break; case "POST": submitVote(req, resp); @@ -143,6 +122,5 @@ public void service(HttpRequest req, HttpResponse resp) throws IOException, SQLE resp.getWriter().write(String.format("HTTP Method %s is not supported", method)); break; } - } } From ff96d056aa20782c518d45ff7e6f74aac80d8a45 Mon Sep 17 00:00:00 2001 From: Shubha Rajan Date: Fri, 29 Apr 2022 14:30:16 -0700 Subject: [PATCH 20/25] combine DatabaseSetup and InputValidator classes --- .../ConnectionPoolContextListener.java | 4 +- .../ConnectorConnectionPoolFactory.java | 7 ++-- .../com/example/cloudsql/IndexServlet.java | 4 +- .../com/example/cloudsql/InputValidator.java | 37 ------------------- .../{DatabaseSetup.java => Utils.java} | 20 +++++++++- .../com/example/cloudsql/functions/Main.java | 9 ++--- 6 files changed, 27 insertions(+), 54 deletions(-) delete mode 100644 cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/InputValidator.java rename cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/{DatabaseSetup.java => Utils.java} (69%) 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 21c385fef06..db3373071a0 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 @@ -18,8 +18,6 @@ import com.zaxxer.hikari.HikariDataSource; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; -import java.sql.Connection; -import java.sql.PreparedStatement; import java.sql.SQLException; import javax.servlet.ServletContext; import javax.servlet.ServletContextEvent; @@ -57,7 +55,7 @@ public void contextInitialized(ServletContextEvent event) { servletContext.setAttribute("my-pool", pool); } try { - DatabaseSetup.createTable(pool); + Utils.createTable(pool); } catch (SQLException ex) { throw new RuntimeException( "Unable to verify table schema. Please double check the steps" diff --git a/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/ConnectorConnectionPoolFactory.java b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/ConnectorConnectionPoolFactory.java index 58aadcafe72..1e8441c9279 100644 --- a/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/ConnectorConnectionPoolFactory.java +++ b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/ConnectorConnectionPoolFactory.java @@ -37,7 +37,6 @@ public class ConnectorConnectionPoolFactory extends ConnectionPoolFactory { private static final String DB_NAME = System.getenv("DB_NAME"); public static DataSource createConnectionPool() { - // The configuration object specifies behaviors for the connection pool. HikariConfig config = new HikariConfig(); @@ -56,9 +55,9 @@ public static DataSource createConnectionPool() { config.addDataSourceProperty("cloudSqlInstance", INSTANCE_CONNECTION_NAME); // [END cloud_sql_mysql_servlet_connect_connector] - // Unix sockets are not natively supported in Java, so it is necessary to use the Cloud SQL JDBC - // Socket Factory to connect. - // Note: For Java users, the Cloud SQL JDBC Socket Factory can provide authenticated connections + // Unix sockets are not natively supported in Java, so it is necessary to use the Cloud SQL + // Java Connector to connect. + // Note: For Java users, the Cloud SQL Java Connector can provide authenticated connections // which is usually preferable to using the Cloud SQL Proxy with Unix sockets. // See https://github.com/GoogleCloudPlatform/cloud-sql-jdbc-socket-factory for details. if (INSTANCE_UNIX_SOCKET != null) { 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 68ee3962ad5..6551d57e899 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 @@ -23,10 +23,8 @@ import java.sql.SQLException; import java.sql.Timestamp; import java.util.Date; -import java.util.Locale; import java.util.logging.Level; import java.util.logging.Logger; -import javax.annotation.Nullable; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; @@ -73,7 +71,7 @@ public void doGet(HttpServletRequest req, HttpServletResponse 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 = InputValidator.validateTeam(req.getParameter("team")); + String team = Utils.validateTeam(req.getParameter("team")); Timestamp now = new Timestamp(new Date().getTime()); if (team == null) { resp.setStatus(400); diff --git a/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/InputValidator.java b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/InputValidator.java deleted file mode 100644 index 065ee7b71d9..00000000000 --- a/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/InputValidator.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2022 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.util.Locale; -import javax.annotation.Nullable; - -public class InputValidator { - - // Used to validate user input. All user provided data should be validated and sanitized before - // being used something like a SQL query. Returns null if invalid. - @Nullable - public static String validateTeam(String input) { - if (input != null) { - input = input.toUpperCase(Locale.ENGLISH); - // Must be either "TABS" or "SPACES" - if (!"TABS".equals(input) && !"SPACES".equals(input)) { - return null; - } - } - return input; - } -} diff --git a/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/DatabaseSetup.java b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/Utils.java similarity index 69% rename from cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/DatabaseSetup.java rename to cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/Utils.java index 3b58dd5379c..a6da84573dd 100644 --- a/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/DatabaseSetup.java +++ b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/Utils.java @@ -19,9 +19,25 @@ import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; +import java.util.Locale; +import javax.annotation.Nullable; import javax.sql.DataSource; -public class DatabaseSetup { +public class Utils { + + // Used to validate user input. All user provided data should be validated and sanitized before + // being used something like a SQL query. Returns null if invalid. + @Nullable + public static String validateTeam(String input) { + if (input != null) { + input = input.toUpperCase(Locale.ENGLISH); + // Must be either "TABS" or "SPACES" + if (!"TABS".equals(input) && !"SPACES".equals(input)) { + return null; + } + } + return input; + } public static void createTable(DataSource pool) throws SQLException { // Safely attempt to create the table schema. @@ -35,4 +51,6 @@ public static void createTable(DataSource pool) throws SQLException { } } } + + } diff --git a/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/functions/Main.java b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/functions/Main.java index f0d8c847da0..3d70fadd335 100644 --- a/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/functions/Main.java +++ b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/functions/Main.java @@ -17,10 +17,9 @@ package com.example.cloudsql.functions; import com.example.cloudsql.ConnectorConnectionPoolFactory; -import com.example.cloudsql.DatabaseSetup; -import com.example.cloudsql.InputValidator; import com.example.cloudsql.TcpConnectionPoolFactory; import com.example.cloudsql.TemplateData; +import com.example.cloudsql.Utils; import com.google.cloud.functions.HttpFunction; import com.google.cloud.functions.HttpRequest; import com.google.cloud.functions.HttpResponse; @@ -33,10 +32,8 @@ import java.sql.SQLException; import java.sql.Timestamp; import java.util.Date; -import java.util.Locale; import java.util.logging.Level; import java.util.logging.Logger; -import javax.annotation.Nullable; import javax.sql.DataSource; public class Main implements HttpFunction { @@ -61,7 +58,7 @@ private void returnVoteCounts(HttpRequest req, HttpResponse resp) private void submitVote(HttpRequest req, HttpResponse resp) throws IOException { Timestamp now = new Timestamp(new Date().getTime()); JsonObject body = gson.fromJson(req.getReader(), JsonObject.class); - String team = InputValidator.validateTeam(body.get("team").getAsString()); + String team = Utils.validateTeam(body.get("team").getAsString()); if (team == null) { resp.setStatusCode(400); resp.getWriter().append("Invalid team specified."); @@ -99,7 +96,7 @@ public void service(HttpRequest req, HttpResponse resp) throws IOException, SQLE pool = ConnectorConnectionPoolFactory.createConnectionPool(); } try { - DatabaseSetup.createTable(pool); + Utils.createTable(pool); } catch (SQLException ex) { throw new RuntimeException( "Unable to verify table schema. Please double check the steps" From 77b0a55c6a1cdede861786b78e4742f88ae46227 Mon Sep 17 00:00:00 2001 From: Shubha Rajan Date: Fri, 29 Apr 2022 14:43:16 -0700 Subject: [PATCH 21/25] refactor cloud function to use initialization on demand --- .../com/example/cloudsql/functions/Main.java | 53 ++++++++++++------- 1 file changed, 35 insertions(+), 18 deletions(-) diff --git a/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/functions/Main.java b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/functions/Main.java index 3d70fadd335..f1876bc9699 100644 --- a/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/functions/Main.java +++ b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/functions/Main.java @@ -38,12 +38,45 @@ public class Main implements HttpFunction { - private DataSource pool; private Logger logger = Logger.getLogger(Main.class.getName()); private static final Gson gson = new Gson(); + // Declared at cold-start, but only initialized if/when the function executes + // Uses the "initialization-on-demand holder" idiom + // More information: https://en.wikipedia.org/wiki/Initialization-on-demand_holder_idiom + private static class PoolHolder { + // Making the default constructor private prohibits instantiation of this class + private PoolHolder() {} + + // This value is initialized only if (and when) the getInstance() function below is called + private static final DataSource INSTANCE = setupPool(); + + private static DataSource setupPool() { + DataSource pool; + if (System.getenv("INSTANCE_HOST") != null) { + pool = TcpConnectionPoolFactory.createConnectionPool(); + } else { + pool = ConnectorConnectionPoolFactory.createConnectionPool(); + } + try { + Utils.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); + } + return pool; + } + + private static DataSource getInstance() { + return PoolHolder.INSTANCE; + } + } + private void returnVoteCounts(HttpRequest req, HttpResponse resp) throws SQLException, IOException { + DataSource pool = PoolHolder.getInstance(); TemplateData templateData = TemplateData.getTemplateData(pool); JsonObject respContent = new JsonObject(); @@ -56,6 +89,7 @@ private void returnVoteCounts(HttpRequest req, HttpResponse resp) } private void submitVote(HttpRequest req, HttpResponse resp) throws IOException { + DataSource pool = PoolHolder.getInstance(); Timestamp now = new Timestamp(new Date().getTime()); JsonObject body = gson.fromJson(req.getReader(), JsonObject.class); String team = Utils.validateTeam(body.get("team").getAsString()); @@ -88,23 +122,6 @@ private void submitVote(HttpRequest req, HttpResponse resp) throws IOException { @Override public void service(HttpRequest req, HttpResponse resp) throws IOException, SQLException { - // lazily initialize pool and create table - if (pool == null) { - if (System.getenv("INSTANCE_HOST") != null) { - pool = TcpConnectionPoolFactory.createConnectionPool(); - } else { - pool = ConnectorConnectionPoolFactory.createConnectionPool(); - } - try { - Utils.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); - } - resp.setStatusCode(HttpURLConnection.HTTP_CREATED); - } String method = req.getMethod(); switch (method) { From e2e31833859e98948f7bc459b0b29b435759cc34 Mon Sep 17 00:00:00 2001 From: Shubha Rajan Date: Fri, 29 Apr 2022 14:43:43 -0700 Subject: [PATCH 22/25] formatting --- .../src/main/java/com/example/cloudsql/functions/Main.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/functions/Main.java b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/functions/Main.java index f1876bc9699..e42f4ce7128 100644 --- a/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/functions/Main.java +++ b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/functions/Main.java @@ -45,8 +45,10 @@ public class Main implements HttpFunction { // Uses the "initialization-on-demand holder" idiom // More information: https://en.wikipedia.org/wiki/Initialization-on-demand_holder_idiom private static class PoolHolder { + // Making the default constructor private prohibits instantiation of this class - private PoolHolder() {} + private PoolHolder() { + } // This value is initialized only if (and when) the getInstance() function below is called private static final DataSource INSTANCE = setupPool(); From eeebe2e0c820f6e27a93ff28291e01bdb14e5358 Mon Sep 17 00:00:00 2001 From: Shubha Rajan Date: Mon, 2 May 2022 09:27:17 -0700 Subject: [PATCH 23/25] Update ConnectorConnectionPoolFactory.java --- .../com/example/cloudsql/ConnectorConnectionPoolFactory.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/ConnectorConnectionPoolFactory.java b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/ConnectorConnectionPoolFactory.java index 1e8441c9279..45f1de4ba7a 100644 --- a/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/ConnectorConnectionPoolFactory.java +++ b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/ConnectorConnectionPoolFactory.java @@ -56,7 +56,8 @@ public static DataSource createConnectionPool() { // [END cloud_sql_mysql_servlet_connect_connector] // Unix sockets are not natively supported in Java, so it is necessary to use the Cloud SQL - // Java Connector to connect. + // Java Connector to connect. When setting INSTANCE_UNIX_SOCKET, the connector will + // call an external package that will enable Unix socket connections // Note: For Java users, the Cloud SQL Java Connector can provide authenticated connections // which is usually preferable to using the Cloud SQL Proxy with Unix sockets. // See https://github.com/GoogleCloudPlatform/cloud-sql-jdbc-socket-factory for details. From ba8e5c77117e891d2ab39387f3085cfa4de90132 Mon Sep 17 00:00:00 2001 From: Shubha Rajan Date: Mon, 2 May 2022 09:54:03 -0700 Subject: [PATCH 24/25] Update ConnectorConnectionPoolFactory.java --- .../com/example/cloudsql/ConnectorConnectionPoolFactory.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/ConnectorConnectionPoolFactory.java b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/ConnectorConnectionPoolFactory.java index 45f1de4ba7a..bc34c6c06cb 100644 --- a/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/ConnectorConnectionPoolFactory.java +++ b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/ConnectorConnectionPoolFactory.java @@ -57,7 +57,7 @@ public static DataSource createConnectionPool() { // [END cloud_sql_mysql_servlet_connect_connector] // Unix sockets are not natively supported in Java, so it is necessary to use the Cloud SQL // Java Connector to connect. When setting INSTANCE_UNIX_SOCKET, the connector will - // call an external package that will enable Unix socket connections + // call an external package that will enable Unix socket connections. // Note: For Java users, the Cloud SQL Java Connector can provide authenticated connections // which is usually preferable to using the Cloud SQL Proxy with Unix sockets. // See https://github.com/GoogleCloudPlatform/cloud-sql-jdbc-socket-factory for details. From b4b461c41b88a96292237719a417e2d1d8433c13 Mon Sep 17 00:00:00 2001 From: Shubha Rajan Date: Mon, 2 May 2022 11:30:49 -0700 Subject: [PATCH 25/25] Update TcpConnectionPoolFactory.java --- .../java/com/example/cloudsql/TcpConnectionPoolFactory.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/TcpConnectionPoolFactory.java b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/TcpConnectionPoolFactory.java index 9b92c575207..fec50bb8d8b 100644 --- a/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/TcpConnectionPoolFactory.java +++ b/cloud-sql/mysql/servlet/src/main/java/com/example/cloudsql/TcpConnectionPoolFactory.java @@ -26,7 +26,7 @@ public class TcpConnectionPoolFactory extends ConnectionPoolFactory { // 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. + // secure solution such as https://cloud.google.com/secret-manager/ to help keep secrets safe. 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");