diff --git a/appengine-j8-start/cloudsql/README.md b/appengine-j8-start/cloudsql/README.md new file mode 100644 index 00000000000..3839b8a6b65 --- /dev/null +++ b/appengine-j8-start/cloudsql/README.md @@ -0,0 +1,72 @@ +# Google App Engine Standard Environment Cloud SQL Sample + +This sample demonstrates how to deploy an App Engine Java 8 application that +uses Cloud SQL for storage. + +See the [Google App Engine standard environment documentation][ae-docs] for more +detailed instructions. + +[ae-docs]: https://cloud.google.com/appengine/docs/java/ + +## Setup + +* If you haven't already, Download and initialize the [Cloud + SDK](https://cloud.google.com/sdk/) + + `gcloud init` + +* If you haven't already, Create an App Engine app within the current Google + Cloud Project + + `gcloud app create` + +* If you haven't already, Setup [Application Default + Credentials](https://developers.google.com/identity/protocols/application-default-credentials) + + `gcloud auth application-default login` + +* [Create an + instance](https://cloud.google.com/sql/docs/mysql/create-instance) + +* [Create a + Database](https://cloud.google.com/sql/docs/mysql/create-manage-databases) + +* [Create a user](https://cloud.google.com/sql/docs/mysql/create-manage-users) + +* Note the **Instance connection name** under Overview > properties + +* Update the `` tag in the `pom.xml` with your project name. + +* Update the `` tag in the `pom.xml` with your version name. + +## Testing + +This examples uses a local MySQL server to run tests. + +1. Download and install [MySQL Community + Server](https://dev.mysql.com/downloads/mysql/). + +1. Create the database and user for the tests. + +1. Append the database name, username and password in the `serverUrl` + connection variable in the test files located in + `src/test/java/com/example/appengine`. + +## Running locally + +This example uses the [Cloud SDK Maven +plugin](https://cloud.google.com/appengine/docs/java/tools/using-maven). To run +this sample locally: + + $ mvn appengine:run + +To see the results of the sample application, open +[localhost:8080](http://localhost:8080) in a web browser. + +## Deploying + +In the following command, replace YOUR-PROJECT-ID with your [Google Cloud +Project ID](https://developers.google.com/console/help/new/#projectnumber) and +SOME-VERSION with a valid version number. + + $ mvn appengine:deploy diff --git a/appengine-j8-start/cloudsql/pom.xml b/appengine-j8-start/cloudsql/pom.xml new file mode 100644 index 00000000000..77c0f0a67ac --- /dev/null +++ b/appengine-j8-start/cloudsql/pom.xml @@ -0,0 +1,128 @@ + + + 4.0.0 + war + 1.0 + com.example.appengine + cloudsql + + + stuff-155523 + 1 + 1.9.57 + + UTF-8 + UTF-8 + + 1.8 + 1.8 + + PROJECT INSTANCE NAME + DATABASE USERNAME + DATABASE PASSWORD + DATABASE NAME + false + + + + + + javax.servlet + javax.servlet-api + 3.1.0 + jar + provided + + + + jstl + jstl + 1.2 + + + + taglibs + standard + 1.1.2 + + + + com.google.appengine + appengine-api-1.0-sdk + ${appengine.sdk.version} + + + + org.jsoup + jsoup + 1.10.3 + + + + + + junit + junit + 4.12 + + + + com.google.truth + truth + 0.36 + + + + mysql + mysql-connector-java + 5.1.42 + + + + com.google.cloud.sql + mysql-socket-factory + 1.0.4 + + + + + + + ${project.build.directory}/${project.build.finalName}/WEB-INF/classes + + + + org.apache.maven.plugins + 3.5.1 + maven-compiler-plugin + + + + com.google.cloud.tools + appengine-maven-plugin + 1.3.1 + + ${appengine.app.appId} + ${appengine.app.version} + PATH TO CLOUD SDK DIRECTORY + + + + + + + diff --git a/appengine-j8-start/cloudsql/src/main/java/com/example/appengine/cloudsql/CreateRecord.java b/appengine-j8-start/cloudsql/src/main/java/com/example/appengine/cloudsql/CreateRecord.java new file mode 100644 index 00000000000..fd3505596ed --- /dev/null +++ b/appengine-j8-start/cloudsql/src/main/java/com/example/appengine/cloudsql/CreateRecord.java @@ -0,0 +1,118 @@ +/** + * Copyright 2017 Google Inc. + * + *

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + *

