Week 1
You may use these tools during project development
git status
git init -b main
git checkout -b main
.env
Initial commit
git remote add origin https://github.com/YOUR-USERNAME/java3-project.git
git config --global credential.helper cache
jdbc:mysql://<your-server-name>.mysql.
database.azure.com:3306/<your-database-name>?serverTimezone=UTC&sslmode=required&
user=<your-user-name>&password=<your-password>
https://<your-app-name>.scm.azurewebsites.net/
Close your current project in IntelliJ. Install the Azure Toolkit for IntelliJ plugin and restart IntelliJ.
Re-open your class project.
To sign in to your Azure account, navigate to the left-hand Azure Explorer sidebar, and then click the Azure Sign In icon. Alternatively, you can navigate to Tools, expand Azure, and then click Sign in.
In the Azure Sign In window, select Azure CLI or OAuth 2.0, and then click Sign in.
In the browser, sign in with your account and then go back to IntelliJ.
In the Select Subscriptions dialog box, click on the subscription that you want to use, then click Select.
Open the Azure option. Open the Azure Database for MySQL, Resource Groups, and Web Apps options to see the items you created last class.
Right-click the web app and open in the browser.
You can even use this area to create new resource groups, web apps, and databases.
Right-click the database that has public access and choose "Open with Database Tools". The Azure database info should be populated. Enter your username and password.
If you need to reset your password, go to the web portal, and click the "Reset password" button.
Select Forever in the Save input.
Download any missing drivers.
Click Test Connection. A "Succeeded" message should display.
jdbc:mysql://<your-server-name>.mysql.
database.azure.com:3306/<your-database-name>?serverTimezone=UTC&sslmode=required&
user=<your-user-name>&password=<your-password>
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.
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 you need to connect to.
AZURE_MYSQL_CONNECTIONSTRING="XXXX"
import io.github.cdimascio.dotenv.Dotenv;
import io.github.cdimascio.dotenv.DotenvException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
public class MySQL_Connect {
public static Connection getConnection() throws SQLException {
try {
Class.forName("com.mysql.cj.jdbc.Driver");
} catch (ClassNotFoundException e) {
throw new SQLException("MySQL driver not found");
}
Dotenv dotenv = null;
try {
dotenv = Dotenv.load();
} catch(DotenvException e) {
throw new SQLException("Could not find .env file");
}
String connectionString = dotenv.get("AZURE_MYSQL_CONNECTIONSTRING");
if(connectionString == null) {
throw new SQLException("Connection string not found");
}
try {
Connection connection = DriverManager.getConnection(connectionString);
if (connection.isValid(2)) {
return connection;
}
} catch (SQLException e) {
throw new SQLException(e.getMessage());
}
return null;
}
public static void main(String[] args) {
try {
if(getConnection() != null) {
System.out.println("Connection successful");
}
} catch (SQLException e) {
System.out.println(e.getMessage());
}
}
}
Connection - An interface for classes representing a communication session with a database server.
Connection String - A string containing the information needed to establish a connection to a database server. The string typically includes:
DriverManager - The DriverManager class provides functionality for managing JDBC drivers. Use the DriverMananger to get a Connection object with the getConnection() method.
Port - A port is a positive integer that identifies a specific server software running on a host machine.
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,
timezone VARCHAR(50) NOT NULL DEFAULT 'America/Chicago',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
CREATE PROCEDURE sp_add_user(
IN p_email VARCHAR(255),
IN p_password VARCHAR(255),
IN p_status VARCHAR(10),
IN p_privileges VARCHAR(10)
)
BEGIN
INSERT INTO user (email, password, status, privileges)
VALUES (p_email,p_password,p_status,p_privileges);
END;
CALL sp_add_user('delete-me@example.com', 'badpassword1', 'inactive', 'subscriber');
CALL sp_add_user('delete-me2@example.com', 'badpassword2', 'inactive', 'subscriber');
SELECT * FROM user;
CREATE PROCEDURE sp_get_all_users()
BEGIN
SELECT user_id, first_name, last_name, email, phone, password, language, status, privileges, created_at, timezone
FROM user;
END;
Generate a default and parameterized constructor, getter and setter methods, a toString method (id, firstName, lastName, email), and equals method.
import java.time.Instant;
public class User {
private int userId;
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 createdAt;
private String timezone;
}
@Override
public int compareTo(User o) {
int result = this.lastName.compareToIgnoreCase(o.lastName);
if(result == 0) {
result = this.firstName.compareToIgnoreCase(o.firstName);
}
return result;
}
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.
import edu.kirkwood.jcommerce.model.User;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import static edu.kirkwood.shared.MySQL_Connect.getConnection;
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;
}
}
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;
}
}
The Connection object has a prepareCall() method that allows us to create a CallableStatement object.
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;
}
}
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.
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 createdAt = resultSet.getTimestamp("created_at").toInstant();
String timezone = resultSet.getString("timezone");
User user = new User(userId, firstName, lastName, email, phone, password, language, status, privileges, created_at, timezone);
list.add(user);
}
}
}
} catch (SQLException e) {
throw new RuntimeException(e)
}
return list;
}
}
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.
Since we don't know how many records the query will return, this statement is best used as the condition of a while loop.
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.
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) {
getAll().forEach(System.out::println);
}
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.
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 |
A CallableStatement allows non-SQL statements (such as stored procedures) to be executed against the database.
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.setAttribute("pageTitle", "All Users");
req.getRequestDispatcher("WEB-INF/ecommerce/admin-users.jsp").forward(req, resp);
}
}
Step 5: Create/Update a JSP
Create a JSP in the "WEB-INF" folder called admin-users.jsp.
Add this HTML.
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<%@ taglib prefix="fmt" uri="jakarta.tags.fmt" %>
<fmt:setLocale value="en-US" />
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>${pageTitle}</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
</head>
<body>
<div class="container py-4">
<div class="row">
<!-- Main content START -->
<div class="col-xl-12">
<!-- Title -->
<h1>All Users</h1>
<p class="lead">
<c:choose>
<c:when test="${users.size() == 1}">There is 1 user</c:when>
<c:otherwise>There are ${users.size()} users</c:otherwise>
</c:choose>
</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>
<th scope="col">Timezone</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.createdAt}</td>
<td>${user.timezone}</td>
</c:forEach>
</tbody>
</table>
</div>
</c:if>
</div> <%-- Col END --%>
</div> <%-- Row END --%>
</div> <%-- Container END --%>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz"
crossorigin="anonymous"></script>
</body>
</html>
Run Tomcat. Visit "/users". A table with two users should display.
Run this query to delete one of the users.
DELETE FROM user WHERE user_id = 2;
Refresh the local version of the website. It should display "There is 1 user".
Delete the last user.
Refresh the local version of the website. It should display "There are 0 users".
Commit/push changes to GitHub.
Visit "/users" on your live site. A 500 server error will occur.
Go to Azure and click your web app. In the left navigation, click Monitoring > App Service Logs.
Turn on File System Application logging.
Type 7 in the retention period field. Click Save.
Click Log Stream to find the .env file not found error.
If you don't see the error, click the Reconnect button.
Last semester, we built the war file from IntelliJ and deployed it to Azure through the Azure Toolbox plugin. This worked because IntelliJ has access to the .env file.
This semester, the GitHub Action builds the war file and deploys to Azure using continuous integration and continuous delivery/deployment (CI/CD). GitHub does not have access to the .env file because we ignored it in the .gitignore file.
In IntelliJ, open the src/main/webapp/WEB-INF/web.xml file.
This file is called the deployment descriptor. Its purpose is to provide instructions to the Jakarta EE web application server for deploying and running the web application.
Add this code between the <web-app> tags.
<jsp-config>
<jsp-property-group>
<url-pattern>*.jsp</url-pattern>
<page-encoding>UTF-8</page-encoding>
<include-prelude>/WEB-INF/top.jspf</include-prelude>
<trim-directive-whitespaces>true</trim-directive-whitespaces>
<default-content-type>text/html</default-content-type>
</jsp-property-group>
<jsp-property-group>
<url-pattern>*.jsp</url-pattern>
<page-encoding>UTF-8</page-encoding>
<include-coda>/WEB-INF/bottom.jspf</include-coda>
<trim-directive-whitespaces>true</trim-directive-whitespaces>
<default-content-type>text/html</default-content-type>
</jsp-property-group>
</jsp-config>
The <jsp-config>
element can contain 1 or more <jsp-property-group>
elements.
Each group must define distinct <url-pattern>
tags. In this example, we are telling Tomcat to apply the base.jspf file to the beginning of all files ending in .jsp anywhere in the application.
<page-encoding>
indicates page encoding information. UTF-8 is the preferred encoding for e-mail and web pages.
UTF is backwards compatible with ASCII, results in fewer internationalization issues, has been implemented in all modern operating systems, and has been implemented in standards such as JSON.
<trim-directive-whitespaces>
controls whether template text containing only white spaces must be removed from the response output. Without this, there will be a couple empty lines at the top of our HTML source code.
<default-content-type>
Specifies the default content type. If the page directive does not include a contentType attribute, it specifies the default response content type.
Context Parameters can be added to the web.xml file to set global variables for the entire application.
Use environment variables for variables that need to be accessed by servlets, like passwords and API keys.
Use the web.xml file for variables that need to be accessed by JSPs.
<context-param>
<param-name>appURLLocal</param-name>
<param-value>http://localhost:8080/your_project_name_war_exploded</param-value>
</context-param>
<context-param>
<param-name>appURLCloud</param-name>
<param-value>https://your-app-name.azurewebsites.net</param-value>
</context-param>
Substitute the param-value tags with your URLs.
I would like you to remove the last "/" at the end of both URLs.
Create the top.jspf file in the WEB-INF folder.
jspf stands for Java Server Page Fragment
Add this code to include page and taglib directives, to set an "appURL" variable based on whether you are running the app locally or on Microsoft Azure, and to set doctype, html, head, meta, title, Bootstrap link and script, jQuery script, and body tags.
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<%@ taglib prefix="fmt" uri="jakarta.tags.fmt" %>
<fmt:setLocale value="en-US" />
<c:choose>
<c:when test="${pageContext.request.serverName eq 'localhost' }">
<c:set var="appURL" value="${initParam['appURLLocal']}"></c:set>
</c:when>
<c:otherwise>
<c:set var="appURL" value="${initParam['appURLCloud']}"></c:set>
</c:otherwise>
</c:choose>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>${pageTitle}</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
<script defer src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz"
crossorigin="anonymous"></script>
<script defer src="https://code.jquery.com/jquery-3.7.1.min.js"
integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo="
crossorigin="anonymous"></script>
</head>
<body>
Create the bottom.jspf file in the WEB-INF folder
Add this code to close the body and html tags.
</body>
</html>
Remove the related code from admin-users.jsp. The deployment descriptor will now include it via base.jspf.
Remove the related code from admin-users.jsp. The deployment descriptor will now include it via base.jspf.
Temporarily add this to admin-user.jsp to see the value set.
<h1>${appURL}</h1>
Run the app and view the users page.
Note that the page title requires it to be set as a servlet request attribute.
By default, Java web applications display detailed error messages that disclose the server version and detailed stack trace information that can, in some situations, display snippets of Java code. This information is a boon to hackers looking for as much information about their victims as possible.
Fortunately, it's very easy to configure web.xml to display custom error pages.
An error page will be displayed using the following configuration whenever the application responds with an HTTP 500 error.
You can add additional entries for other HTTP status codes.
405 errors will occur when Azure fails to send emails.
<error-page>
<error-code>404</error-code>
<location>/fileNotFound</location>
</error-page>
<error-page>
<exception-type>java.lang.Throwable</exception-type>
<location>/errorHandler</location>
</error-page>
In the "shared" package, create a new class called "ErrorHandler". Add this code.
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 static jakarta.servlet.RequestDispatcher.*;
import java.io.IOException;
@WebServlet("/errorHandler")
public class ErrorHandler extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.setAttribute("pageTitle", "Error");
req.getRequestDispatcher("WEB-INF/error.jsp").forward(req, resp);
}
}
In the "shared" package, create a new class called "FileNotFound". Add this code.
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 static jakarta.servlet.RequestDispatcher.*;
import java.io.IOException;
@WebServlet("/fileNotFound")
public class FileNotFound extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.setAttribute("pageTitle", "File Not Found");
req.getRequestDispatcher("WEB-INF/fileNotFound.jsp").forward(req, resp);
}
}
In the "WEB-INF" folder, create a file called "error.jsp". Add this code.
<main>
<div class="container pt-5">
<div class="row">
<div class="col-12 text-center">
<h2>Oh no!</h2>
<p class="mb-4">Something went wrong. We are sorry for the inconvenience.</p>
<a href="${appURL}" class="btn btn-primary">Take me to the Homepage</a>
</div>
</div>
</div>
</main>
In the "WEB-INF" folder, create a file called "fileNotFound.jsp". Add this code.
<main>
<div class="container pt-5">
<div class="row">
<div class="col-12 text-center">
<h1>404</h1>
<h2>Page Not Found</h2>
<p class="mb-4">The page you are looking for does not exist.</p>
<a href="${appURL}" class="btn btn-primary">Take me to the Homepage</a>
</div>
</div>
</div>
</main>
Add this code to the doGet method of the AdminUsers servlet to cause a server error.
String query = req.getParameter("q");
query.toString();
Run Tomcat locally. Visit "/users" to see the custom error page. Visit "/users2" to see the custom file not found page.
Remove the two lines of code from the doGet method that is causing the server error.
Commit and push the work to GitHub. Wait for the app to deploy and test the live website.
Test the "Take me to the Homepage" buttons. Note they should go to localhost when running Tomcat locally and they should go to your azurewebsites.net URL when running on the live server.
Check out the available static error fields from the RequestDispatcher interface.
In the ErrorHandler, before setting the pageTitle attribute, get some of those fields and assign them to String variables.
Format the error message output.
String errorMsg = "<strong>Error code:</strong> " + req.getAttribute(RequestDispatcher.ERROR_STATUS_CODE) + "<br>";
errorMsg += "<strong>Exception:</strong> " + req.getAttribute(RequestDispatcher.ERROR_EXCEPTION_TYPE) + "<br>";
errorMsg += "<strong>Message:</strong> " + req.getAttribute(RequestDispatcher.ERROR_MESSAGE); // Some Exceptions may not have messages
req.setAttribute("errorMsg", errorMsg);
In web.xml, set a context-param for debugging.
<context-param>
<param-name>debugging</param-name>
<param-value>true</param-value>
</context-param>
Update error.jsp to conditionally display error messages based on the value of the debugging setting.
<c:choose>
<c:when test="${initParam['debugging'] eq 'true'}">
<p>${errorMsg}</p>
</c:when>
<c:otherwise>
<h2>Oh no!</h2>
<p class="mb-4">Something went wrong. We are sorry for the inconvenience.</p>
<a href="${appURL}" class="btn btn-primary">Take me to the Homepage</a>
</c:otherwise>
</c:choose>
Add this code to the doGet method of the AdminUsers servlet to cause a server error.
String query = req.getParameter("q");
query.toString();
Run Tomcat locally. Visit "/users" to see the custom error page.
Remove the two lines of code from the doGet method that is causing the server error.
Commit and push the work to GitHub. Wait for the app to deploy and test the live website.
Visit "/users" to see the custom error page.
Set the debugging context-param to anything but true if you do not want users to see error messages on your live site.
In the shared package, create a class called Config. Add this code I got from ChatGPT to retrieve system environment variables.
import java.util.Optional;
public class Config {
public static String getEnv(String key) {
return Optional.ofNullable(System.getenv(key))
.orElseThrow(() -> new IllegalStateException("Environment variable " + key + " is not set"));
}
}
Windows Users: Create an AZURE_MYSQL_CONNECTIONSTRING environment variable the same way you created a JAVA_HOME, MAVEN_HOME, and CATALINA_HOME environment variable. Restart your computer.
Mac Users: The ChatGPT thread on the previous page contains directions on how to set system environment variables.
export AZURE_MYSQL_CONNECTIONSTRING='jdbc:mysql..'
printenv
to display a list of currently set environment variables.Update the MySQL_Connect class to use the Config class instead of the Dotenv class.
String connectionString = "";
try {
connectionString = Config.getEnv("AZURE_MYSQL_CONNECTIONSTRING");
} catch (IllegalStateException e) {
throw new SQLException(e.getMessage());
}
Run Tomcat. Go to "/users". The program should work without errors.
Commit and push the work to GitHub. Wait for the app to deploy and test the live website.
Visit "/users" to see the list of users, not an error page.