Java 3

Week 1

Course Objectives

  • Explain how standard documentation and diagrams apply to Java web application.
  • Write Java applications that use database technologies and consumes web services.
  • Display an understanding of the role of security in modern software development.
  • Show awareness of human behavior as it applies to software interaction.

What You'll Learn

  • Deploy web apps to Microsoft Azure and Heroku
  • Page loading indicators
  • Custom error pages
  • Sending e-mail
  • Database tables, schemas and stored procedures
  • JDBC Database connection
  • CallableStatement and ResultSet
  • Plain Old Java Objects (POJOs), Data Access Objects (DAOs), Java Servlets, and Java Server Pages (JSP)
  • Website themes
  • Including JSP fragments, header and footer
  • Sign up a user
  • 2-Factor Authentication
  • Session configuration
  • Session cookies
  • Login/logout

What You'll Learn

  • User permissions - what users can create, read ,update, and delete data
  • Login Attempts
  • Forgot password
  • Update and delete users
  • External authentication with GitHub
  • Cross-Site Scripting (XSS) attacks and other vulnerabilities
  • Internationalization
  • Custom JSTL tags
  • Send Text and Voice messages
  • APIs
  • Web sockets, multi-user applications
  • Credit Card processing
  • The general form of the connection URL is
    jdbc:[driver]://[serverName[.hostName][:portNumber]]/[schema][?property=value[&property=value]]
  • A Connection String uses a set of properties to establish a connection to a database server. It consists of:

    • Driver (jdbc:)

    • Connection protocol (mysql://, postgresql://, mariadb://, etc.)

    • IP Address: a universal resource locator (URL) that identifies a specific host machine on a network.

    • A port number - a positive integer that identifies a specific server software running on a host machine.

    • A database resource name

    • Any other vendor specific properties

    • You will need to create a connection string for each database schema that you need to connect to.

.env

  • Create a .env file in the src/main/resources/ folder.
  • Add database environment variables.
MYSQL_DRIVER=com.mysql.cj.jdbc.Driver
AZURE_MYSQL_CONNECTION=jdbc:mysql://<DB-SERVER-NAME>.mysql.database.azure.com:3306/<DB-SCHEMA>?serverTimezone=UTC&useSSL=true&requireSSL=false
DB_USER=<DB-USER-NAME>
DB_PASSWORD=<DB-PASSWORD>

Azure Database

  • Go to https://portal.azure.com.
  • Create a new Azure Database for MySQL flexible server.
  • Select the Resource Group you set up in Java 2.
  • Enter a server name related to your project name.
  • Select (US) Central US for the Region.
  • Create an admin username and password. Assign them to your environment variables.
  • Check "Add firewall rule for current IP Address".
  • Click "Review + Create", then "Create".

Azure Database

  • Wait for the deployment to complete.
  • Click the blue "Go to Resource" button or the name of your Azure Database for MySQL flexible server on the portal home page.
  • Click Settings in the left navigation, then click Networking.
  • Check the box labeled Allow public access from any Azure service within Azure to this server.
  • Click the "Add 0.0.0.0 to 255.255.255.255" button.
  • Click the Save button
  • You should be able to connect to the database from the MySQL Workbench or other app.

pom.xml

  • Add the dotenv, mysql, and jstl dependencies to the pom.xml file.

Database Drivers

  • Add dependencies to your pom.xml file for any database you plan to use. Each database vendor will have a specific jar file that includes the implementation of these different interfaces to work with their particular database.

MySQL_Connect Class

  • Create a MySQL_Connect class in a shared package
import io.github.cdimascio.dotenv.Dotenv;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

public class Database {
    public static Connection getConnection() {
        Dotenv dotenv = Dotenv.load();
        String db_full_driver = dotenv.get("DB_FULL_DRIVER");
        String connectionString = dotenv.get("LEARNX_DB_CONNECTION_STRING");
        String user = dotenv.get("DB_PASSWORD");
        String password = dotenv.get("DB_USER");
        try {
            Class.forName(db_full_driver);
        } catch (ClassNotFoundException e) {
            // what to do if the driver is not found
        }

        try {
            Connection connection = DriverManager.getConnection(connectionString, user, password);
            if (connection.isValid(2)) {
                return connection;
            }
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
        return null;
    }
}
  • A Connection object is created from the DriverManager class when calling the .getConnection() method.
  • Driver: a vendor's implementation of JDBC
  • DriverManager: maintains a registry of Drivers
  • Connection: represents a communication session with a relational database server.

The DriverManager class is used to get an instance of a Connection object by using the JDBC driver named in the connectionString URL

The URL syntax for a JDBC driver is:

jdbc:<driver>:[subsubprotocol:][databaseName][;attribute=value]

Each vendor (MySQL, PostgreSQL, etc) can implement its own subprotocol.

To test if the connection is valid we can call the isValid() method on the Connection object. If it's connected to the database, it returns true, otherwise it returns false.

User Table

  • Create a user database table by running the following SQL command.
  • I am setting all fields to NOT NULL, meaning that they are required.
    • If the user doesn't enter a first name, last name, or phone number, I would prefer to enter them as empty strings rather than null because null values can be difficult to deal with.
CREATE TABLE user (
    user_id INT AUTO_INCREMENT PRIMARY KEY,
    first_name VARCHAR(255) NOT NULL DEFAULT '',
    last_name VARCHAR(255) NOT NULL DEFAULT '',
    email VARCHAR(255) NOT NULL UNIQUE,
    phone VARCHAR(255) NOT NULL DEFAULT '',
    password VARCHAR(255) NOT NULL DEFAULT '',
    language VARCHAR(255) NOT NULL DEFAULT 'en-US',
    status enum('inactive', 'active', 'locked') NOT NULL,
    privileges enum('subscriber', 'user', 'premium', 'admin') NOT NULL, 
    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);

Add User

CREATE PROCEDURE sp_add_user(
    IN p_email VARCHAR(255),
    IN p_password VARCHAR(255)
)
BEGIN
    INSERT INTO user (email, password)
    VALUES (p_email,p_password);
END;
  • Step 1: Create/Update a stored procedure
  • Create two users by running these queries
CALL sp_add_user('delete-me@example.com', 'badpassword1');
CALL sp_add_user('delete-me2@example.com', 'badpassword2');
  • View the new users
SELECT * FROM user;

Get All Users

CREATE PROCEDURE sp_get_all_users()
BEGIN
    SELECT user_id, first_name, last_name, email, phone, password, language, status, privileges, created_at
    FROM user;
END;
  • Step 1: Create/Update a stored procedure

User POJO

  • Step 2: Create/Update a POJO.
  • Create an User POJO class in the model package.
  • Generate a default and parameterized constructor, getter and setter methods, and a toString method.

import java.time.Instant;

public class User {
    private int user_id;
    private String firstName;
    private String lastName;
    private String email;
    private String phone;
    private char[] password;
    private String language;
    private String status;
    private String privileges;
    private Instant created_at;
}

User POJO

  • The getPassword() method returns a char[] and not a String, because String is an immutable data type. That means String objects cannot be changed after a value has been created on the "String Pool". The String pool will eventually be removed by the "Java Garbage Collection Process". Until then, this value will continue to exist on the "Heap".
  • Therefore, the char[] is used to store the password to help with security. This is because the character array can be cleared as an empty array immediately after using it. It will not be stored in the heap so no unauthorized user can access the heap and retrieve the entered password.

Get All Users

public class UserDAO {
  public static List<User> getAll() {
      List<User> list = new ArrayList<>();
      try (Connection connection = getConnection()) {
          
      } catch (SQLException e) {
          throw new RuntimeException(e)
      }
      return list;
  }
}
  • Step 3: Create/Update a Data Access Object (DAO)
  • Create a static getAll method that returns a list of users.
  • It's important that you always close a database connection. Since the Connection class implements the AutoClosable interface, closing the connection can be done using a try-with-resources block.

Get All Users

public class UserDAO {
  public static List<User> getAll() {
      List<User> list = new ArrayList<>();
      try (Connection connection = getConnection()) {
      	try (CallableStatement statement = connection.prepareCall("{CALL sp_get_all_users()}")) {

  		}
      } catch (SQLException e) {
          throw new RuntimeException(e)
      }
      return list;
  }
}
  • After making a database connection we need a Statement object to encapsulate a SQL query.
  • A CallableStatement allows non-SQL statements (such as stored procedures) to be executed against the database.
  • The Connection object has a prepareCall() method that allows us to create a CallableStatement object.

Get All Users

public class UserDAO {
  public static List<User> getAll() {
      List<User> list = new ArrayList<>();
      try (Connection connection = getConnection()) {
      	try (CallableStatement statement = connection.prepareCall("{CALL sp_get_all_users()}")) {
        	try (ResultSet resultSet = statement.executeQuery()) {
            
            }
  		}
      } catch (SQLException e) {
          throw new RuntimeException(e)
      }
      return list;
  }
}
  • Then we use the CallableStatement's executeQuery() method to send the stored procedure to the database to execute the query.
  • The ResultSet object represents the database response - the actual data returned by the SQL command.

  • Using a try-with-resources statement will automatically close each Autocloseable object.

Get All Users

public class UserDAO {
  public static List<User> getAll() {
      List<User> list = new ArrayList<>();
      try (Connection connection = getConnection()) {
      	try (CallableStatement statement = connection.prepareCall("{CALL sp_get_all_users()}")) {
        	try (ResultSet resultSet = statement.executeQuery()) {
            	while (resultSet.next()) {
                	int userId = resultSet.getInt("user_id");
                  	String firstName = resultSet.getString("first_name");
                  	String lastName = resultSet.getString("last_name");
                  	String email = resultSet.getString("email");
                  	String phone = resultSet.getString("phone");
                  	char[] password = resultSet.getString("password").toCharArray();
                  	String language = resultSet.getString("language");
                  	String status = resultSet.getString("status");
                  	String privileges = resultSet.getString("privileges");
                  	Instant created_at = resultSet.getTimestamp("created_at").toInstant();
                  	User user = new User(userId, firstName, lastName, email, phone, password, language, status, privileges, created_at);
                  	list.add(user);
              	}
            }
  		}
      } catch (SQLException e) {
          throw new RuntimeException(e)
      }
      return list;
  }
}
  • Inside that try-with-resources statement, loop through all of the results, obtaining the data and instantiating an object.
  • Before closing the resultSet and statement, we add a while loop to iterate over each element in the resultSet.

Get All Users

  • ResultSet behaves like a Java Collections iterator in that it allows you to review the contents of each record returned by the query, one record at a time.

  • The .next() method of the ResultSet class will place a pointer at the first row of the result sets returned by the SQL query.

  • The method returns true if there is a record a the pointer location, false if no records are remaining.
  • Since we don't know how many records the query will return, this statement is best used as the condition of a while loop.

Get All Users

  • JDBC does not auto-generate Objects for each row and is ignorant of the data types in each row. The developer must tell the program which fields and data types you want to extract from each row.

  • The .getString() method can be used with a column name as its argument  to return the value as a String.
  • The .getInt() and .getDouble() methods do the same thing but return specified numeric values.
  • The .getDate() method can be used to return a Date object, which will need to be converted to a LocalDate or LocalDateTime object.
  • The .getTimestamp() method can be used to return Timestamp object. Call the .toLocalDateTime() method.

Get All Users

  • Add a main method to the UserDAO class to test the getAll method. Run the method and it should print the users you have in your database table.

public static void main(String[] args) {
	System.out.println(getAll().forEach(System.out::println));
}

Understanding the JDBC API

  • java.sql.Connection: A connection that represents the session between your Java application and the database
    Connection connection = DriverManager.getConnection(url, username, password);

  • java.sql.Statement: An object used to execute a static SQL statement and return the result.
    Statement statement = connection.createStatement();

    To execute SQL queries with JDBC, you must create a SQL query wrapper object, an instance of the Statement object.

  • java.sql.ResultSet: An object representing a database result set.
    String query = "SELECT * FROM User";
    ResultSet resultSet = statement.executeQuery(query);
    Use the Statement instance to execute a SQL query:

Statement execute methods

  • A ResultSet object represents the database query response in a table-like structure that holds records returned - the actual data returned by the SQL command. The next() method returns true or false. If true, the resultSet object points to the first row of data.

Method Returns Used for
executeQuery(sqlString) ResultSet SELECT statement
executeUpdate(sqlString) int (rows affected) INSERT, UPDATE, DELETE, or a DDL (DROP, CREATE, ALTER)
execute(sqlString) boolean (true if there was a ResultSet) Any SQL command or commands

Using CallableStatement

  • A CallableStatement allows non-SQL statements (such as stored procedures) to be executed against the database. 

The CallableStatement object encapsulates and execute a stored procedure. The Connection object has a prepareCall() method that allows us to create a CallableStatement object.

Then we use the PreparedStatement's executeQuery() method to send the stored procedure to the database to execute the query.

Again, the ResultSet object represents the database response - the actual data returned by the SQL command.

Get All Users

  • Step 4: Create/Update a Servlet
  • Create a servlet in the "ecommerce/controllers/" package called AdminUser. Set the URL pattern to "/users". Forward get requests to a JSP.
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.util.List;

@WebServlet("/users")
public class AdminUsers extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        List<User> users = UserDAO.getAll();
        req.setAttribute("users", users);
        req.getRequestDispatcher("WEB-INF/ecommerce/users.jsp").forward(req, resp);
    }
}