http://www.apache.org/licenses/LICENSE-2.0 + * + *

Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.example.appengine.cloudsql; + +import java.io.IOException; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.util.Date; +import java.util.Map; +import java.util.stream.Collectors; +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.jsoup.Jsoup; +import org.jsoup.safety.Whitelist; + +@SuppressWarnings("serial") +@WebServlet(name = "create", description = "Write post content to SQL DB", urlPatterns = "/create") +public class CreateRecord extends HttpServlet { + // Creates a record from data sent in a HTML form and stores it in Cloud SQL + + Connection conn; + + // Table creation queries + final String createContentTableSql = + "CREATE TABLE IF NOT EXISTS posts ( post_id INT NOT NULL " + + "AUTO_INCREMENT, author_id INT NOT NULL, timestamp DATETIME NOT NULL, " + + "title VARCHAR(256) NOT NULL, " + + "body VARCHAR(1337) NOT NULL, PRIMARY KEY (post_id) )"; + + final String createUserTableSql = + "CREATE TABLE IF NOT EXISTS users ( user_id INT NOT NULL " + + "AUTO_INCREMENT, user_fullname VARCHAR(64) NOT NULL, " + + "PRIMARY KEY (user_id) )"; + + // Post creation query + final String createPostSql = + "INSERT INTO posts (author_id, timestamp, title, body) VALUES (?, ?, ?, ?)"; + + @Override + public void doPost(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + + // Take the values submitted in an HTML form, clean them through jSoup, store + // them in a Cloud SQL database and then send the user to a JSP that has a + // personalised confirmation message. + + // Create a map of the httpParameters that we want and run it through jSoup + Map blogContent = + req.getParameterMap() + .entrySet() + .stream() + .filter(a -> a.getKey().startsWith("blogContent_")) + .collect( + Collectors.toMap( + p -> p.getKey(), p -> Jsoup.clean(p.getValue()[0], Whitelist.basic()))); + + // Build the SQL command to insert the blog post into the database + try (PreparedStatement statementCreatePost = conn.prepareStatement(createPostSql)) { + statementCreatePost.setInt( + 1, + Integer.parseInt( + blogContent.get( + "blogContent_id"))); // set the author to the user ID from the user table + statementCreatePost.setTimestamp(2, new Timestamp(new Date().getTime())); + statementCreatePost.setString(3, blogContent.get("blogContent_title")); + statementCreatePost.setString(4, blogContent.get("blogContent_description")); + statementCreatePost.executeUpdate(); + + conn.close(); // close the connection to the MySQL server + + // Send the user to the confirmation page with personalised confirmation text + String confirmation = "Post with title " + blogContent.get("blogContent_title") + " created."; + + req.setAttribute("confirmation", confirmation); + req.getRequestDispatcher("/confirm.jsp").forward(req, resp); + + } catch (SQLException e) { + throw new ServletException("SQL error when creating post", e); + } + } + + @Override + public void init() throws ServletException { + try { + String url = System.getProperty("cloudsql"); + + try { + conn = DriverManager.getConnection(url); // Connect to the database + + // Create the tables for first use + conn.createStatement().executeUpdate(createContentTableSql); // Create content table + conn.createStatement().executeUpdate(createUserTableSql); // Create user table + } catch (SQLException e) { + throw new ServletException("Unable to connect to Cloud SQL", e); + } + + } finally { + // Nothing really to do here. + } + } +} diff --git a/appengine-j8-start/cloudsql/src/main/java/com/example/appengine/cloudsql/DeleteRecords.java b/appengine-j8-start/cloudsql/src/main/java/com/example/appengine/cloudsql/DeleteRecords.java new file mode 100644 index 00000000000..98546e64de5 --- /dev/null +++ b/appengine-j8-start/cloudsql/src/main/java/com/example/appengine/cloudsql/DeleteRecords.java @@ -0,0 +1,80 @@ +/** + * Copyright 2017 Google Inc. + * + *

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + *

http://www.apache.org/licenses/LICENSE-2.0 + * + *

Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.example.appengine.cloudsql; + +import java.io.IOException; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.Base64; +import java.util.Map; +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +@SuppressWarnings("serial") +@WebServlet(name = "delete", description = "List the latest news posts", urlPatterns = "/delete") +public class DeleteRecords extends HttpServlet { + + Connection conn; + + // Delete query + final String deleteSql = "DELETE FROM posts WHERE post_id = ?"; + + @Override + public void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + // Delete the record for a given ID + + Map userData = req.getParameterMap(); + + String[] postId = userData.get("id"); + String decodedId = + new String(Base64.getUrlDecoder().decode(postId[0])); // decode the websafe ID + + try (PreparedStatement statementDeletePost = conn.prepareStatement(deleteSql)) { + statementDeletePost.setString(1, decodedId); + statementDeletePost.executeUpdate(); + + conn.close(); // Close the connection to the MySQL server + + final String confirmation = "Post ID " + decodedId + " has been deleted."; + + req.setAttribute("confirmation", confirmation); + req.getRequestDispatcher("/confirm.jsp").forward(req, resp); + + } catch (SQLException e) { + throw new ServletException("SQL error", e); + } + } + + @Override + public void init() throws ServletException { + try { + String url = System.getProperty("cloudsql"); + + try { + conn = DriverManager.getConnection(url); + } catch (SQLException e) { + throw new ServletException("Unable to connect to Cloud SQL", e); + } + + } finally { + // Nothing really to do here. + } + } +} diff --git a/appengine-j8-start/cloudsql/src/main/java/com/example/appengine/cloudsql/ListResults.java b/appengine-j8-start/cloudsql/src/main/java/com/example/appengine/cloudsql/ListResults.java new file mode 100644 index 00000000000..a52b5c54170 --- /dev/null +++ b/appengine-j8-start/cloudsql/src/main/java/com/example/appengine/cloudsql/ListResults.java @@ -0,0 +1,138 @@ +/** + * Copyright 2017 Google Inc. + * + *

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + *

http://www.apache.org/licenses/LICENSE-2.0 + * + *

Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.example.appengine.cloudsql; + +import java.io.IOException; +import java.io.PrintWriter; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +@SuppressWarnings("serial") +@WebServlet(name = "list", description = "List the latest news posts", urlPatterns = "/") +public class ListResults extends HttpServlet { + // Displays the stored blog posts + + Connection conn; // DB Connection object + + // Table creation queries + final String createContentTableSql = + "CREATE TABLE IF NOT EXISTS posts ( post_id INT NOT NULL " + + "AUTO_INCREMENT, author_id INT NOT NULL, timestamp DATETIME NOT NULL, " + + "title VARCHAR(256) NOT NULL, " + + "body VARCHAR(1337) NOT NULL, PRIMARY KEY (post_id) )"; + + final String createUserTableSql = + "CREATE TABLE IF NOT EXISTS users ( user_id INT NOT NULL " + + "AUTO_INCREMENT, user_fullname VARCHAR(64) NOT NULL, " + + "PRIMARY KEY (user_id) )"; + + // Create a test user + final String createTestUserSql = "INSERT INTO users (user_fullname) VALUES ('Test User')"; + + // Blog post retrieval queries + final String selectSql = + "SELECT posts.post_id, users.user_fullname, posts.timestamp, posts.title, posts.body FROM posts, users " + + "WHERE (posts.author_id = users.user_id) AND (posts.body != \"\") ORDER BY posts.post_id DESC"; + + // Preformatted HTML + String headers = + "

Welcome to the App Engine Blog

Add a new post

"; + String blogPostDisplayFormat = + "

%s

Posted at: %s by %s [update] | [delete]

%s

"; + + @Override + public void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + // Retrieve blog posts from Cloud SQL database and display them + + PrintWriter out = resp.getWriter(); + + out.println(headers); // Print HTML headers + + try (ResultSet rs = conn.prepareStatement(selectSql).executeQuery()) { + Map> storedPosts = new HashMap<>(); + + while (rs.next()) { + Map blogPostContents = new HashMap<>(); + + // Store the particulars for a blog in a map + blogPostContents.put("author", rs.getString("users.user_fullname")); + blogPostContents.put("title", rs.getString("posts.title")); + blogPostContents.put("body", rs.getString("posts.body")); + blogPostContents.put("publishTime", rs.getString("posts.timestamp")); + + // Store the post in a map with key of the postId + storedPosts.put(rs.getInt("posts.post_id"), blogPostContents); + } + + // Iterate the map and display each record's contents on screen + storedPosts.forEach( + (k, v) -> { + // Encode the ID into a websafe string + String encodedID = Base64.getUrlEncoder().encodeToString(String.valueOf(k).getBytes()); + + // Build up string with values from Cloud SQL + String recordOutput = + String.format( + blogPostDisplayFormat, + v.get("title"), + v.get("publishTime"), + v.get("author"), + encodedID, + encodedID, + v.get("body")); + + out.println(recordOutput); // print out the HTML + }); + + } catch (SQLException e) { + throw new ServletException("SQL error", e); + } + } + + @Override + public void init() throws ServletException { + try { + String url = System.getProperty("cloudsql"); + + try { + conn = DriverManager.getConnection(url); + + // Create the tables so that the SELECT query doesn't throw an exception + // if the user visits the page before any posts have been added + + conn.createStatement().executeUpdate(createContentTableSql); // create content table + conn.createStatement().executeUpdate(createUserTableSql); // create user table + + // Create a test user + conn.createStatement().executeUpdate(createTestUserSql); + } catch (SQLException e) { + throw new ServletException("Unable to connect to SQL server", e); + } + + } finally { + // Nothing really to do here. + } + } +} diff --git a/appengine-j8-start/cloudsql/src/main/java/com/example/appengine/cloudsql/PostForm.java b/appengine-j8-start/cloudsql/src/main/java/com/example/appengine/cloudsql/PostForm.java new file mode 100644 index 00000000000..45186761ad7 --- /dev/null +++ b/appengine-j8-start/cloudsql/src/main/java/com/example/appengine/cloudsql/PostForm.java @@ -0,0 +1,76 @@ +/** + * Copyright 2017 Google Inc. + * + *

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + *

http://www.apache.org/licenses/LICENSE-2.0 + * + *

Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.example.appengine.cloudsql; + +import java.io.IOException; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.HashMap; +import java.util.Map; +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +@SuppressWarnings("serial") +@WebServlet(name = "FormServlet", description = "List the latest news posts", value = "/blogpost") +public class PostForm extends HttpServlet { + + Connection conn; + + final String getUserId = "SELECT user_id, user_fullname FROM users"; + Map users = new HashMap(); + + @Override + public void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + // Display users' full name for the blog creation form + // Send a Map of user IDs and full names to the HTML form + // Find the user ID from the full name + + try (ResultSet rs = conn.prepareStatement(getUserId).executeQuery()) { + while (rs.next()) { + users.put(rs.getInt("user_id"), rs.getString("user_fullname")); + } + + conn.close(); // close the database connection + + req.setAttribute("users", users); + req.getRequestDispatcher("/form.jsp") + .forward(req, resp); // Send the map of tuples to the update page + + } catch (SQLException e) { + throw new ServletException("SQL error", e); + } + } + + @Override + public void init() throws ServletException { + try { + String url = System.getProperty("cloudsql"); + + try { + conn = DriverManager.getConnection(url); + } catch (SQLException e) { + throw new ServletException("Unable to connect to Cloud SQL", e); + } + + } finally { + // Nothing really to do here. + } + } +} diff --git a/appengine-j8-start/cloudsql/src/main/java/com/example/appengine/cloudsql/UpdateRecords.java b/appengine-j8-start/cloudsql/src/main/java/com/example/appengine/cloudsql/UpdateRecords.java new file mode 100644 index 00000000000..d82383a43cd --- /dev/null +++ b/appengine-j8-start/cloudsql/src/main/java/com/example/appengine/cloudsql/UpdateRecords.java @@ -0,0 +1,131 @@ +/** + * Copyright 2017 Google Inc. + * + *

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + *

http://www.apache.org/licenses/LICENSE-2.0 + * + *

Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.example.appengine.cloudsql; + +import java.io.IOException; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Base64; +import java.util.Map; +import java.util.stream.Collectors; +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.jsoup.Jsoup; +import org.jsoup.safety.Whitelist; + +@SuppressWarnings("serial") +@WebServlet(name = "update", description = "Update a post", urlPatterns = "/update") +public class UpdateRecords extends HttpServlet { + // Update an existing blog post + // doGet() - retrieves the existing post data sends it to the input form + // doPost() - stores updated blog post in Cloud SQL + + Connection conn; + + // SQL commands + final String selectSql = + "SELECT posts.title, posts.body, posts.author_id FROM posts WHERE post_id = ? LIMIT 1"; + final String updateSql = "UPDATE posts SET title = ?, body = ? WHERE post_id = ?"; + + @Override + public void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + // Retrieve the existing post data and send it to the update form + + Map userData = req.getParameterMap(); + String[] postId = userData.get("id"); // Grab the websafe ID + String decodedId = + new String(Base64.getUrlDecoder().decode(postId[0])); // Decode the websafe ID + + try (PreparedStatement statementSelectPost = conn.prepareStatement(selectSql)) { + statementSelectPost.setString(1, decodedId); // Include the decoded ID in the query + ResultSet rs = statementSelectPost.executeQuery(); // Retrieve the post + rs.next(); // Move the cursor + + // Build out the query with user submitted data + req.setAttribute("title", rs.getString("title")); + req.setAttribute("body", rs.getString("body")); + req.setAttribute("author", rs.getString("author_id")); + req.setAttribute("id", decodedId); + + // Page formatting + final String pageTitle = "Updating blog post"; + req.setAttribute("pagetitle", pageTitle); + + req.getRequestDispatcher("/form.jsp") + .forward(req, resp); // Send the user to the confirmation page + + } catch (SQLException e) { + throw new ServletException("SQL error", e); + } + } + + @Override + public void doPost(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + // Store the updated post + + // Create a map of the httpParameters that we want and run it through jSoup + // jSoup is used to santise the content + Map blogContent = + req.getParameterMap() + .entrySet() + .stream() + .filter(a -> a.getKey().startsWith("blogContent_")) + .collect( + Collectors.toMap( + p -> p.getKey(), p -> Jsoup.clean(p.getValue()[0], Whitelist.basic()))); + + try (PreparedStatement statementUpdatePost = conn.prepareStatement(updateSql)) { + // Build out the query with dat// Build out the query with dataa + statementUpdatePost.setString(1, blogContent.get("blogContent_title")); + statementUpdatePost.setString(2, blogContent.get("blogContent_description")); + statementUpdatePost.setString(3, blogContent.get("blogContent_id")); + statementUpdatePost.executeUpdate(); + + conn.close(); // Close the connection to the MySQL server + + final String confirmation = + "Blog post " + blogContent.get("blogContent_title") + " has been updated"; + + req.setAttribute("confirmation", confirmation); + req.getRequestDispatcher("/confirm.jsp").forward(req, resp); + + } catch (SQLException e) { + throw new ServletException("SQL error", e); + } + } + + @Override + public void init() throws ServletException { + try { + String url = System.getProperty("cloudsql"); + + try { + conn = DriverManager.getConnection(url); + } catch (SQLException e) { + throw new ServletException("Unable to connect to Cloud SQL", e); + } + + } finally { + // Nothing really to do here. + } + } +} diff --git a/appengine-j8-start/cloudsql/src/main/webapp/WEB-INF/appengine-web.xml b/appengine-j8-start/cloudsql/src/main/webapp/WEB-INF/appengine-web.xml new file mode 100644 index 00000000000..7c92ec4ad01 --- /dev/null +++ b/appengine-j8-start/cloudsql/src/main/webapp/WEB-INF/appengine-web.xml @@ -0,0 +1,29 @@ + + + + + true + java8 + + true + + + + + + + + + + diff --git a/appengine-j8-start/cloudsql/src/main/webapp/WEB-INF/web.xml b/appengine-j8-start/cloudsql/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000000..e755940a0ff --- /dev/null +++ b/appengine-j8-start/cloudsql/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,28 @@ + + + + + + blog + /* + + + CONFIDENTIAL + + + diff --git a/appengine-j8-start/cloudsql/src/main/webapp/confirm.jsp b/appengine-j8-start/cloudsql/src/main/webapp/confirm.jsp new file mode 100644 index 00000000000..9c0d0e021ba --- /dev/null +++ b/appengine-j8-start/cloudsql/src/main/webapp/confirm.jsp @@ -0,0 +1,23 @@ +<%-- +Copyright 2017 Google Inc. + +

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. +--%> + + + + + Database confirmation + + +

${confirmation}

+ + diff --git a/appengine-j8-start/cloudsql/src/main/webapp/form.jsp b/appengine-j8-start/cloudsql/src/main/webapp/form.jsp new file mode 100644 index 00000000000..5098bdffdd7 --- /dev/null +++ b/appengine-j8-start/cloudsql/src/main/webapp/form.jsp @@ -0,0 +1,51 @@ +<%-- +Copyright 2017 Google Inc. + +

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. +--%> + + +<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> +<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn"%> +

+ + +

Create a new blog post

+
+
+ + +
+ + +

+ + +
+ + +
+ + +
+ +
+ + +
+ + +
+
diff --git a/appengine-j8-start/cloudsql/src/main/webapp/index.jsp b/appengine-j8-start/cloudsql/src/main/webapp/index.jsp new file mode 100644 index 00000000000..252b94935cc --- /dev/null +++ b/appengine-j8-start/cloudsql/src/main/webapp/index.jsp @@ -0,0 +1,32 @@ +<%-- +Copyright 2017 Google Inc. + +

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. +--%> + +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> + + + + The App Engine Blog + + +

Welcome to the App Engine Blog

+ + +

+

Posted at: by

+ +

[update] | [delete]

+
+
+ + diff --git a/appengine-j8-start/cloudsql/src/test/java/com/example/appengine/CreateRecordTest.java b/appengine-j8-start/cloudsql/src/test/java/com/example/appengine/CreateRecordTest.java new file mode 100644 index 00000000000..b85ec9bdb5e --- /dev/null +++ b/appengine-j8-start/cloudsql/src/test/java/com/example/appengine/CreateRecordTest.java @@ -0,0 +1,139 @@ +/* + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.appengine.cloudsql; + +import static com.google.common.truth.Truth.assertThat; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.Timestamp; +import java.util.Date; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@link CreateRecords}. */ +@RunWith(JUnit4.class) +public final class CreateRecordTest { + + Connection conn; + + // MySQL server connection URL - use localhost for testing + // Format: jdbc:mysql://localhost/DATABASE_NAME?useSSL=false&user=USERNAME&password=PASSWORD + final String serverUrl = + "jdbc:mysql://localhost/DATABASE_NAME?useSSL=false&user=USERNAME&password=PASSWORD"; + + // Table creation SQL commands + final String createContentTableSql = + "CREATE TABLE IF NOT EXISTS posts ( post_id INT NOT NULL " + + "AUTO_INCREMENT, author_id INT NOT NULL, timestamp DATETIME NOT NULL, " + + "title VARCHAR(256) NOT NULL, " + + "body VARCHAR(1337) NOT NULL, PRIMARY KEY (post_id) )"; + + final String createUserTableSql = + "CREATE TABLE IF NOT EXISTS users ( user_id INT NOT NULL " + + "AUTO_INCREMENT, user_fullname VARCHAR(64) NOT NULL, " + + "PRIMARY KEY (user_id) )"; + + // Record SQL commands + final String createPostSql = + "INSERT INTO posts (author_id, timestamp, title, body) VALUES (?, ?, ?, ?)"; + + final String selectSql = + "SELECT posts.post_id, users.user_fullname, posts.title, posts.body FROM posts, users WHERE (posts.author_id = users.user_id) AND (posts.body != \"\") ORDER BY posts.post_id ASC"; + + // User creation SQL command + final String createUserSql = "INSERT INTO users (user_id, user_fullname) VALUES (?, ?)"; + + // Drop table commands + final String dropContentTableSql = "DROP TABLE posts"; + + final String dropUserTableSql = "DROP TABLE users"; + + @Before + public void setUp() throws Exception { + // Connect to the MySQL server for testing and create the databases + + try { + conn = DriverManager.getConnection(serverUrl); + } catch (Exception e) { + throw new Exception("Unable to connect to Cloud SQL", e); + } + + // Create tables if required + conn.createStatement().executeUpdate(createContentTableSql); // create content table + conn.createStatement().executeUpdate(createUserTableSql); // create user table + } + + @After + public void tearDown() throws Exception { + + try { + // Drop created tables + PreparedStatement dropContentStatement = conn.prepareStatement(dropContentTableSql); + PreparedStatement dropUserStatement = conn.prepareStatement(dropUserTableSql); + + dropContentStatement.executeUpdate(); + dropUserStatement.executeUpdate(); + + conn.close(); // close the database connection + } catch (Exception e) { + throw new Exception("Unable to drop tables and close MySQL connection", e); + } + } + + @Test + public void createTest() throws Exception { + /* Retrieve records from Cloud SQL */ + + // Setup phase + + // Create a test author + PreparedStatement userInsert = conn.prepareStatement(createUserSql); + + userInsert.setInt(1, 1); + userInsert.setString(2, "Test User"); + + // Create four records and store them in Cloud SQL + PreparedStatement postStatement = conn.prepareStatement(createPostSql); + + postStatement.setInt(1, 1); + postStatement.setTimestamp(2, new Timestamp(new Date().getTime())); + postStatement.setString(3, "Post 1 title"); + postStatement.setString(4, "Post 1 content"); + + // Excute phase + + userInsert.executeUpdate(); // store the new user + postStatement.executeUpdate(); // store the prepared queries + + ResultSet rs = + conn.prepareStatement(selectSql) + .executeQuery(); // retrieve the posts and put them into a ResultSet for iteration + + // Check phase + + rs.next(); // initialise the cursor + assertThat((String) rs.getString("users.user_fullname")).isEqualTo("Test User"); + assertThat((String) rs.getString("posts.title")).isEqualTo("Post 1 title"); + assertThat((String) rs.getString("posts.body")).isEqualTo("Post 1 content"); + } +} diff --git a/appengine-j8-start/cloudsql/src/test/java/com/example/appengine/DeleteRecordsTest.java b/appengine-j8-start/cloudsql/src/test/java/com/example/appengine/DeleteRecordsTest.java new file mode 100644 index 00000000000..777d89001b8 --- /dev/null +++ b/appengine-j8-start/cloudsql/src/test/java/com/example/appengine/DeleteRecordsTest.java @@ -0,0 +1,197 @@ +/* + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.appengine.cloudsql; + +import static com.google.common.truth.Truth.assertThat; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.Timestamp; +import java.util.Base64; +import java.util.Date; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/* Unit tests for {@link DeleteRecords}. */ +@RunWith(JUnit4.class) +public final class DeleteRecordsTest { + + Connection conn; + + // MySQL server connection URL - use localhost for testing + // Format: jdbc:mysql://localhost/DATABASE_NAME?useSSL=false&user=USERNAME&password=PASSWORD + final String serverUrl = + "jdbc:mysql://localhost/DATABASE?useSSL=false&user=USERNAME&password=PASSWORD"; + + // Table creation SQL commands + final String createContentTableSql = + "CREATE TABLE IF NOT EXISTS posts ( post_id INT NOT NULL " + + "AUTO_INCREMENT, author_id INT NOT NULL, timestamp DATETIME NOT NULL, " + + "title VARCHAR(256) NOT NULL, " + + "body VARCHAR(1337) NOT NULL, PRIMARY KEY (post_id) )"; + + final String createUserTableSql = + "CREATE TABLE IF NOT EXISTS users ( user_id INT NOT NULL " + + "AUTO_INCREMENT, user_fullname VARCHAR(64) NOT NULL, " + + "PRIMARY KEY (user_id) )"; + + // Record SQL commands + final String createPostSql = + "INSERT INTO posts (post_id, author_id, timestamp, title, body) VALUES (?, ?, ?, ?, ?)"; + + final String countSelectSql = "SELECT COUNT(*) as total FROM posts"; + + // User creation SQL command + final String createUserSql = "INSERT INTO users (user_id, user_fullname) VALUES (?, ?)"; + + // Delete record SQL command + final String deleteRecordSql = "DELETE FROM posts WHERE post_id = ?"; + + // Drop table commands + final String dropContentTableSql = "DROP TABLE posts"; + + final String dropUserTableSql = "DROP TABLE users"; + + @Before + public void setUp() throws Exception { + // Connect to the MySQL server for testing and create the databases + + try { + conn = DriverManager.getConnection(serverUrl); + + // Create tables + conn.createStatement().executeUpdate(createContentTableSql); // create content table + conn.createStatement().executeUpdate(createUserTableSql); // create user table + } catch (Exception e) { + throw new Exception("Unable to connect to Cloud SQL", e); + } + } + + @After + public void tearDown() throws Exception { + // Drop created tables + + try { + PreparedStatement dropContentStatement = conn.prepareStatement(dropContentTableSql); + PreparedStatement dropUserStatement = conn.prepareStatement(dropUserTableSql); + + dropContentStatement.executeUpdate(); + dropUserStatement.executeUpdate(); + + conn.close(); // close the database connection + } catch (Exception e) { + throw new Exception("Unable to drop tables and close MySQL connection", e); + } + } + + @Test + public void deleteRecordsTest() throws Exception { + /* Delete a record stored in Cloud SQL */ + + // Setup phase + + // Create a test author + PreparedStatement userInsert = conn.prepareStatement(createUserSql); + + userInsert.setInt(1, 1); // set the UID manually + userInsert.setString(2, "Test User"); + + // Create a record and store it in Cloud SQL + PreparedStatement postStatement = conn.prepareStatement(createPostSql); + + postStatement.setInt(1, 1); // set the ID manually + postStatement.setInt(2, 1); + postStatement.setTimestamp(3, new Timestamp(new Date().getTime())); + postStatement.setString(4, "Post 1 title"); + postStatement.setString(5, "Post 1 content"); + + // Excute phase + + userInsert.executeUpdate(); // store the new user + postStatement.executeUpdate(); // store the prepared queries + + // Encode the ID (to replicate behaviour as seen in application) + String encodedId = Base64.getUrlEncoder().encodeToString(String.valueOf("1").getBytes()); + + // Decode ID in preparation to use in Cloud SQL query + String decodedId = new String(Base64.getUrlDecoder().decode(encodedId)); + + try { + PreparedStatement statementDeletePost = conn.prepareStatement(deleteRecordSql); + statementDeletePost.setString(1, decodedId); // delete post with ID = 1 + statementDeletePost.executeUpdate(); // delete the record from the database + + PreparedStatement statementCountPosts = conn.prepareStatement(countSelectSql); + ResultSet rs = + statementCountPosts.executeQuery(); // check to see how many records are in the table + + // Check phase + + rs.next(); // increment the cursor + + // the table should be empty + assertThat((String) rs.getString("total")).named("query results").isEqualTo("0"); + } catch (Exception e) { + throw new Exception("Error deleting record from database", e); + } + } + + @Rule public final ExpectedException exception = ExpectedException.none(); + + @Test + public void deleteInvalidRecordsTest() throws Exception { + /* + * Tests to see if an invalid ID will result in record deletion + * In this test no deletion should occur + */ + + // Setup phase + + // Create a test author + PreparedStatement userInsert = conn.prepareStatement(createUserSql); + + userInsert.setInt(1, 1); // set the UID manually + userInsert.setString(2, "Test User"); + + // Create a record and store it in Cloud SQL + PreparedStatement postStatement = conn.prepareStatement(createPostSql); + + postStatement.setInt(1, 1); // set the ID manually + postStatement.setInt(2, 1); + postStatement.setTimestamp(3, new Timestamp(new Date().getTime())); + postStatement.setString(4, "Post 1 title"); + postStatement.setString(5, "Post 1 content"); + + // Excute phase + + userInsert.executeUpdate(); // store the new user + postStatement.executeUpdate(); // store the prepared queries + + // Test if the user tries to an ID (unencoded) in the URI + + // Decode ID in preparation to use in Cloud SQL query but with unencoded ID + exception.expect(IllegalArgumentException.class); + String decodedId = new String(Base64.getUrlDecoder().decode("1")); + } +} diff --git a/appengine-j8-start/cloudsql/src/test/java/com/example/appengine/ListRecordsTest.java b/appengine-j8-start/cloudsql/src/test/java/com/example/appengine/ListRecordsTest.java new file mode 100644 index 00000000000..d818f51516c --- /dev/null +++ b/appengine-j8-start/cloudsql/src/test/java/com/example/appengine/ListRecordsTest.java @@ -0,0 +1,231 @@ +/* + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.appengine.cloudsql; + +import static com.google.common.truth.Truth.assertThat; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.Timestamp; +import java.util.Date; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@link ListRecords}. */ +@RunWith(JUnit4.class) +public final class ListRecordsTest { + + Connection conn; + + // MySQL server connection URL - use localhost for testing + // Format: jdbc:mysql://localhost/DATABASE_NAME?useSSL=false&user=USERNAME&password=PASSWORD + final String serverUrl = + "jdbc:mysql://localhost/DATABASE?useSSL=false&user=USERNAME&password=PASSWORD"; + + // Table creation SQL commands + final String createContentTableSql = + "CREATE TABLE IF NOT EXISTS posts ( post_id INT NOT NULL " + + "AUTO_INCREMENT, author_id INT NOT NULL, timestamp DATETIME NOT NULL, " + + "title VARCHAR(256) NOT NULL, " + + "body VARCHAR(1337) NOT NULL, PRIMARY KEY (post_id) )"; + + final String createUserTableSql = + "CREATE TABLE IF NOT EXISTS users ( user_id INT NOT NULL " + + "AUTO_INCREMENT, user_fullname VARCHAR(64) NOT NULL, " + + "PRIMARY KEY (user_id) )"; + + // Record SQL commands + final String createPostSql = + "INSERT INTO posts (post_id, author_id, timestamp, title, body) VALUES (?, ?, ?, ?, ?)"; + + final String selectSql = + "SELECT posts.post_id, users.user_fullname, posts.title, posts.body FROM posts, users WHERE (posts.author_id = users.user_id) AND (posts.body != \"\") ORDER BY posts.post_id ASC"; + + // User creation SQL command + final String createUserSql = "INSERT INTO users (user_id, user_fullname) VALUES (?, ?)"; + + // Drop table commands + final String dropContentTableSql = "DROP TABLE posts"; + + final String dropUserTableSql = "DROP TABLE users"; + + @Before + public void setUp() throws Exception { + // Connect to the MySQL server for testing and create the databases + + try { + conn = DriverManager.getConnection(serverUrl); + } catch (Exception e) { + throw new Exception("Unable to connect to Cloud SQL", e); + } + + // Create tables if required + conn.createStatement().executeUpdate(createContentTableSql); // create content table + conn.createStatement().executeUpdate(createUserTableSql); // create user table + } + + @After + public void tearDown() throws Exception { + // Drop created tables + + try { + PreparedStatement dropContentStatement = conn.prepareStatement(dropContentTableSql); + PreparedStatement dropUserStatement = conn.prepareStatement(dropUserTableSql); + + dropContentStatement.executeUpdate(); + dropUserStatement.executeUpdate(); + + conn.close(); // close the database connection + } catch (Exception e) { + throw new Exception("Unable to drop tables and close MySQL connection", e); + } + } + + @Test + public void listTest() throws Exception { + /* Retrieve records from Cloud SQL */ + + int counter = 1; // counter for iterating through results + + // Setup phase + + // Create a test author + PreparedStatement userInsert = conn.prepareStatement(createUserSql); + + userInsert.setInt(1, 1); + userInsert.setString(2, "Test User"); + + // Create four records and store them in Cloud SQL + + PreparedStatement postBatch = conn.prepareStatement(createPostSql); + + postBatch.setInt(1, 1); // Manually set the ID because we care about the order + postBatch.setInt(2, 1); // Author ID equals newly created author + postBatch.setTimestamp(3, new Timestamp(new Date().getTime())); + postBatch.setString(4, "Post 1 title"); + postBatch.setString(5, "Post 1 content"); + postBatch.addBatch(); + + postBatch.setInt(1, 2); + postBatch.setInt(2, 1); + postBatch.setTimestamp(3, new Timestamp(new Date().getTime())); + postBatch.setString(4, "Post 2 title"); + postBatch.setString(5, "Post 2 content"); + postBatch.addBatch(); + + postBatch.setInt(1, 3); + postBatch.setInt(2, 1); + postBatch.setTimestamp(3, new Timestamp(new Date().getTime())); + postBatch.setString(4, "Post 3 title"); + postBatch.setString(5, "Post 3 content"); + postBatch.addBatch(); + + postBatch.setInt(1, 4); + postBatch.setInt(2, 1); + postBatch.setTimestamp(3, new Timestamp(new Date().getTime())); + postBatch.setString(4, "Post 4 title"); + postBatch.setString(5, "Post 4 content"); + postBatch.addBatch(); + + // Excute phase + + userInsert.executeUpdate(); // store the new user + postBatch.executeBatch(); // store the prepared queries + + ResultSet rs = + conn.prepareStatement(selectSql) + .executeQuery(); // retrieve the posts and put them into a ResultSet for iteration + + // Check phase + + while (rs.next() && counter <= 4) { + assertThat((String) rs.getString("users.user_fullname")).isEqualTo("Test User"); + assertThat((String) rs.getString("posts.title")).isEqualTo("Post " + counter + " title"); + assertThat((String) rs.getString("posts.body")).isEqualTo("Post " + counter + " content"); + counter++; + } + } + + @Test + public void displayPostWithContentTest() throws Exception { + /* Retrieve only records that have content */ + + final String countSelectSql = + "SELECT COUNT(*) as total FROM posts, users WHERE (posts.author_id = users.user_id) AND (posts.body != \"\")"; + + // Setup phase + + // Create a test author + PreparedStatement userInsert = conn.prepareStatement(createUserSql); + + userInsert.setInt(1, 1); + userInsert.setString(2, "Test User"); + + // Create four records and store them in Cloud SQL + + PreparedStatement postBatch = conn.prepareStatement(createPostSql); + + postBatch.setInt(1, 1); // Manually set the ID because we care about the order + postBatch.setInt(2, 1); // Author ID equals newly created author + postBatch.setTimestamp(3, new Timestamp(new Date().getTime())); + postBatch.setString(4, "Post 1 title"); + postBatch.setString(5, "Post 1 content"); + postBatch.addBatch(); + + postBatch.setInt(1, 2); + postBatch.setInt(2, 1); + postBatch.setTimestamp(3, new Timestamp(new Date().getTime())); + postBatch.setString(4, "Post 2 title"); + postBatch.setString(5, "Post 2 content"); + postBatch.addBatch(); + + postBatch.setInt(1, 3); + postBatch.setInt(2, 1); + postBatch.setTimestamp(3, new Timestamp(new Date().getTime())); + postBatch.setString(4, "Post 3 title"); + postBatch.setString(5, "Post 3 content"); + postBatch.addBatch(); + + postBatch.setInt(1, 4); + postBatch.setInt(2, 1); + postBatch.setTimestamp(3, new Timestamp(new Date().getTime())); + postBatch.setString(4, "Post 4 title"); + postBatch.setString(5, ""); // No content in this post + postBatch.addBatch(); + + // Execute phase + + userInsert.executeUpdate(); + postBatch.executeBatch(); + + ResultSet rs = + conn.prepareStatement(countSelectSql) + .executeQuery(); // retrieve the posts and put them into a ResultSet for iteration + + // Check phase + + rs.next(); // move the cursor + + // There should only be three posts returned by the query + assertThat((String) rs.getString("total")).named("query results").isEqualTo("3"); + } +} diff --git a/appengine-j8-start/cloudsql/src/test/java/com/example/appengine/PostFormTest.java b/appengine-j8-start/cloudsql/src/test/java/com/example/appengine/PostFormTest.java new file mode 100644 index 00000000000..7030fd56643 --- /dev/null +++ b/appengine-j8-start/cloudsql/src/test/java/com/example/appengine/PostFormTest.java @@ -0,0 +1,111 @@ +/* + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.appengine.cloudsql; + +import static com.google.common.truth.Truth.assertThat; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@link PostForm}. */ +@RunWith(JUnit4.class) +public final class PostFormTest { + + Connection conn; + + // MySQL server connection URL - use localhost for testing + // Format: jdbc:mysql://localhost/DATABASE_NAME?useSSL=false&user=USERNAME&password=PASSWORD + final String serverUrl = + "jdbc:mysql://localhost/DATABASE?useSSL=false&user=USERNAME&password=PASSWORD"; + + // Table creation SQL command + final String createUserTableSql = + "CREATE TABLE IF NOT EXISTS users ( user_id INT NOT NULL " + + "AUTO_INCREMENT, user_fullname VARCHAR(64) NOT NULL, " + + "PRIMARY KEY (user_id) )"; + + // User creation SQL command + final String createUserSql = "INSERT INTO users (user_id, user_fullname) VALUES (?, ?)"; + + // User selection SQL command + final String selectUserSql = "SELECT user_id, user_fullname FROM users"; + + final String dropUserTableSql = "DROP TABLE users"; + + @Before + public void setUp() throws Exception { + // Connect to the MySQL server for testing and create the databases + + try { + conn = DriverManager.getConnection(serverUrl); + } catch (Exception e) { + throw new Exception("Unable to connect to Cloud SQL", e); + } + + // Create table + conn.createStatement().executeUpdate(createUserTableSql); // create user table + } + + @After + public void tearDown() throws Exception { + // Drop created tables and close the connection + + try { + PreparedStatement dropUserStatement = conn.prepareStatement(dropUserTableSql); + dropUserStatement.executeUpdate(); + + conn.close(); // close the database connection + } catch (Exception e) { + throw new Exception("Unable to drop tables and close MySQL connection", e); + } + } + + @Test + public void listTest() throws Exception { + // Retrieve user's names from Cloud SQL + + // Setup phase + + // Create a user + PreparedStatement userInsert = conn.prepareStatement(createUserSql); + + userInsert.setInt(1, 1); + userInsert.setString(2, "Test User"); + + // Excute phase + + userInsert.executeUpdate(); // store the new user + + ResultSet rs = + conn.prepareStatement(selectUserSql) + .executeQuery(); // retrieve the user and store them in a ResultSet for iteration + + // Check phase + + while (rs.next()) { + assertThat((String) rs.getString("user_id")).isEqualTo("1"); + assertThat((String) rs.getString("user_fullname")).isEqualTo("Test User"); + } + } +} diff --git a/appengine-j8-start/cloudsql/src/test/java/com/example/appengine/UpdateRecordsTest.java b/appengine-j8-start/cloudsql/src/test/java/com/example/appengine/UpdateRecordsTest.java new file mode 100644 index 00000000000..3a244214434 --- /dev/null +++ b/appengine-j8-start/cloudsql/src/test/java/com/example/appengine/UpdateRecordsTest.java @@ -0,0 +1,206 @@ +/* + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.appengine.cloudsql; + +import static com.google.common.truth.Truth.assertThat; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.Timestamp; +import java.util.Base64; +import java.util.Date; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/* Unit tests for {@link UpdateRecords}. */ +@RunWith(JUnit4.class) +public final class UpdateRecordsTest { + + Connection conn; + + // MySQL server connection URL - use localhost for testing + // Format: jdbc:mysql://localhost/DATABASE_NAME?useSSL=false&user=USERNAME&password=PASSWORD + final String serverUrl = + "jdbc:mysql://localhost/DATABASE?useSSL=false&user=USERNAME&password=PASSWORD"; + + // Table creation SQL commands + final String createContentTableSql = + "CREATE TABLE IF NOT EXISTS posts ( post_id INT NOT NULL " + + "AUTO_INCREMENT, author_id INT NOT NULL, timestamp DATETIME NOT NULL, " + + "title VARCHAR(256) NOT NULL, " + + "body VARCHAR(1337) NOT NULL, PRIMARY KEY (post_id) )"; + + final String createUserTableSql = + "CREATE TABLE IF NOT EXISTS users ( user_id INT NOT NULL " + + "AUTO_INCREMENT, user_fullname VARCHAR(64) NOT NULL, " + + "PRIMARY KEY (user_id) )"; + + // Record SQL commands + final String createPostSql = + "INSERT INTO posts (post_id, author_id, timestamp, title, body) VALUES (?, ?, ?, ?, ?)"; + + // User creation SQL command + final String createUserSql = "INSERT INTO users (user_id, user_fullname) VALUES (?, ?)"; + + // Update record SQL command + final String updateSql = "UPDATE posts SET title = ?, body = ? WHERE post_id = ?"; + + // Select record SQL command + final String selectSql = + "SELECT posts.post_id, posts.title, posts.body, users.user_fullname FROM posts, users WHERE (post_id = ?) AND (posts.author_id = users.user_id) LIMIT 1"; + + // Drop table commands + final String dropContentTableSql = "DROP TABLE posts"; + + final String dropUserTableSql = "DROP TABLE users"; + + @Before + public void setUp() throws Exception { + // Connect to the MySQL server for testing and create the databases + + try { + conn = DriverManager.getConnection(serverUrl); // Connect to the database + + // Create tables + conn.createStatement().executeUpdate(createContentTableSql); // Create content table + conn.createStatement().executeUpdate(createUserTableSql); // Create user table + } catch (Exception e) { + throw new Exception("Unable to connect to Cloud SQL", e); + } + } + + @After + public void tearDown() throws Exception { + // Drop created tables + + try { + PreparedStatement dropContentStatement = conn.prepareStatement(dropContentTableSql); + PreparedStatement dropUserStatement = conn.prepareStatement(dropUserTableSql); + + dropContentStatement.executeUpdate(); + dropUserStatement.executeUpdate(); + + conn.close(); // close the database connection + } catch (Exception e) { + throw new Exception("Unable to drop tables and close MySQL connection", e); + } + } + + @Test + public void updateRecordsTest() throws Exception { + /* Update a record stored in Cloud SQL */ + + // Setup phase + + // Create a test author + PreparedStatement userInsert = conn.prepareStatement(createUserSql); + + userInsert.setInt(1, 1); // set the UID manually + userInsert.setString(2, "Test User"); + + // Create a record and store it in Cloud SQL + PreparedStatement postStatement = conn.prepareStatement(createPostSql); + + postStatement.setInt(1, 1); // set the ID manually + postStatement.setInt(2, 1); + postStatement.setTimestamp(3, new Timestamp(new Date().getTime())); + postStatement.setString(4, "Post 1 title"); + postStatement.setString(5, "Post 1 content"); + + // Excute phase + + userInsert.executeUpdate(); // store the new user + postStatement.executeUpdate(); // store the prepared queries + + // Encode the ID (to replicate behaviour as seen in application) + String encodedId = Base64.getUrlEncoder().encodeToString(String.valueOf("1").getBytes()); + + // Decode ID in preparation to use in Cloud SQL query + String decodedId = new String(Base64.getUrlDecoder().decode(encodedId)); + + try { + PreparedStatement statementUpdatePost = conn.prepareStatement(updateSql); + statementUpdatePost.setString(1, "Post 1 title updated"); + statementUpdatePost.setString(2, "Post 1 content updated"); + statementUpdatePost.setString(3, decodedId); // update post with ID = 1 + statementUpdatePost.executeUpdate(); // update the record + + PreparedStatement statementPost = conn.prepareStatement(selectSql); + statementPost.setString(1, decodedId); + ResultSet rs = statementPost.executeQuery(); // return the updated blog post + + // Check phase + + rs.next(); // increment the cursor + + assertThat((String) rs.getString("posts.post_id")).isEqualTo("1"); // ID hasn't changed + assertThat((String) rs.getString("users.user_fullname")) + .isEqualTo("Test User"); // Author hasn't changed + assertThat((String) rs.getString("posts.title")) + .isEqualTo("Post 1 title updated"); // Title has been updated + assertThat((String) rs.getString("posts.body")) + .isEqualTo("Post 1 content updated"); // Content has been updated + } catch (Exception e) { + throw new Exception("Error updating record from database", e); + } + } + + @Rule public final ExpectedException exception = ExpectedException.none(); + + @Test + public void updateInvalidRecordsTest() throws Exception { + /* + * Tests to see if an invalid ID will result in updating a record + * In this test no update should occur with an exception being thrown + */ + + // Setup phase + + // Create a test author + PreparedStatement userInsert = conn.prepareStatement(createUserSql); + + userInsert.setInt(1, 1); // set the UID manually + userInsert.setString(2, "Test User"); + + // Create a record and store it in Cloud SQL + PreparedStatement postStatement = conn.prepareStatement(createPostSql); + + postStatement.setInt(1, 1); // set the ID manually + postStatement.setInt(2, 1); + postStatement.setTimestamp(3, new Timestamp(new Date().getTime())); + postStatement.setString(4, "Post 1 title"); + postStatement.setString(5, "Post 1 content"); + + // Excute phase + + userInsert.executeUpdate(); // store the new user + postStatement.executeUpdate(); // store the prepared queries + + // Test if the user tries to an ID (unencoded) in the URI + + // Decode ID in preparation to use in Cloud SQL query but with unencoded ID + exception.expect(IllegalArgumentException.class); + String decodedId = new String(Base64.getUrlDecoder().decode("1")); + } +}