Get All Users

  • Step 5: Create/Update a JSP

  • Create a JSP in the "WEB-INF/ecommerce/" folder called users.jsp. 

  • Add this HTML. Add a non-breaking space (&nbsp;) anywhere a space is needed.

    <div class="container">
        <div class="row">
            <!-- Main content START -->
            <div class="col-xl-12">
                <!-- Title -->
                <h1>All Users</h1>
                <p class="lead">There ${users.size() == 1 ? "is" : "are"} ${users.size()} user${users.size() != 1 ? "s" : ""}</p>
                <c:if test="${users.size() > 0}">
                    <div class="table-responsive">
                        <table class="table table-bordered">
                            <thead>
                            <tr>
                                <th scope="col"></th>
                                <th scope="col">First name</th>
                                <th scope="col">Last name</th>
                                <th scope="col">Email</th>
                                <th scope="col">Phone</th>
                                <th scope="col">Language</th>
                                <th scope="col">Status</th>
                                <th scope="col">Privileges</th>
                                <th scope="col">Created At</th>
                            </tr>
                            </thead>
                            <tbody>
                            <c:forEach items="${users}" var="user">
                                <tr>
                                    <td>
                                    	<a href="edit-user?user_id=${user.userId}" class="btn btn-sm btn-outline-primary">Edit</a>
                    					<a href="delete-user?user_id=${user.userId}" class="btn btn-sm btn-outline-danger">Delete</a>
                                    </td>
                                    <td>${user.firstName}</td>
                                    <td>${user.lastName}</td>
                                    <td>${user.email}</td>
                                    <td>${user.phone}</td>
                                    <td>${user.language}</td>
                                    <td>${user.status}</td>
                                    <td>${user.privileges}</td>
                                    <td>${user.created_at}</td>
                            </c:forEach>
                            </tbody>
                        </table>
                    </div>
                </c:if>
            </div> <!-- Col END -->
        </div> <!-- Row END -->
    </div> <!-- Container END -->