From 42e5210c07615c18c62c4151931d03c25c7be658 Mon Sep 17 00:00:00 2001 From: David Whitlock Date: Sun, 14 Dec 2025 10:55:10 -0800 Subject: [PATCH 01/36] Added a simple Course class that we'll eventually persist to a database. --- .../main/java/edu/pdx/cs/joy/jdbc/Course.java | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 examples/src/main/java/edu/pdx/cs/joy/jdbc/Course.java diff --git a/examples/src/main/java/edu/pdx/cs/joy/jdbc/Course.java b/examples/src/main/java/edu/pdx/cs/joy/jdbc/Course.java new file mode 100644 index 000000000..2fcecf5ff --- /dev/null +++ b/examples/src/main/java/edu/pdx/cs/joy/jdbc/Course.java @@ -0,0 +1,90 @@ +package edu.pdx.cs.joy.jdbc; + +/** + * Represents a course in a college course catalog. + * Each course has a title and is associated with a department. + */ +public class Course { + private String title; + private int departmentId; + + /** + * Creates a new Course with the specified title and department ID. + * + * @param title the title of the course + * @param departmentId the numeric ID of the department offering this course + */ + public Course(String title, int departmentId) { + this.title = title; + this.departmentId = departmentId; + } + + /** + * Creates a new Course with no initial values. + * Useful for frameworks that use reflection. + */ + public Course() { + } + + /** + * Returns the title of this course. + * + * @return the course title + */ + public String getTitle() { + return title; + } + + /** + * Sets the title of this course. + * + * @param title the course title + */ + public void setTitle(String title) { + this.title = title; + } + + /** + * Returns the department ID for this course. + * + * @return the department ID + */ + public int getDepartmentId() { + return departmentId; + } + + /** + * Sets the department ID for this course. + * + * @param departmentId the department ID + */ + public void setDepartmentId(int departmentId) { + this.departmentId = departmentId; + } + + @Override + public String toString() { + return "Course{" + + "title='" + title + '\'' + + ", departmentId=" + departmentId + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Course course = (Course) o; + + if (departmentId != course.departmentId) return false; + return title != null ? title.equals(course.title) : course.title == null; + } + + @Override + public int hashCode() { + int result = title != null ? title.hashCode() : 0; + result = 31 * result + departmentId; + return result; + } +} From c6533f78d438fb4c6b5a21675b04f0f71df565b5 Mon Sep 17 00:00:00 2001 From: David Whitlock Date: Sun, 14 Dec 2025 11:14:39 -0800 Subject: [PATCH 02/36] A simple Data Access Object for persisting Course objects to an embedded H2 Database. --- examples/pom.xml | 5 + .../java/edu/pdx/cs/joy/jdbc/CourseDAO.java | 100 ++++++++++++++++ .../edu/pdx/cs/joy/jdbc/CourseDAOTest.java | 111 ++++++++++++++++++ 3 files changed, 216 insertions(+) create mode 100644 examples/src/main/java/edu/pdx/cs/joy/jdbc/CourseDAO.java create mode 100644 examples/src/test/java/edu/pdx/cs/joy/jdbc/CourseDAOTest.java diff --git a/examples/pom.xml b/examples/pom.xml index f4252b19d..dfe7ec84d 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -42,6 +42,11 @@ 4.0.5 runtime + + com.h2database + h2 + 2.2.224 + io.github.davidwhitlock.joy projects diff --git a/examples/src/main/java/edu/pdx/cs/joy/jdbc/CourseDAO.java b/examples/src/main/java/edu/pdx/cs/joy/jdbc/CourseDAO.java new file mode 100644 index 000000000..72844b74e --- /dev/null +++ b/examples/src/main/java/edu/pdx/cs/joy/jdbc/CourseDAO.java @@ -0,0 +1,100 @@ +package edu.pdx.cs.joy.jdbc; + +import java.sql.*; +import java.util.ArrayList; +import java.util.List; + +/** + * Data Access Object for managing Course entities in the database. + * Demonstrates basic JDBC operations: CREATE, READ. + */ +public class CourseDAO { + + private final Connection connection; + + /** + * Creates a new CourseDAO with the specified database connection. + * + * @param connection the database connection to use + */ + public CourseDAO(Connection connection) { + this.connection = connection; + } + + /** + * Saves a course to the database. + * + * @param course the course to save + * @throws SQLException if a database error occurs + */ + public void save(Course course) throws SQLException { + String sql = "INSERT INTO courses (title, department_id) VALUES (?, ?)"; + + try (PreparedStatement statement = connection.prepareStatement(sql)) { + statement.setString(1, course.getTitle()); + statement.setInt(2, course.getDepartmentId()); + statement.executeUpdate(); + } + } + + /** + * Finds a course by its title. + * + * @param title the title to search for + * @return the course with the given title, or null if not found + * @throws SQLException if a database error occurs + */ + public Course findByTitle(String title) throws SQLException { + String sql = "SELECT title, department_id FROM courses WHERE title = ?"; + + try (PreparedStatement statement = connection.prepareStatement(sql)) { + statement.setString(1, title); + + try (ResultSet resultSet = statement.executeQuery()) { + if (resultSet.next()) { + return extractCourseFromResultSet(resultSet); + } + } + } + + return null; + } + + /** + * Finds all courses associated with a specific department. + * + * @param departmentId the department ID to search for + * @return a list of courses in the department + * @throws SQLException if a database error occurs + */ + public List findByDepartmentId(int departmentId) throws SQLException { + List courses = new ArrayList<>(); + String sql = "SELECT title, department_id FROM courses WHERE department_id = ?"; + + try (PreparedStatement statement = connection.prepareStatement(sql)) { + statement.setInt(1, departmentId); + + try (ResultSet resultSet = statement.executeQuery()) { + while (resultSet.next()) { + courses.add(extractCourseFromResultSet(resultSet)); + } + } + } + + return courses; + } + + /** + * Extracts a Course object from the current row of a ResultSet. + * + * @param resultSet the result set positioned at a course row + * @return a Course object with data from the result set + * @throws SQLException if a database error occurs + */ + private Course extractCourseFromResultSet(ResultSet resultSet) throws SQLException { + String title = resultSet.getString("title"); + int departmentId = resultSet.getInt("department_id"); + return new Course(title, departmentId); + } +} + diff --git a/examples/src/test/java/edu/pdx/cs/joy/jdbc/CourseDAOTest.java b/examples/src/test/java/edu/pdx/cs/joy/jdbc/CourseDAOTest.java new file mode 100644 index 000000000..24dde7841 --- /dev/null +++ b/examples/src/test/java/edu/pdx/cs/joy/jdbc/CourseDAOTest.java @@ -0,0 +1,111 @@ +package edu.pdx.cs.joy.jdbc; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.List; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; + +public class CourseDAOTest { + + private Connection connection; + private CourseDAO courseDAO; + + @BeforeEach + public void setUp() throws SQLException { + // Create an in-memory H2 database + connection = DriverManager.getConnection("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1"); + + // Drop the table if it exists from a previous test, then create it + try (Statement statement = connection.createStatement()) { + statement.execute("DROP TABLE IF EXISTS courses"); + statement.execute( + "CREATE TABLE courses (" + + " id IDENTITY PRIMARY KEY," + + " title VARCHAR(255) NOT NULL," + + " department_id INTEGER NOT NULL" + + ")" + ); + } + + // Initialize the DAO with the connection + courseDAO = new CourseDAO(connection); + } + + @AfterEach + public void tearDown() throws SQLException { + if (connection != null && !connection.isClosed()) { + // Drop the table and close the connection + try (Statement statement = connection.createStatement()) { + statement.execute("DROP TABLE IF EXISTS courses"); + } + connection.close(); + } + } + + @Test + public void testPersistAndFetchCourse() throws SQLException { + // Create a course + int csDepartmentId = 101; + String javaCourseName = "Introduction to Java"; + Course course = new Course(javaCourseName, csDepartmentId); + + // Persist the course + courseDAO.save(course); + + // Fetch the course by title + Course fetchedCourse = courseDAO.findByTitle(javaCourseName); + + // Validate the fetched course using Hamcrest assertions + assertThat(fetchedCourse, is(notNullValue())); + assertThat(fetchedCourse.getTitle(), is(equalTo(javaCourseName))); + assertThat(fetchedCourse.getDepartmentId(), is(equalTo(csDepartmentId))); + } + + @Test + public void testFetchNonExistentCourse() throws SQLException { + // Try to fetch a course that doesn't exist + Course fetchedCourse = courseDAO.findByTitle("Nonexistent Course"); + + // Validate that null is returned + assertThat(fetchedCourse, is(nullValue())); + } + + @Test + public void testPersistMultipleCourses() throws SQLException { + // Create multiple courses + int csDepartmentId = 102; + int mathDepartmentId = 103; + String dataStructuresName = "Data Structures"; + String algorithmsName = "Algorithms"; + String calculusName = "Calculus"; + + Course course1 = new Course(dataStructuresName, csDepartmentId); + Course course2 = new Course(algorithmsName, csDepartmentId); + Course course3 = new Course(calculusName, mathDepartmentId); + + // Persist all courses + courseDAO.save(course1); + courseDAO.save(course2); + courseDAO.save(course3); + + // Fetch courses by department + List coursesByDept102 = courseDAO.findByDepartmentId(csDepartmentId); + List coursesByDept103 = courseDAO.findByDepartmentId(mathDepartmentId); + + // Validate using Hamcrest matchers + assertThat(coursesByDept102, hasSize(2)); + assertThat(coursesByDept102, hasItem(hasProperty("title", is(dataStructuresName)))); + assertThat(coursesByDept102, hasItem(hasProperty("title", is(algorithmsName)))); + + assertThat(coursesByDept103, hasSize(1)); + assertThat(coursesByDept103, hasItem(hasProperty("title", is(calculusName)))); + } +} From c1010e2c2936db59d233ebfe23a3bdd46bff32a8 Mon Sep 17 00:00:00 2001 From: David Whitlock Date: Sun, 14 Dec 2025 11:21:23 -0800 Subject: [PATCH 03/36] Move code to create and drop database to the DAO. --- .../java/edu/pdx/cs/joy/jdbc/CourseDAO.java | 30 +++++++++++++++++++ .../edu/pdx/cs/joy/jdbc/CourseDAOTest.java | 17 ++--------- 2 files changed, 33 insertions(+), 14 deletions(-) diff --git a/examples/src/main/java/edu/pdx/cs/joy/jdbc/CourseDAO.java b/examples/src/main/java/edu/pdx/cs/joy/jdbc/CourseDAO.java index 72844b74e..88687e390 100644 --- a/examples/src/main/java/edu/pdx/cs/joy/jdbc/CourseDAO.java +++ b/examples/src/main/java/edu/pdx/cs/joy/jdbc/CourseDAO.java @@ -21,6 +21,36 @@ public CourseDAO(Connection connection) { this.connection = connection; } + /** + * Drops the courses table from the database if it exists. + * + * @param connection the database connection to use + * @throws SQLException if a database error occurs + */ + public static void dropTable(Connection connection) throws SQLException { + try (Statement statement = connection.createStatement()) { + statement.execute("DROP TABLE IF EXISTS courses"); + } + } + + /** + * Creates the courses table in the database. + * + * @param connection the database connection to use + * @throws SQLException if a database error occurs + */ + public static void createTable(Connection connection) throws SQLException { + try (Statement statement = connection.createStatement()) { + statement.execute( + "CREATE TABLE courses (" + + " id IDENTITY PRIMARY KEY," + + " title VARCHAR(255) NOT NULL," + + " department_id INTEGER NOT NULL" + + ")" + ); + } + } + /** * Saves a course to the database. * diff --git a/examples/src/test/java/edu/pdx/cs/joy/jdbc/CourseDAOTest.java b/examples/src/test/java/edu/pdx/cs/joy/jdbc/CourseDAOTest.java index 24dde7841..d3f2807a0 100644 --- a/examples/src/test/java/edu/pdx/cs/joy/jdbc/CourseDAOTest.java +++ b/examples/src/test/java/edu/pdx/cs/joy/jdbc/CourseDAOTest.java @@ -7,7 +7,6 @@ import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; -import java.sql.Statement; import java.util.List; import static org.hamcrest.MatcherAssert.assertThat; @@ -24,16 +23,8 @@ public void setUp() throws SQLException { connection = DriverManager.getConnection("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1"); // Drop the table if it exists from a previous test, then create it - try (Statement statement = connection.createStatement()) { - statement.execute("DROP TABLE IF EXISTS courses"); - statement.execute( - "CREATE TABLE courses (" + - " id IDENTITY PRIMARY KEY," + - " title VARCHAR(255) NOT NULL," + - " department_id INTEGER NOT NULL" + - ")" - ); - } + CourseDAO.dropTable(connection); + CourseDAO.createTable(connection); // Initialize the DAO with the connection courseDAO = new CourseDAO(connection); @@ -43,9 +34,7 @@ public void setUp() throws SQLException { public void tearDown() throws SQLException { if (connection != null && !connection.isClosed()) { // Drop the table and close the connection - try (Statement statement = connection.createStatement()) { - statement.execute("DROP TABLE IF EXISTS courses"); - } + CourseDAO.dropTable(connection); connection.close(); } } From c8ce0c4107a6056a30cbc822267b8d45e3b267e4 Mon Sep 17 00:00:00 2001 From: David Whitlock Date: Sun, 14 Dec 2025 11:36:50 -0800 Subject: [PATCH 04/36] Create an integration test that validates how to persist an H2 database to a file on the local file system. --- .../java/edu/pdx/cs/joy/jdbc/CourseDAOIT.java | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 examples/src/it/java/edu/pdx/cs/joy/jdbc/CourseDAOIT.java diff --git a/examples/src/it/java/edu/pdx/cs/joy/jdbc/CourseDAOIT.java b/examples/src/it/java/edu/pdx/cs/joy/jdbc/CourseDAOIT.java new file mode 100644 index 000000000..9c99c2892 --- /dev/null +++ b/examples/src/it/java/edu/pdx/cs/joy/jdbc/CourseDAOIT.java @@ -0,0 +1,95 @@ +package edu.pdx.cs.joy.jdbc; + +import org.junit.jupiter.api.*; + +import java.io.File; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; + +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class CourseDAOIT { + + private static final String TEST_COURSE_TITLE = "Advanced JDBC Programming"; + private static final int TEST_DEPARTMENT_ID = 201; + + private static String dbFilePath; + private Connection connection; + private CourseDAO courseDAO; + + @BeforeAll + public static void createTable() throws SQLException { + // Create database file in temporary directory + String tempDir = System.getProperty("java.io.tmpdir"); + dbFilePath = tempDir + File.separator + "CourseDAOIT.db"; + + // Connect to the file-based H2 database + Connection connection = DriverManager.getConnection("jdbc:h2:" + dbFilePath); + + // Create the courses table + CourseDAO.createTable(connection); + + connection.close(); + } + + @BeforeEach + public void setUp() throws SQLException { + // Connect to the existing database file + connection = DriverManager.getConnection("jdbc:h2:" + dbFilePath); + courseDAO = new CourseDAO(connection); + } + + @AfterEach + public void tearDown() throws SQLException { + if (connection != null && !connection.isClosed()) { + connection.close(); + } + } + + @AfterAll + public static void cleanUp() throws SQLException { + // Connect one final time to drop the table and clean up + Connection connection = DriverManager.getConnection("jdbc:h2:" + dbFilePath); + CourseDAO.dropTable(connection); + connection.close(); + + // Delete the database files + deleteIfExists(new File(dbFilePath + ".mv.db")); + deleteIfExists(new File(dbFilePath + ".trace.db")); + } + + private static void deleteIfExists(File file) { + if (file.exists()) { + assertThat(file.delete(), is(true)); + } + } + + @Test + @Order(1) + public void testPersistCourse() throws SQLException { + // Create and persist a course + Course course = new Course(TEST_COURSE_TITLE, TEST_DEPARTMENT_ID); + courseDAO.save(course); + + // Verify the course was saved by fetching it in the same test + Course fetchedCourse = courseDAO.findByTitle(TEST_COURSE_TITLE); + assertThat(fetchedCourse, is(notNullValue())); + assertThat(fetchedCourse.getTitle(), is(equalTo(TEST_COURSE_TITLE))); + assertThat(fetchedCourse.getDepartmentId(), is(equalTo(TEST_DEPARTMENT_ID))); + } + + @Test + @Order(2) + public void testFindPersistedCourse() throws SQLException { + // Search for the course that was persisted in the previous test + Course fetchedCourse = courseDAO.findByTitle(TEST_COURSE_TITLE); + + // Validate that the course persisted between test methods + assertThat(fetchedCourse, is(notNullValue())); + assertThat(fetchedCourse.getTitle(), is(equalTo(TEST_COURSE_TITLE))); + assertThat(fetchedCourse.getDepartmentId(), is(equalTo(TEST_DEPARTMENT_ID))); + } +} From 94f11c78f8a97876b45d527dc49c7779281e9d05 Mon Sep 17 00:00:00 2001 From: David Whitlock Date: Sun, 14 Dec 2025 11:43:07 -0800 Subject: [PATCH 05/36] Refactor code that creates the connection to the H2 database to a H2DatabaseHelper class. --- .../java/edu/pdx/cs/joy/jdbc/CourseDAOIT.java | 7 ++-- .../edu/pdx/cs/joy/jdbc/H2DatabaseHelper.java | 37 +++++++++++++++++++ .../edu/pdx/cs/joy/jdbc/CourseDAOTest.java | 3 +- 3 files changed, 41 insertions(+), 6 deletions(-) create mode 100644 examples/src/main/java/edu/pdx/cs/joy/jdbc/H2DatabaseHelper.java diff --git a/examples/src/it/java/edu/pdx/cs/joy/jdbc/CourseDAOIT.java b/examples/src/it/java/edu/pdx/cs/joy/jdbc/CourseDAOIT.java index 9c99c2892..a26e934c2 100644 --- a/examples/src/it/java/edu/pdx/cs/joy/jdbc/CourseDAOIT.java +++ b/examples/src/it/java/edu/pdx/cs/joy/jdbc/CourseDAOIT.java @@ -4,7 +4,6 @@ import java.io.File; import java.sql.Connection; -import java.sql.DriverManager; import java.sql.SQLException; import static org.hamcrest.MatcherAssert.assertThat; @@ -27,7 +26,7 @@ public static void createTable() throws SQLException { dbFilePath = tempDir + File.separator + "CourseDAOIT.db"; // Connect to the file-based H2 database - Connection connection = DriverManager.getConnection("jdbc:h2:" + dbFilePath); + Connection connection = H2DatabaseHelper.createFileBasedConnection(dbFilePath); // Create the courses table CourseDAO.createTable(connection); @@ -38,7 +37,7 @@ public static void createTable() throws SQLException { @BeforeEach public void setUp() throws SQLException { // Connect to the existing database file - connection = DriverManager.getConnection("jdbc:h2:" + dbFilePath); + connection = H2DatabaseHelper.createFileBasedConnection(dbFilePath); courseDAO = new CourseDAO(connection); } @@ -52,7 +51,7 @@ public void tearDown() throws SQLException { @AfterAll public static void cleanUp() throws SQLException { // Connect one final time to drop the table and clean up - Connection connection = DriverManager.getConnection("jdbc:h2:" + dbFilePath); + Connection connection = H2DatabaseHelper.createFileBasedConnection(dbFilePath); CourseDAO.dropTable(connection); connection.close(); diff --git a/examples/src/main/java/edu/pdx/cs/joy/jdbc/H2DatabaseHelper.java b/examples/src/main/java/edu/pdx/cs/joy/jdbc/H2DatabaseHelper.java new file mode 100644 index 000000000..274520395 --- /dev/null +++ b/examples/src/main/java/edu/pdx/cs/joy/jdbc/H2DatabaseHelper.java @@ -0,0 +1,37 @@ +package edu.pdx.cs.joy.jdbc; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; + +/** + * Helper class for creating connections to H2 databases. + * Provides factory methods for both in-memory and file-based H2 databases. + */ +public class H2DatabaseHelper { + + /** + * Creates a connection to an in-memory H2 database. + * The database will persist as long as at least one connection remains open + * due to the DB_CLOSE_DELAY=-1 parameter. + * + * @param databaseName the name of the in-memory database + * @return a connection to the in-memory H2 database + * @throws SQLException if a database error occurs + */ + public static Connection createInMemoryConnection(String databaseName) throws SQLException { + return DriverManager.getConnection("jdbc:h2:mem:" + databaseName + ";DB_CLOSE_DELAY=-1"); + } + + /** + * Creates a connection to a file-based H2 database. + * The database will be persisted to a file at the specified path. + * + * @param filePath the path to the database file (without the .mv.db extension) + * @return a connection to the file-based H2 database + * @throws SQLException if a database error occurs + */ + public static Connection createFileBasedConnection(String filePath) throws SQLException { + return DriverManager.getConnection("jdbc:h2:" + filePath); + } +} diff --git a/examples/src/test/java/edu/pdx/cs/joy/jdbc/CourseDAOTest.java b/examples/src/test/java/edu/pdx/cs/joy/jdbc/CourseDAOTest.java index d3f2807a0..ff1bab0b4 100644 --- a/examples/src/test/java/edu/pdx/cs/joy/jdbc/CourseDAOTest.java +++ b/examples/src/test/java/edu/pdx/cs/joy/jdbc/CourseDAOTest.java @@ -5,7 +5,6 @@ import org.junit.jupiter.api.Test; import java.sql.Connection; -import java.sql.DriverManager; import java.sql.SQLException; import java.util.List; @@ -20,7 +19,7 @@ public class CourseDAOTest { @BeforeEach public void setUp() throws SQLException { // Create an in-memory H2 database - connection = DriverManager.getConnection("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1"); + connection = H2DatabaseHelper.createInMemoryConnection("test"); // Drop the table if it exists from a previous test, then create it CourseDAO.dropTable(connection); From 98a6597b498b3a01d01b79a36c4a8a802b6076cb Mon Sep 17 00:00:00 2001 From: David Whitlock Date: Sun, 14 Dec 2025 11:49:03 -0800 Subject: [PATCH 06/36] Added a Department domain object that is persisted to the database. --- .../java/edu/pdx/cs/joy/jdbc/Department.java | 91 +++++++++++ .../edu/pdx/cs/joy/jdbc/DepartmentDAO.java | 148 ++++++++++++++++++ .../pdx/cs/joy/jdbc/DepartmentDAOTest.java | 137 ++++++++++++++++ 3 files changed, 376 insertions(+) create mode 100644 examples/src/main/java/edu/pdx/cs/joy/jdbc/Department.java create mode 100644 examples/src/main/java/edu/pdx/cs/joy/jdbc/DepartmentDAO.java create mode 100644 examples/src/test/java/edu/pdx/cs/joy/jdbc/DepartmentDAOTest.java diff --git a/examples/src/main/java/edu/pdx/cs/joy/jdbc/Department.java b/examples/src/main/java/edu/pdx/cs/joy/jdbc/Department.java new file mode 100644 index 000000000..757d31803 --- /dev/null +++ b/examples/src/main/java/edu/pdx/cs/joy/jdbc/Department.java @@ -0,0 +1,91 @@ +package edu.pdx.cs.joy.jdbc; + +/** + * Represents a department in a college course catalog. + * Each department has a unique ID and a name. + */ +public class Department { + private int id; + private String name; + + /** + * Creates a new Department with the specified ID and name. + * + * @param id the unique identifier for the department + * @param name the name of the department + */ + public Department(int id, String name) { + this.id = id; + this.name = name; + } + + /** + * Creates a new Department with no initial values. + * Useful for frameworks that use reflection. + */ + public Department() { + } + + /** + * Returns the ID of this department. + * + * @return the department ID + */ + public int getId() { + return id; + } + + /** + * Sets the ID of this department. + * + * @param id the department ID + */ + public void setId(int id) { + this.id = id; + } + + /** + * Returns the name of this department. + * + * @return the department name + */ + public String getName() { + return name; + } + + /** + * Sets the name of this department. + * + * @param name the department name + */ + public void setName(String name) { + this.name = name; + } + + @Override + public String toString() { + return "Department{" + + "id=" + id + + ", name='" + name + '\'' + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Department that = (Department) o; + + if (id != that.id) return false; + return name != null ? name.equals(that.name) : that.name == null; + } + + @Override + public int hashCode() { + int result = id; + result = 31 * result + (name != null ? name.hashCode() : 0); + return result; + } +} + diff --git a/examples/src/main/java/edu/pdx/cs/joy/jdbc/DepartmentDAO.java b/examples/src/main/java/edu/pdx/cs/joy/jdbc/DepartmentDAO.java new file mode 100644 index 000000000..3a1161b9d --- /dev/null +++ b/examples/src/main/java/edu/pdx/cs/joy/jdbc/DepartmentDAO.java @@ -0,0 +1,148 @@ +package edu.pdx.cs.joy.jdbc; + +import java.sql.*; +import java.util.ArrayList; +import java.util.List; + +/** + * Data Access Object for managing Department entities in the database. + * Demonstrates basic JDBC operations: CREATE, READ. + */ +public class DepartmentDAO { + + private final Connection connection; + + /** + * Creates a new DepartmentDAO with the specified database connection. + * + * @param connection the database connection to use + */ + public DepartmentDAO(Connection connection) { + this.connection = connection; + } + + /** + * Drops the departments table from the database if it exists. + * + * @param connection the database connection to use + * @throws SQLException if a database error occurs + */ + public static void dropTable(Connection connection) throws SQLException { + try (Statement statement = connection.createStatement()) { + statement.execute("DROP TABLE IF EXISTS departments"); + } + } + + /** + * Creates the departments table in the database. + * + * @param connection the database connection to use + * @throws SQLException if a database error occurs + */ + public static void createTable(Connection connection) throws SQLException { + try (Statement statement = connection.createStatement()) { + statement.execute( + "CREATE TABLE departments (" + + " id INTEGER PRIMARY KEY," + + " name VARCHAR(255) NOT NULL" + + ")" + ); + } + } + + /** + * Saves a department to the database. + * + * @param department the department to save + * @throws SQLException if a database error occurs + */ + public void save(Department department) throws SQLException { + String sql = "INSERT INTO departments (id, name) VALUES (?, ?)"; + + try (PreparedStatement statement = connection.prepareStatement(sql)) { + statement.setInt(1, department.getId()); + statement.setString(2, department.getName()); + statement.executeUpdate(); + } + } + + /** + * Finds a department by its ID. + * + * @param id the ID to search for + * @return the department with the given ID, or null if not found + * @throws SQLException if a database error occurs + */ + public Department findById(int id) throws SQLException { + String sql = "SELECT id, name FROM departments WHERE id = ?"; + + try (PreparedStatement statement = connection.prepareStatement(sql)) { + statement.setInt(1, id); + + try (ResultSet resultSet = statement.executeQuery()) { + if (resultSet.next()) { + return extractDepartmentFromResultSet(resultSet); + } + } + } + + return null; + } + + /** + * Finds a department by its name. + * + * @param name the name to search for + * @return the department with the given name, or null if not found + * @throws SQLException if a database error occurs + */ + public Department findByName(String name) throws SQLException { + String sql = "SELECT id, name FROM departments WHERE name = ?"; + + try (PreparedStatement statement = connection.prepareStatement(sql)) { + statement.setString(1, name); + + try (ResultSet resultSet = statement.executeQuery()) { + if (resultSet.next()) { + return extractDepartmentFromResultSet(resultSet); + } + } + } + + return null; + } + + /** + * Finds all departments in the database. + * + * @return a list of all departments + * @throws SQLException if a database error occurs + */ + public List findAll() throws SQLException { + List departments = new ArrayList<>(); + String sql = "SELECT id, name FROM departments ORDER BY id"; + + try (Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery(sql)) { + while (resultSet.next()) { + departments.add(extractDepartmentFromResultSet(resultSet)); + } + } + + return departments; + } + + /** + * Extracts a Department object from the current row of a ResultSet. + * + * @param resultSet the result set positioned at a department row + * @return a Department object with data from the result set + * @throws SQLException if a database error occurs + */ + private Department extractDepartmentFromResultSet(ResultSet resultSet) throws SQLException { + int id = resultSet.getInt("id"); + String name = resultSet.getString("name"); + return new Department(id, name); + } +} + diff --git a/examples/src/test/java/edu/pdx/cs/joy/jdbc/DepartmentDAOTest.java b/examples/src/test/java/edu/pdx/cs/joy/jdbc/DepartmentDAOTest.java new file mode 100644 index 000000000..b0023929a --- /dev/null +++ b/examples/src/test/java/edu/pdx/cs/joy/jdbc/DepartmentDAOTest.java @@ -0,0 +1,137 @@ +package edu.pdx.cs.joy.jdbc; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.List; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; + +public class DepartmentDAOTest { + + private Connection connection; + private DepartmentDAO departmentDAO; + + @BeforeEach + public void setUp() throws SQLException { + // Create an in-memory H2 database + connection = H2DatabaseHelper.createInMemoryConnection("test"); + + // Drop the table if it exists from a previous test, then create it + DepartmentDAO.dropTable(connection); + DepartmentDAO.createTable(connection); + + // Initialize the DAO with the connection + departmentDAO = new DepartmentDAO(connection); + } + + @AfterEach + public void tearDown() throws SQLException { + if (connection != null && !connection.isClosed()) { + // Drop the table and close the connection + DepartmentDAO.dropTable(connection); + connection.close(); + } + } + + @Test + public void testPersistAndFetchDepartmentById() throws SQLException { + // Create a department + Department department = new Department(101, "Computer Science"); + + // Persist the department + departmentDAO.save(department); + + // Fetch the department by ID + Department fetchedDepartment = departmentDAO.findById(101); + + // Validate the fetched department using Hamcrest assertions + assertThat(fetchedDepartment, is(notNullValue())); + assertThat(fetchedDepartment.getId(), is(equalTo(101))); + assertThat(fetchedDepartment.getName(), is(equalTo("Computer Science"))); + } + + @Test + public void testFindDepartmentByName() throws SQLException { + // Create and persist a department + Department department = new Department(102, "Mathematics"); + departmentDAO.save(department); + + // Fetch the department by name + Department fetchedDepartment = departmentDAO.findByName("Mathematics"); + + // Validate the fetched department + assertThat(fetchedDepartment, is(notNullValue())); + assertThat(fetchedDepartment.getId(), is(equalTo(102))); + assertThat(fetchedDepartment.getName(), is(equalTo("Mathematics"))); + } + + @Test + public void testFetchNonExistentDepartmentById() throws SQLException { + // Try to fetch a department that doesn't exist + Department fetchedDepartment = departmentDAO.findById(999); + + // Validate that null is returned + assertThat(fetchedDepartment, is(nullValue())); + } + + @Test + public void testFetchNonExistentDepartmentByName() throws SQLException { + // Try to fetch a department that doesn't exist + Department fetchedDepartment = departmentDAO.findByName("Nonexistent Department"); + + // Validate that null is returned + assertThat(fetchedDepartment, is(nullValue())); + } + + @Test + public void testFindAllDepartments() throws SQLException { + // Create multiple departments + Department dept1 = new Department(101, "Computer Science"); + Department dept2 = new Department(102, "Mathematics"); + Department dept3 = new Department(103, "Physics"); + + // Persist all departments + departmentDAO.save(dept1); + departmentDAO.save(dept2); + departmentDAO.save(dept3); + + // Fetch all departments + List allDepartments = departmentDAO.findAll(); + + // Validate using Hamcrest matchers + assertThat(allDepartments, hasSize(3)); + assertThat(allDepartments, hasItem(hasProperty("name", is("Computer Science")))); + assertThat(allDepartments, hasItem(hasProperty("name", is("Mathematics")))); + assertThat(allDepartments, hasItem(hasProperty("name", is("Physics")))); + assertThat(allDepartments, hasItem(hasProperty("id", is(101)))); + assertThat(allDepartments, hasItem(hasProperty("id", is(102)))); + assertThat(allDepartments, hasItem(hasProperty("id", is(103)))); + } + + @Test + public void testFindAllReturnsEmptyListWhenNoDepartments() throws SQLException { + // Fetch all departments from empty table + List allDepartments = departmentDAO.findAll(); + + // Validate that an empty list is returned + assertThat(allDepartments, is(empty())); + } + + @Test + public void testDepartmentEquality() throws SQLException { + // Create and persist a department + Department original = new Department(104, "Engineering"); + departmentDAO.save(original); + + // Fetch the department + Department fetched = departmentDAO.findById(104); + + // Validate that the objects are equal + assertThat(fetched, is(equalTo(original))); + } +} From 209f5019d19cc8d1975b7262f57b954a7c110111 Mon Sep 17 00:00:00 2001 From: David Whitlock Date: Sun, 14 Dec 2025 11:54:05 -0800 Subject: [PATCH 07/36] Use the Department class as an example for persisting objects to a file-backed database. --- ...{CourseDAOIT.java => DepartmentDAOIT.java} | 52 +++++++++---------- 1 file changed, 26 insertions(+), 26 deletions(-) rename examples/src/it/java/edu/pdx/cs/joy/jdbc/{CourseDAOIT.java => DepartmentDAOIT.java} (53%) diff --git a/examples/src/it/java/edu/pdx/cs/joy/jdbc/CourseDAOIT.java b/examples/src/it/java/edu/pdx/cs/joy/jdbc/DepartmentDAOIT.java similarity index 53% rename from examples/src/it/java/edu/pdx/cs/joy/jdbc/CourseDAOIT.java rename to examples/src/it/java/edu/pdx/cs/joy/jdbc/DepartmentDAOIT.java index a26e934c2..d8085b04e 100644 --- a/examples/src/it/java/edu/pdx/cs/joy/jdbc/CourseDAOIT.java +++ b/examples/src/it/java/edu/pdx/cs/joy/jdbc/DepartmentDAOIT.java @@ -10,26 +10,26 @@ import static org.hamcrest.Matchers.*; @TestMethodOrder(MethodOrderer.OrderAnnotation.class) -public class CourseDAOIT { +public class DepartmentDAOIT { - private static final String TEST_COURSE_TITLE = "Advanced JDBC Programming"; private static final int TEST_DEPARTMENT_ID = 201; + private static final String TEST_DEPARTMENT_NAME = "Computer Science"; private static String dbFilePath; private Connection connection; - private CourseDAO courseDAO; + private DepartmentDAO departmentDAO; @BeforeAll public static void createTable() throws SQLException { // Create database file in temporary directory String tempDir = System.getProperty("java.io.tmpdir"); - dbFilePath = tempDir + File.separator + "CourseDAOIT.db"; + dbFilePath = tempDir + File.separator + "DepartmentDAOIT.db"; // Connect to the file-based H2 database Connection connection = H2DatabaseHelper.createFileBasedConnection(dbFilePath); - // Create the courses table - CourseDAO.createTable(connection); + // Create the departments table + DepartmentDAO.createTable(connection); connection.close(); } @@ -38,7 +38,7 @@ public static void createTable() throws SQLException { public void setUp() throws SQLException { // Connect to the existing database file connection = H2DatabaseHelper.createFileBasedConnection(dbFilePath); - courseDAO = new CourseDAO(connection); + departmentDAO = new DepartmentDAO(connection); } @AfterEach @@ -52,7 +52,7 @@ public void tearDown() throws SQLException { public static void cleanUp() throws SQLException { // Connect one final time to drop the table and clean up Connection connection = H2DatabaseHelper.createFileBasedConnection(dbFilePath); - CourseDAO.dropTable(connection); + DepartmentDAO.dropTable(connection); connection.close(); // Delete the database files @@ -68,27 +68,27 @@ private static void deleteIfExists(File file) { @Test @Order(1) - public void testPersistCourse() throws SQLException { - // Create and persist a course - Course course = new Course(TEST_COURSE_TITLE, TEST_DEPARTMENT_ID); - courseDAO.save(course); - - // Verify the course was saved by fetching it in the same test - Course fetchedCourse = courseDAO.findByTitle(TEST_COURSE_TITLE); - assertThat(fetchedCourse, is(notNullValue())); - assertThat(fetchedCourse.getTitle(), is(equalTo(TEST_COURSE_TITLE))); - assertThat(fetchedCourse.getDepartmentId(), is(equalTo(TEST_DEPARTMENT_ID))); + public void testPersistDepartment() throws SQLException { + // Create and persist a department + Department department = new Department(TEST_DEPARTMENT_ID, TEST_DEPARTMENT_NAME); + departmentDAO.save(department); + + // Verify the department was saved by fetching it in the same test + Department fetchedDepartment = departmentDAO.findById(TEST_DEPARTMENT_ID); + assertThat(fetchedDepartment, is(notNullValue())); + assertThat(fetchedDepartment.getId(), is(equalTo(TEST_DEPARTMENT_ID))); + assertThat(fetchedDepartment.getName(), is(equalTo(TEST_DEPARTMENT_NAME))); } @Test @Order(2) - public void testFindPersistedCourse() throws SQLException { - // Search for the course that was persisted in the previous test - Course fetchedCourse = courseDAO.findByTitle(TEST_COURSE_TITLE); - - // Validate that the course persisted between test methods - assertThat(fetchedCourse, is(notNullValue())); - assertThat(fetchedCourse.getTitle(), is(equalTo(TEST_COURSE_TITLE))); - assertThat(fetchedCourse.getDepartmentId(), is(equalTo(TEST_DEPARTMENT_ID))); + public void testFindPersistedDepartment() throws SQLException { + // Search for the department that was persisted in the previous test + Department fetchedDepartment = departmentDAO.findById(TEST_DEPARTMENT_ID); + + // Validate that the department persisted between test methods + assertThat(fetchedDepartment, is(notNullValue())); + assertThat(fetchedDepartment.getId(), is(equalTo(TEST_DEPARTMENT_ID))); + assertThat(fetchedDepartment.getName(), is(equalTo(TEST_DEPARTMENT_NAME))); } } From e647012e997c34a4f46fb5c31f43be92a9d0ca47 Mon Sep 17 00:00:00 2001 From: David Whitlock Date: Sun, 14 Dec 2025 12:03:52 -0800 Subject: [PATCH 08/36] Make a Course's "departmentId" a foreign key to the "id" field of the Department table. --- .../java/edu/pdx/cs/joy/jdbc/CourseDAO.java | 3 +- .../edu/pdx/cs/joy/jdbc/CourseDAOTest.java | 44 ++++++++++++++++--- 2 files changed, 41 insertions(+), 6 deletions(-) diff --git a/examples/src/main/java/edu/pdx/cs/joy/jdbc/CourseDAO.java b/examples/src/main/java/edu/pdx/cs/joy/jdbc/CourseDAO.java index 88687e390..3101a1cca 100644 --- a/examples/src/main/java/edu/pdx/cs/joy/jdbc/CourseDAO.java +++ b/examples/src/main/java/edu/pdx/cs/joy/jdbc/CourseDAO.java @@ -45,7 +45,8 @@ public static void createTable(Connection connection) throws SQLException { "CREATE TABLE courses (" + " id IDENTITY PRIMARY KEY," + " title VARCHAR(255) NOT NULL," + - " department_id INTEGER NOT NULL" + + " department_id INTEGER NOT NULL," + + " FOREIGN KEY (department_id) REFERENCES departments(id)" + ")" ); } diff --git a/examples/src/test/java/edu/pdx/cs/joy/jdbc/CourseDAOTest.java b/examples/src/test/java/edu/pdx/cs/joy/jdbc/CourseDAOTest.java index ff1bab0b4..bf17ab2cc 100644 --- a/examples/src/test/java/edu/pdx/cs/joy/jdbc/CourseDAOTest.java +++ b/examples/src/test/java/edu/pdx/cs/joy/jdbc/CourseDAOTest.java @@ -10,38 +10,52 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.*; +import static org.junit.jupiter.api.Assertions.assertThrows; public class CourseDAOTest { private Connection connection; private CourseDAO courseDAO; + private DepartmentDAO departmentDAO; @BeforeEach public void setUp() throws SQLException { // Create an in-memory H2 database connection = H2DatabaseHelper.createInMemoryConnection("test"); - // Drop the table if it exists from a previous test, then create it + // Drop tables if they exist from a previous test, then create them + // Note: Must drop courses first due to foreign key constraint CourseDAO.dropTable(connection); + DepartmentDAO.dropTable(connection); + + // Create departments table first, then courses (due to foreign key) + DepartmentDAO.createTable(connection); CourseDAO.createTable(connection); - // Initialize the DAO with the connection + // Initialize the DAOs with the connection courseDAO = new CourseDAO(connection); + departmentDAO = new DepartmentDAO(connection); } @AfterEach public void tearDown() throws SQLException { if (connection != null && !connection.isClosed()) { - // Drop the table and close the connection + // Drop tables and close the connection + // Note: Must drop courses first due to foreign key constraint CourseDAO.dropTable(connection); + DepartmentDAO.dropTable(connection); connection.close(); } } @Test public void testPersistAndFetchCourse() throws SQLException { - // Create a course + // Create and persist a department first (required for foreign key) int csDepartmentId = 101; + Department department = new Department(csDepartmentId, "Computer Science"); + departmentDAO.save(department); + + // Create a course String javaCourseName = "Introduction to Java"; Course course = new Course(javaCourseName, csDepartmentId); @@ -68,9 +82,16 @@ public void testFetchNonExistentCourse() throws SQLException { @Test public void testPersistMultipleCourses() throws SQLException { - // Create multiple courses + // Create and persist departments first (required for foreign key) int csDepartmentId = 102; int mathDepartmentId = 103; + + Department csDepartment = new Department(csDepartmentId, "Computer Science"); + Department mathDepartment = new Department(mathDepartmentId, "Mathematics"); + departmentDAO.save(csDepartment); + departmentDAO.save(mathDepartment); + + // Create multiple courses String dataStructuresName = "Data Structures"; String algorithmsName = "Algorithms"; String calculusName = "Calculus"; @@ -96,4 +117,17 @@ public void testPersistMultipleCourses() throws SQLException { assertThat(coursesByDept103, hasSize(1)); assertThat(coursesByDept103, hasItem(hasProperty("title", is(calculusName)))); } + + @Test + public void testForeignKeyConstraintPreventsInvalidDepartmentId() { + // Try to create a course with a non-existent department ID + Course course = new Course("Database Systems", 999); + + // Attempting to save should throw an SQLException due to foreign key constraint + SQLException exception = assertThrows(SQLException.class, () -> { + courseDAO.save(course); + }); + assertThat(exception.getMessage(), containsString("Referential integrity")); + } + } From fdc208a80397bd42d371893804be2d7b574f8d35 Mon Sep 17 00:00:00 2001 From: David Whitlock Date: Sun, 14 Dec 2025 12:11:08 -0800 Subject: [PATCH 09/36] Automatically generate the "id" for Department objects. --- .../edu/pdx/cs/joy/jdbc/DepartmentDAOIT.java | 22 ++++++--- .../edu/pdx/cs/joy/jdbc/DepartmentDAO.java | 20 ++++++-- .../edu/pdx/cs/joy/jdbc/CourseDAOTest.java | 20 +++++--- .../pdx/cs/joy/jdbc/DepartmentDAOTest.java | 49 ++++++++++++------- 4 files changed, 74 insertions(+), 37 deletions(-) diff --git a/examples/src/it/java/edu/pdx/cs/joy/jdbc/DepartmentDAOIT.java b/examples/src/it/java/edu/pdx/cs/joy/jdbc/DepartmentDAOIT.java index d8085b04e..2559b8efc 100644 --- a/examples/src/it/java/edu/pdx/cs/joy/jdbc/DepartmentDAOIT.java +++ b/examples/src/it/java/edu/pdx/cs/joy/jdbc/DepartmentDAOIT.java @@ -12,8 +12,8 @@ @TestMethodOrder(MethodOrderer.OrderAnnotation.class) public class DepartmentDAOIT { - private static final int TEST_DEPARTMENT_ID = 201; private static final String TEST_DEPARTMENT_NAME = "Computer Science"; + private static int generatedDepartmentId; private static String dbFilePath; private Connection connection; @@ -69,14 +69,21 @@ private static void deleteIfExists(File file) { @Test @Order(1) public void testPersistDepartment() throws SQLException { - // Create and persist a department - Department department = new Department(TEST_DEPARTMENT_ID, TEST_DEPARTMENT_NAME); + // Create and persist a department (ID will be auto-generated) + Department department = new Department(); + department.setName(TEST_DEPARTMENT_NAME); departmentDAO.save(department); + // Store the auto-generated ID for use in subsequent tests + generatedDepartmentId = department.getId(); + + // Verify that an ID was auto-generated + assertThat(generatedDepartmentId, is(greaterThan(0))); + // Verify the department was saved by fetching it in the same test - Department fetchedDepartment = departmentDAO.findById(TEST_DEPARTMENT_ID); + Department fetchedDepartment = departmentDAO.findById(generatedDepartmentId); assertThat(fetchedDepartment, is(notNullValue())); - assertThat(fetchedDepartment.getId(), is(equalTo(TEST_DEPARTMENT_ID))); + assertThat(fetchedDepartment.getId(), is(equalTo(generatedDepartmentId))); assertThat(fetchedDepartment.getName(), is(equalTo(TEST_DEPARTMENT_NAME))); } @@ -84,11 +91,12 @@ public void testPersistDepartment() throws SQLException { @Order(2) public void testFindPersistedDepartment() throws SQLException { // Search for the department that was persisted in the previous test - Department fetchedDepartment = departmentDAO.findById(TEST_DEPARTMENT_ID); + // using the auto-generated ID + Department fetchedDepartment = departmentDAO.findById(generatedDepartmentId); // Validate that the department persisted between test methods assertThat(fetchedDepartment, is(notNullValue())); - assertThat(fetchedDepartment.getId(), is(equalTo(TEST_DEPARTMENT_ID))); + assertThat(fetchedDepartment.getId(), is(equalTo(generatedDepartmentId))); assertThat(fetchedDepartment.getName(), is(equalTo(TEST_DEPARTMENT_NAME))); } } diff --git a/examples/src/main/java/edu/pdx/cs/joy/jdbc/DepartmentDAO.java b/examples/src/main/java/edu/pdx/cs/joy/jdbc/DepartmentDAO.java index 3a1161b9d..6fa948e55 100644 --- a/examples/src/main/java/edu/pdx/cs/joy/jdbc/DepartmentDAO.java +++ b/examples/src/main/java/edu/pdx/cs/joy/jdbc/DepartmentDAO.java @@ -43,7 +43,7 @@ public static void createTable(Connection connection) throws SQLException { try (Statement statement = connection.createStatement()) { statement.execute( "CREATE TABLE departments (" + - " id INTEGER PRIMARY KEY," + + " id IDENTITY PRIMARY KEY," + " name VARCHAR(255) NOT NULL" + ")" ); @@ -52,17 +52,27 @@ public static void createTable(Connection connection) throws SQLException { /** * Saves a department to the database. + * The department's ID will be automatically generated by the database. * * @param department the department to save * @throws SQLException if a database error occurs */ public void save(Department department) throws SQLException { - String sql = "INSERT INTO departments (id, name) VALUES (?, ?)"; + String sql = "INSERT INTO departments (name) VALUES (?)"; - try (PreparedStatement statement = connection.prepareStatement(sql)) { - statement.setInt(1, department.getId()); - statement.setString(2, department.getName()); + try (PreparedStatement statement = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { + statement.setString(1, department.getName()); statement.executeUpdate(); + + // Retrieve the auto-generated ID and set it on the department object + try (ResultSet generatedKeys = statement.getGeneratedKeys()) { + if (generatedKeys.next()) { + int generatedId = generatedKeys.getInt(1); + department.setId(generatedId); + } else { + throw new SQLException("Creating department failed, no ID obtained."); + } + } } } diff --git a/examples/src/test/java/edu/pdx/cs/joy/jdbc/CourseDAOTest.java b/examples/src/test/java/edu/pdx/cs/joy/jdbc/CourseDAOTest.java index bf17ab2cc..4e2740709 100644 --- a/examples/src/test/java/edu/pdx/cs/joy/jdbc/CourseDAOTest.java +++ b/examples/src/test/java/edu/pdx/cs/joy/jdbc/CourseDAOTest.java @@ -51,10 +51,13 @@ public void tearDown() throws SQLException { @Test public void testPersistAndFetchCourse() throws SQLException { // Create and persist a department first (required for foreign key) - int csDepartmentId = 101; - Department department = new Department(csDepartmentId, "Computer Science"); + Department department = new Department(); + department.setName("Computer Science"); departmentDAO.save(department); + // Get the auto-generated department ID + int csDepartmentId = department.getId(); + // Create a course String javaCourseName = "Introduction to Java"; Course course = new Course(javaCourseName, csDepartmentId); @@ -83,14 +86,19 @@ public void testFetchNonExistentCourse() throws SQLException { @Test public void testPersistMultipleCourses() throws SQLException { // Create and persist departments first (required for foreign key) - int csDepartmentId = 102; - int mathDepartmentId = 103; + Department csDepartment = new Department(); + csDepartment.setName("Computer Science"); + + Department mathDepartment = new Department(); + mathDepartment.setName("Mathematics"); - Department csDepartment = new Department(csDepartmentId, "Computer Science"); - Department mathDepartment = new Department(mathDepartmentId, "Mathematics"); departmentDAO.save(csDepartment); departmentDAO.save(mathDepartment); + // Get the auto-generated department IDs + int csDepartmentId = csDepartment.getId(); + int mathDepartmentId = mathDepartment.getId(); + // Create multiple courses String dataStructuresName = "Data Structures"; String algorithmsName = "Algorithms"; diff --git a/examples/src/test/java/edu/pdx/cs/joy/jdbc/DepartmentDAOTest.java b/examples/src/test/java/edu/pdx/cs/joy/jdbc/DepartmentDAOTest.java index b0023929a..53162e3c0 100644 --- a/examples/src/test/java/edu/pdx/cs/joy/jdbc/DepartmentDAOTest.java +++ b/examples/src/test/java/edu/pdx/cs/joy/jdbc/DepartmentDAOTest.java @@ -40,25 +40,30 @@ public void tearDown() throws SQLException { @Test public void testPersistAndFetchDepartmentById() throws SQLException { - // Create a department - Department department = new Department(101, "Computer Science"); + // Create a department (ID will be auto-generated) + Department department = new Department(); + department.setName("Computer Science"); // Persist the department departmentDAO.save(department); - // Fetch the department by ID - Department fetchedDepartment = departmentDAO.findById(101); + // Verify that an ID was auto-generated + assertThat(department.getId(), is(greaterThan(0))); + + // Fetch the department by the auto-generated ID + Department fetchedDepartment = departmentDAO.findById(department.getId()); // Validate the fetched department using Hamcrest assertions assertThat(fetchedDepartment, is(notNullValue())); - assertThat(fetchedDepartment.getId(), is(equalTo(101))); + assertThat(fetchedDepartment.getId(), is(equalTo(department.getId()))); assertThat(fetchedDepartment.getName(), is(equalTo("Computer Science"))); } @Test public void testFindDepartmentByName() throws SQLException { - // Create and persist a department - Department department = new Department(102, "Mathematics"); + // Create and persist a department (ID will be auto-generated) + Department department = new Department(); + department.setName("Mathematics"); departmentDAO.save(department); // Fetch the department by name @@ -66,7 +71,7 @@ public void testFindDepartmentByName() throws SQLException { // Validate the fetched department assertThat(fetchedDepartment, is(notNullValue())); - assertThat(fetchedDepartment.getId(), is(equalTo(102))); + assertThat(fetchedDepartment.getId(), is(equalTo(department.getId()))); assertThat(fetchedDepartment.getName(), is(equalTo("Mathematics"))); } @@ -90,10 +95,15 @@ public void testFetchNonExistentDepartmentByName() throws SQLException { @Test public void testFindAllDepartments() throws SQLException { - // Create multiple departments - Department dept1 = new Department(101, "Computer Science"); - Department dept2 = new Department(102, "Mathematics"); - Department dept3 = new Department(103, "Physics"); + // Create multiple departments (IDs will be auto-generated) + Department dept1 = new Department(); + dept1.setName("Computer Science"); + + Department dept2 = new Department(); + dept2.setName("Mathematics"); + + Department dept3 = new Department(); + dept3.setName("Physics"); // Persist all departments departmentDAO.save(dept1); @@ -108,9 +118,9 @@ public void testFindAllDepartments() throws SQLException { assertThat(allDepartments, hasItem(hasProperty("name", is("Computer Science")))); assertThat(allDepartments, hasItem(hasProperty("name", is("Mathematics")))); assertThat(allDepartments, hasItem(hasProperty("name", is("Physics")))); - assertThat(allDepartments, hasItem(hasProperty("id", is(101)))); - assertThat(allDepartments, hasItem(hasProperty("id", is(102)))); - assertThat(allDepartments, hasItem(hasProperty("id", is(103)))); + assertThat(allDepartments, hasItem(hasProperty("id", is(dept1.getId())))); + assertThat(allDepartments, hasItem(hasProperty("id", is(dept2.getId())))); + assertThat(allDepartments, hasItem(hasProperty("id", is(dept3.getId())))); } @Test @@ -124,12 +134,13 @@ public void testFindAllReturnsEmptyListWhenNoDepartments() throws SQLException { @Test public void testDepartmentEquality() throws SQLException { - // Create and persist a department - Department original = new Department(104, "Engineering"); + // Create and persist a department (ID will be auto-generated) + Department original = new Department(); + original.setName("Engineering"); departmentDAO.save(original); - // Fetch the department - Department fetched = departmentDAO.findById(104); + // Fetch the department by its auto-generated ID + Department fetched = departmentDAO.findById(original.getId()); // Validate that the objects are equal assertThat(fetched, is(equalTo(original))); From 306bcd2643661a15d8ed35a5eb7eccca99d91066 Mon Sep 17 00:00:00 2001 From: David Whitlock Date: Sun, 14 Dec 2025 12:19:18 -0800 Subject: [PATCH 10/36] Re-added the one-arg constructor to the Department class. --- .../edu/pdx/cs/joy/jdbc/DepartmentDAOIT.java | 3 +-- .../java/edu/pdx/cs/joy/jdbc/Department.java | 10 ++++++++++ .../edu/pdx/cs/joy/jdbc/CourseDAOTest.java | 10 +++------- .../pdx/cs/joy/jdbc/DepartmentDAOTest.java | 20 ++++++------------- 4 files changed, 20 insertions(+), 23 deletions(-) diff --git a/examples/src/it/java/edu/pdx/cs/joy/jdbc/DepartmentDAOIT.java b/examples/src/it/java/edu/pdx/cs/joy/jdbc/DepartmentDAOIT.java index 2559b8efc..eb7d37dea 100644 --- a/examples/src/it/java/edu/pdx/cs/joy/jdbc/DepartmentDAOIT.java +++ b/examples/src/it/java/edu/pdx/cs/joy/jdbc/DepartmentDAOIT.java @@ -70,8 +70,7 @@ private static void deleteIfExists(File file) { @Order(1) public void testPersistDepartment() throws SQLException { // Create and persist a department (ID will be auto-generated) - Department department = new Department(); - department.setName(TEST_DEPARTMENT_NAME); + Department department = new Department(TEST_DEPARTMENT_NAME); departmentDAO.save(department); // Store the auto-generated ID for use in subsequent tests diff --git a/examples/src/main/java/edu/pdx/cs/joy/jdbc/Department.java b/examples/src/main/java/edu/pdx/cs/joy/jdbc/Department.java index 757d31803..4ccdefd59 100644 --- a/examples/src/main/java/edu/pdx/cs/joy/jdbc/Department.java +++ b/examples/src/main/java/edu/pdx/cs/joy/jdbc/Department.java @@ -19,6 +19,16 @@ public Department(int id, String name) { this.name = name; } + /** + * Creates a new Department with the specified name. + * The ID will be auto-generated when the department is saved to the database. + * + * @param name the name of the department + */ + public Department(String name) { + this.name = name; + } + /** * Creates a new Department with no initial values. * Useful for frameworks that use reflection. diff --git a/examples/src/test/java/edu/pdx/cs/joy/jdbc/CourseDAOTest.java b/examples/src/test/java/edu/pdx/cs/joy/jdbc/CourseDAOTest.java index 4e2740709..63a55f2f5 100644 --- a/examples/src/test/java/edu/pdx/cs/joy/jdbc/CourseDAOTest.java +++ b/examples/src/test/java/edu/pdx/cs/joy/jdbc/CourseDAOTest.java @@ -51,8 +51,7 @@ public void tearDown() throws SQLException { @Test public void testPersistAndFetchCourse() throws SQLException { // Create and persist a department first (required for foreign key) - Department department = new Department(); - department.setName("Computer Science"); + Department department = new Department("Computer Science"); departmentDAO.save(department); // Get the auto-generated department ID @@ -86,11 +85,8 @@ public void testFetchNonExistentCourse() throws SQLException { @Test public void testPersistMultipleCourses() throws SQLException { // Create and persist departments first (required for foreign key) - Department csDepartment = new Department(); - csDepartment.setName("Computer Science"); - - Department mathDepartment = new Department(); - mathDepartment.setName("Mathematics"); + Department csDepartment = new Department("Computer Science"); + Department mathDepartment = new Department("Mathematics"); departmentDAO.save(csDepartment); departmentDAO.save(mathDepartment); diff --git a/examples/src/test/java/edu/pdx/cs/joy/jdbc/DepartmentDAOTest.java b/examples/src/test/java/edu/pdx/cs/joy/jdbc/DepartmentDAOTest.java index 53162e3c0..6dcdaf10e 100644 --- a/examples/src/test/java/edu/pdx/cs/joy/jdbc/DepartmentDAOTest.java +++ b/examples/src/test/java/edu/pdx/cs/joy/jdbc/DepartmentDAOTest.java @@ -41,8 +41,7 @@ public void tearDown() throws SQLException { @Test public void testPersistAndFetchDepartmentById() throws SQLException { // Create a department (ID will be auto-generated) - Department department = new Department(); - department.setName("Computer Science"); + Department department = new Department("Computer Science"); // Persist the department departmentDAO.save(department); @@ -62,8 +61,7 @@ public void testPersistAndFetchDepartmentById() throws SQLException { @Test public void testFindDepartmentByName() throws SQLException { // Create and persist a department (ID will be auto-generated) - Department department = new Department(); - department.setName("Mathematics"); + Department department = new Department("Mathematics"); departmentDAO.save(department); // Fetch the department by name @@ -96,14 +94,9 @@ public void testFetchNonExistentDepartmentByName() throws SQLException { @Test public void testFindAllDepartments() throws SQLException { // Create multiple departments (IDs will be auto-generated) - Department dept1 = new Department(); - dept1.setName("Computer Science"); - - Department dept2 = new Department(); - dept2.setName("Mathematics"); - - Department dept3 = new Department(); - dept3.setName("Physics"); + Department dept1 = new Department("Computer Science"); + Department dept2 = new Department("Mathematics"); + Department dept3 = new Department("Physics"); // Persist all departments departmentDAO.save(dept1); @@ -135,8 +128,7 @@ public void testFindAllReturnsEmptyListWhenNoDepartments() throws SQLException { @Test public void testDepartmentEquality() throws SQLException { // Create and persist a department (ID will be auto-generated) - Department original = new Department(); - original.setName("Engineering"); + Department original = new Department("Engineering"); departmentDAO.save(original); // Fetch the department by its auto-generated ID From 265781349ddc50e645d0e38b3af6204cf03fdcbb Mon Sep 17 00:00:00 2001 From: David Whitlock Date: Sun, 21 Dec 2025 07:44:42 -0800 Subject: [PATCH 11/36] Write a simple command line program that persists a Department object to an H2 database stored in on disk. --- .../edu/pdx/cs/joy/jdbc/DepartmentDAOIT.java | 6 +-- .../edu/pdx/cs/joy/jdbc/H2DatabaseHelper.java | 7 +-- .../pdx/cs/joy/jdbc/PersistDepartment.java | 51 +++++++++++++++++++ 3 files changed, 58 insertions(+), 6 deletions(-) create mode 100644 examples/src/main/java/edu/pdx/cs/joy/jdbc/PersistDepartment.java diff --git a/examples/src/it/java/edu/pdx/cs/joy/jdbc/DepartmentDAOIT.java b/examples/src/it/java/edu/pdx/cs/joy/jdbc/DepartmentDAOIT.java index eb7d37dea..113db8ca6 100644 --- a/examples/src/it/java/edu/pdx/cs/joy/jdbc/DepartmentDAOIT.java +++ b/examples/src/it/java/edu/pdx/cs/joy/jdbc/DepartmentDAOIT.java @@ -26,7 +26,7 @@ public static void createTable() throws SQLException { dbFilePath = tempDir + File.separator + "DepartmentDAOIT.db"; // Connect to the file-based H2 database - Connection connection = H2DatabaseHelper.createFileBasedConnection(dbFilePath); + Connection connection = H2DatabaseHelper.createFileBasedConnection(new File(dbFilePath)); // Create the departments table DepartmentDAO.createTable(connection); @@ -37,7 +37,7 @@ public static void createTable() throws SQLException { @BeforeEach public void setUp() throws SQLException { // Connect to the existing database file - connection = H2DatabaseHelper.createFileBasedConnection(dbFilePath); + connection = H2DatabaseHelper.createFileBasedConnection(new File(dbFilePath)); departmentDAO = new DepartmentDAO(connection); } @@ -51,7 +51,7 @@ public void tearDown() throws SQLException { @AfterAll public static void cleanUp() throws SQLException { // Connect one final time to drop the table and clean up - Connection connection = H2DatabaseHelper.createFileBasedConnection(dbFilePath); + Connection connection = H2DatabaseHelper.createFileBasedConnection(new File(dbFilePath)); DepartmentDAO.dropTable(connection); connection.close(); diff --git a/examples/src/main/java/edu/pdx/cs/joy/jdbc/H2DatabaseHelper.java b/examples/src/main/java/edu/pdx/cs/joy/jdbc/H2DatabaseHelper.java index 274520395..e09501877 100644 --- a/examples/src/main/java/edu/pdx/cs/joy/jdbc/H2DatabaseHelper.java +++ b/examples/src/main/java/edu/pdx/cs/joy/jdbc/H2DatabaseHelper.java @@ -1,5 +1,6 @@ package edu.pdx.cs.joy.jdbc; +import java.io.File; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; @@ -27,11 +28,11 @@ public static Connection createInMemoryConnection(String databaseName) throws SQ * Creates a connection to a file-based H2 database. * The database will be persisted to a file at the specified path. * - * @param filePath the path to the database file (without the .mv.db extension) + * @param databaseFilesDirectory the database file (without the .mv.db extension) * @return a connection to the file-based H2 database * @throws SQLException if a database error occurs */ - public static Connection createFileBasedConnection(String filePath) throws SQLException { - return DriverManager.getConnection("jdbc:h2:" + filePath); + public static Connection createFileBasedConnection(File databaseFilesDirectory) throws SQLException { + return DriverManager.getConnection("jdbc:h2:" + databaseFilesDirectory.getAbsolutePath()); } } diff --git a/examples/src/main/java/edu/pdx/cs/joy/jdbc/PersistDepartment.java b/examples/src/main/java/edu/pdx/cs/joy/jdbc/PersistDepartment.java new file mode 100644 index 000000000..05eb3cfe7 --- /dev/null +++ b/examples/src/main/java/edu/pdx/cs/joy/jdbc/PersistDepartment.java @@ -0,0 +1,51 @@ +package edu.pdx.cs.joy.jdbc; + +import java.io.File; +import java.sql.Connection; +import java.sql.SQLException; + +/** + * A command-line program that demonstrates persisting a Department object + * to an H2 database file using the DepartmentDAO class. + */ +public class PersistDepartment { + + /** + * Main method that persists a new Department to an H2 database file. + * + * @param args command line arguments where args[0] is the path to the database file + * and args[1] is the name of the department to create + * @throws SQLException if a database error occurs + */ + public static void main(String[] args) throws SQLException { + if (args.length < 2) { + System.err.println("Missing required arguments"); + System.err.println("Usage: java PersistDepartment "); + System.exit(1); + } + + String dbFilePath = args[0]; + String departmentName = args[1]; + + File dbFile = new File(dbFilePath); + + try (Connection connection = H2DatabaseHelper.createFileBasedConnection(dbFile)) { + // Create the departments table + DepartmentDAO.createTable(connection); + + // Create a new DepartmentDAO + DepartmentDAO departmentDAO = new DepartmentDAO(connection); + + // Create a new Department object with the name from the command line + Department department = new Department(departmentName); + + // Persist the department to the database + departmentDAO.save(department); + + // Print information about the persisted department + System.out.println("Successfully persisted department to database at: " + dbFile.getAbsolutePath()); + System.out.println(department); + System.out.println("Auto-generated ID: " + department.getId()); + } + } +} From 9b1b8f0bac1cf406f8d7ea78ec2e2c54588eb8d3 Mon Sep 17 00:00:00 2001 From: David Whitlock Date: Sun, 21 Dec 2025 08:55:56 -0800 Subject: [PATCH 12/36] If the name of the department to create isn't provided, then print all departments in the database. --- .../edu/pdx/cs/joy/jdbc/DepartmentDAO.java | 4 +- .../pdx/cs/joy/jdbc/ManageDepartments.java | 62 +++++++++++++++++++ .../pdx/cs/joy/jdbc/PersistDepartment.java | 51 --------------- 3 files changed, 64 insertions(+), 53 deletions(-) create mode 100644 examples/src/main/java/edu/pdx/cs/joy/jdbc/ManageDepartments.java delete mode 100644 examples/src/main/java/edu/pdx/cs/joy/jdbc/PersistDepartment.java diff --git a/examples/src/main/java/edu/pdx/cs/joy/jdbc/DepartmentDAO.java b/examples/src/main/java/edu/pdx/cs/joy/jdbc/DepartmentDAO.java index 6fa948e55..d50649323 100644 --- a/examples/src/main/java/edu/pdx/cs/joy/jdbc/DepartmentDAO.java +++ b/examples/src/main/java/edu/pdx/cs/joy/jdbc/DepartmentDAO.java @@ -34,7 +34,7 @@ public static void dropTable(Connection connection) throws SQLException { } /** - * Creates the departments table in the database. + * Creates the departments table in the database if it does not already exist. * * @param connection the database connection to use * @throws SQLException if a database error occurs @@ -42,7 +42,7 @@ public static void dropTable(Connection connection) throws SQLException { public static void createTable(Connection connection) throws SQLException { try (Statement statement = connection.createStatement()) { statement.execute( - "CREATE TABLE departments (" + + "CREATE TABLE IF NOT EXISTS departments (" + " id IDENTITY PRIMARY KEY," + " name VARCHAR(255) NOT NULL" + ")" diff --git a/examples/src/main/java/edu/pdx/cs/joy/jdbc/ManageDepartments.java b/examples/src/main/java/edu/pdx/cs/joy/jdbc/ManageDepartments.java new file mode 100644 index 000000000..557561d05 --- /dev/null +++ b/examples/src/main/java/edu/pdx/cs/joy/jdbc/ManageDepartments.java @@ -0,0 +1,62 @@ +package edu.pdx.cs.joy.jdbc; + +import java.io.File; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.List; + +/** + * A command-line program that demonstrates persisting a Department object + * to an H2 database file using the DepartmentDAO class. + */ +public class ManageDepartments { + + /** + * Main method that persists a new Department to an H2 database file. + * + * @param args command line arguments where args[0] is the path to the database file + * and args[1] is the optional name of the department to create + * @throws SQLException if a database error occurs + */ + public static void main(String[] args) throws SQLException { + if (args.length < 1) { + System.err.println("Missing database file path"); + System.err.println("Usage: java ManageDepartments [department-name"); + System.exit(1); + } + + String dbFilePath = args[0]; + String departmentName = (args.length > 1 ? args[1] : null); + + File dbFile = new File(dbFilePath); + + try (Connection connection = H2DatabaseHelper.createFileBasedConnection(dbFile)) { + // Create the departments table + DepartmentDAO.createTable(connection); + + // Create a new DepartmentDAO + DepartmentDAO departmentDAO = new DepartmentDAO(connection); + + if (departmentName == null) { + List allDepartments = departmentDAO.findAll(); + System.out.println("Found " + allDepartments.size() + " departments"); + for (Department dept : allDepartments) { + System.out.println(" " + dept); + } + + } else { + // Create a new Department object with the name from the command line + Department department = new Department(departmentName); + + // Persist the department to the database + departmentDAO.save(department); + + // Print information about the persisted department + System.out.println("Successfully persisted department to database at: " + dbFile.getAbsolutePath()); + System.out.println(department); + System.out.println("Auto-generated ID: " + department.getId()); + } + + } + } +} diff --git a/examples/src/main/java/edu/pdx/cs/joy/jdbc/PersistDepartment.java b/examples/src/main/java/edu/pdx/cs/joy/jdbc/PersistDepartment.java deleted file mode 100644 index 05eb3cfe7..000000000 --- a/examples/src/main/java/edu/pdx/cs/joy/jdbc/PersistDepartment.java +++ /dev/null @@ -1,51 +0,0 @@ -package edu.pdx.cs.joy.jdbc; - -import java.io.File; -import java.sql.Connection; -import java.sql.SQLException; - -/** - * A command-line program that demonstrates persisting a Department object - * to an H2 database file using the DepartmentDAO class. - */ -public class PersistDepartment { - - /** - * Main method that persists a new Department to an H2 database file. - * - * @param args command line arguments where args[0] is the path to the database file - * and args[1] is the name of the department to create - * @throws SQLException if a database error occurs - */ - public static void main(String[] args) throws SQLException { - if (args.length < 2) { - System.err.println("Missing required arguments"); - System.err.println("Usage: java PersistDepartment "); - System.exit(1); - } - - String dbFilePath = args[0]; - String departmentName = args[1]; - - File dbFile = new File(dbFilePath); - - try (Connection connection = H2DatabaseHelper.createFileBasedConnection(dbFile)) { - // Create the departments table - DepartmentDAO.createTable(connection); - - // Create a new DepartmentDAO - DepartmentDAO departmentDAO = new DepartmentDAO(connection); - - // Create a new Department object with the name from the command line - Department department = new Department(departmentName); - - // Persist the department to the database - departmentDAO.save(department); - - // Print information about the persisted department - System.out.println("Successfully persisted department to database at: " + dbFile.getAbsolutePath()); - System.out.println(department); - System.out.println("Auto-generated ID: " + department.getId()); - } - } -} From 86690f1f567067c35514c7b4f70d3532138100f8 Mon Sep 17 00:00:00 2001 From: David Whitlock Date: Sun, 21 Dec 2025 10:03:56 -0800 Subject: [PATCH 13/36] Make the ManageDepartments main method more fully-featured. --- .../pdx/cs/joy/jdbc/ManageDepartmentsIT.java | 191 ++++++++++++++++++ .../edu/pdx/cs/joy/jdbc/DepartmentDAO.java | 39 ++++ .../pdx/cs/joy/jdbc/ManageDepartments.java | 148 +++++++++++--- 3 files changed, 352 insertions(+), 26 deletions(-) create mode 100644 examples/src/it/java/edu/pdx/cs/joy/jdbc/ManageDepartmentsIT.java diff --git a/examples/src/it/java/edu/pdx/cs/joy/jdbc/ManageDepartmentsIT.java b/examples/src/it/java/edu/pdx/cs/joy/jdbc/ManageDepartmentsIT.java new file mode 100644 index 000000000..84f87a703 --- /dev/null +++ b/examples/src/it/java/edu/pdx/cs/joy/jdbc/ManageDepartmentsIT.java @@ -0,0 +1,191 @@ +package edu.pdx.cs.joy.jdbc; + +import edu.pdx.cs.joy.InvokeMainTestCase; +import org.junit.jupiter.api.*; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; + +/** + * Integration tests for the ManageDepartments command-line program. + * These tests verify that all CRUD operations work correctly when invoked via the main method. + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class ManageDepartmentsIT extends InvokeMainTestCase { + + private static File tempDbFile; + private static String dbFilePath; + + @BeforeAll + public static void setUp() throws IOException { + tempDbFile = Files.createTempFile("ManageDepartmentsIT", ".db").toFile(); + dbFilePath = tempDbFile.getAbsolutePath(); + // Remove the .db extension since H2 will add .mv.db + if (dbFilePath.endsWith(".db")) { + dbFilePath = dbFilePath.substring(0, dbFilePath.length() - 3); + } + } + + @AfterAll + public static void tearDown() { + // Clean up database files + deleteIfExists(new File(dbFilePath + ".mv.db")); + deleteIfExists(new File(dbFilePath + ".trace.db")); + deleteIfExists(tempDbFile); + } + + private static void deleteIfExists(File file) { + if (file.exists()) { + file.delete(); + } + } + + @Test + @Order(1) + public void testCreateDepartment() { + MainMethodResult result = invokeMain(ManageDepartments.class, dbFilePath, "create", "Computer Science"); + + String output = result.getTextWrittenToStandardOut(); + assertThat(output, containsString("Successfully created department:")); + assertThat(output, containsString("Computer Science")); + assertThat(output, containsString("Auto-generated ID:")); + } + + @Test + @Order(2) + public void testCreateSecondDepartment() { + MainMethodResult result = invokeMain(ManageDepartments.class, dbFilePath, "create", "Mathematics"); + + String output = result.getTextWrittenToStandardOut(); + assertThat(output, containsString("Successfully created department:")); + assertThat(output, containsString("Mathematics")); + assertThat(output, containsString("Auto-generated ID:")); + } + + @Test + @Order(3) + public void testRetrieveDepartmentById() { + MainMethodResult result = invokeMain(ManageDepartments.class, dbFilePath, "retrieve", "1"); + + String output = result.getTextWrittenToStandardOut(); + assertThat(output, containsString("Found department:")); + assertThat(output, containsString("id=1")); + assertThat(output, containsString("Computer Science")); + } + + @Test + @Order(4) + public void testRetrieveNonExistentDepartment() { + MainMethodResult result = invokeMain(ManageDepartments.class, dbFilePath, "retrieve", "999"); + + String output = result.getTextWrittenToStandardOut(); + assertThat(output, containsString("No department found with ID: 999")); + } + + @Test + @Order(5) + public void testListAllDepartments() { + MainMethodResult result = invokeMain(ManageDepartments.class, dbFilePath, "list"); + + String output = result.getTextWrittenToStandardOut(); + assertThat(output, containsString("Found 2 department(s)")); + assertThat(output, containsString("Computer Science")); + assertThat(output, containsString("Mathematics")); + } + + @Test + @Order(6) + public void testUpdateDepartment() { + MainMethodResult result = invokeMain(ManageDepartments.class, dbFilePath, "update", "1", "CS Department"); + + String output = result.getTextWrittenToStandardOut(); + assertThat(output, containsString("Successfully updated department:")); + assertThat(output, containsString("id=1")); + assertThat(output, containsString("CS Department")); + } + + @Test + @Order(7) + public void testRetrieveUpdatedDepartment() { + MainMethodResult result = invokeMain(ManageDepartments.class, dbFilePath, "retrieve", "1"); + + String output = result.getTextWrittenToStandardOut(); + assertThat(output, containsString("CS Department")); + assertThat(output, not(containsString("Computer Science"))); + } + + @Test + @Order(8) + public void testDeleteDepartment() { + MainMethodResult result = invokeMain(ManageDepartments.class, dbFilePath, "delete", "2"); + + String output = result.getTextWrittenToStandardOut(); + assertThat(output, containsString("Successfully deleted department:")); + assertThat(output, containsString("id=2")); + assertThat(output, containsString("Mathematics")); + } + + @Test + @Order(9) + public void testListAfterDelete() { + MainMethodResult result = invokeMain(ManageDepartments.class, dbFilePath, "list"); + + String output = result.getTextWrittenToStandardOut(); + assertThat(output, containsString("Found 1 department(s)")); + assertThat(output, containsString("CS Department")); + assertThat(output, not(containsString("Mathematics"))); + } + + @Test + public void testMissingArguments() { + MainMethodResult result = invokeMain(ManageDepartments.class, dbFilePath); + + String errorOutput = result.getTextWrittenToStandardError(); + assertThat(errorOutput, containsString("Usage: java ManageDepartments")); + } + + @Test + public void testUnknownCommand() { + MainMethodResult result = invokeMain(ManageDepartments.class, dbFilePath, "invalid"); + + String errorOutput = result.getTextWrittenToStandardError(); + assertThat(errorOutput, containsString("Unknown command: invalid")); + assertThat(errorOutput, containsString("Usage:")); + } + + @Test + public void testCreateWithoutName() { + MainMethodResult result = invokeMain(ManageDepartments.class, dbFilePath, "create"); + + String errorOutput = result.getTextWrittenToStandardError(); + assertThat(errorOutput, containsString("Missing department name for create command")); + } + + @Test + public void testRetrieveWithoutId() { + MainMethodResult result = invokeMain(ManageDepartments.class, dbFilePath, "retrieve"); + + String errorOutput = result.getTextWrittenToStandardError(); + assertThat(errorOutput, containsString("Missing department ID for retrieve command")); + } + + @Test + public void testUpdateWithMissingArguments() { + MainMethodResult result = invokeMain(ManageDepartments.class, dbFilePath, "update", "1"); + + String errorOutput = result.getTextWrittenToStandardError(); + assertThat(errorOutput, containsString("Missing arguments for update command")); + } + + @Test + public void testDeleteWithoutId() { + MainMethodResult result = invokeMain(ManageDepartments.class, dbFilePath, "delete"); + + String errorOutput = result.getTextWrittenToStandardError(); + assertThat(errorOutput, containsString("Missing department ID for delete command")); + } +} diff --git a/examples/src/main/java/edu/pdx/cs/joy/jdbc/DepartmentDAO.java b/examples/src/main/java/edu/pdx/cs/joy/jdbc/DepartmentDAO.java index d50649323..0014e77d5 100644 --- a/examples/src/main/java/edu/pdx/cs/joy/jdbc/DepartmentDAO.java +++ b/examples/src/main/java/edu/pdx/cs/joy/jdbc/DepartmentDAO.java @@ -154,5 +154,44 @@ private Department extractDepartmentFromResultSet(ResultSet resultSet) throws SQ String name = resultSet.getString("name"); return new Department(id, name); } + + /** + * Updates an existing department in the database. + * + * @param department the department to update + * @throws SQLException if a database error occurs + */ + public void update(Department department) throws SQLException { + String sql = "UPDATE departments SET name = ? WHERE id = ?"; + + try (PreparedStatement statement = connection.prepareStatement(sql)) { + statement.setString(1, department.getName()); + statement.setInt(2, department.getId()); + int rowsAffected = statement.executeUpdate(); + + if (rowsAffected == 0) { + throw new SQLException("Update failed, no department found with ID: " + department.getId()); + } + } + } + + /** + * Deletes a department from the database by ID. + * + * @param id the ID of the department to delete + * @throws SQLException if a database error occurs + */ + public void delete(int id) throws SQLException { + String sql = "DELETE FROM departments WHERE id = ?"; + + try (PreparedStatement statement = connection.prepareStatement(sql)) { + statement.setInt(1, id); + int rowsAffected = statement.executeUpdate(); + + if (rowsAffected == 0) { + throw new SQLException("Delete failed, no department found with ID: " + id); + } + } + } } diff --git a/examples/src/main/java/edu/pdx/cs/joy/jdbc/ManageDepartments.java b/examples/src/main/java/edu/pdx/cs/joy/jdbc/ManageDepartments.java index 557561d05..7579deb86 100644 --- a/examples/src/main/java/edu/pdx/cs/joy/jdbc/ManageDepartments.java +++ b/examples/src/main/java/edu/pdx/cs/joy/jdbc/ManageDepartments.java @@ -6,57 +6,153 @@ import java.util.List; /** - * A command-line program that demonstrates persisting a Department object - * to an H2 database file using the DepartmentDAO class. + * A command-line program that demonstrates CRUD operations on Department objects + * using the DepartmentDAO class with an H2 database. */ public class ManageDepartments { /** - * Main method that persists a new Department to an H2 database file. + * Main method that performs CRUD operations on departments in an H2 database file. * * @param args command line arguments where args[0] is the path to the database file - * and args[1] is the optional name of the department to create + * and args[1] is the command (create, retrieve, update, delete, or list) * @throws SQLException if a database error occurs */ public static void main(String[] args) throws SQLException { - if (args.length < 1) { - System.err.println("Missing database file path"); - System.err.println("Usage: java ManageDepartments [department-name"); - System.exit(1); + if (args.length < 2) { + printUsage(); + return; } String dbFilePath = args[0]; - String departmentName = (args.length > 1 ? args[1] : null); + String command = args[1].toLowerCase(); File dbFile = new File(dbFilePath); try (Connection connection = H2DatabaseHelper.createFileBasedConnection(dbFile)) { - // Create the departments table + // Create the departments table if it doesn't exist DepartmentDAO.createTable(connection); // Create a new DepartmentDAO DepartmentDAO departmentDAO = new DepartmentDAO(connection); - if (departmentName == null) { - List allDepartments = departmentDAO.findAll(); - System.out.println("Found " + allDepartments.size() + " departments"); - for (Department dept : allDepartments) { - System.out.println(" " + dept); - } + switch (command) { + case "create": + handleCreate(args, departmentDAO); + break; + case "retrieve": + handleRetrieve(args, departmentDAO); + break; + case "update": + handleUpdate(args, departmentDAO); + break; + case "delete": + handleDelete(args, departmentDAO); + break; + case "list": + handleList(departmentDAO); + break; + default: + System.err.println("Unknown command: " + command); + printUsage(); + return; + } + } + } + + private static void printUsage() { + System.err.println("Usage: java ManageDepartments [args...]"); + System.err.println(); + System.err.println("Commands:"); + System.err.println(" create - Create a new department with the given name"); + System.err.println(" retrieve - Retrieve a department by ID"); + System.err.println(" update - Update the name of a department"); + System.err.println(" delete - Delete a department by ID"); + System.err.println(" list - List all departments"); + } - } else { - // Create a new Department object with the name from the command line - Department department = new Department(departmentName); + private static void handleCreate(String[] args, DepartmentDAO departmentDAO) throws SQLException { + if (args.length < 3) { + System.err.println("Missing department name for create command"); + System.err.println("Usage: java ManageDepartments create "); + return; + } - // Persist the department to the database - departmentDAO.save(department); + String departmentName = args[2]; + Department department = new Department(departmentName); + departmentDAO.save(department); - // Print information about the persisted department - System.out.println("Successfully persisted department to database at: " + dbFile.getAbsolutePath()); - System.out.println(department); - System.out.println("Auto-generated ID: " + department.getId()); - } + System.out.println("Successfully created department:"); + System.out.println(department); + System.out.println("Auto-generated ID: " + department.getId()); + } + + private static void handleRetrieve(String[] args, DepartmentDAO departmentDAO) throws SQLException { + if (args.length < 3) { + System.err.println("Missing department ID for retrieve command"); + System.err.println("Usage: java ManageDepartments retrieve "); + return; + } + + int id = Integer.parseInt(args[2]); + Department department = departmentDAO.findById(id); + + if (department == null) { + System.out.println("No department found with ID: " + id); + } else { + System.out.println("Found department:"); + System.out.println(department); + } + } + + private static void handleUpdate(String[] args, DepartmentDAO departmentDAO) throws SQLException { + if (args.length < 4) { + System.err.println("Missing arguments for update command"); + System.err.println("Usage: java ManageDepartments update "); + return; + } + + int id = Integer.parseInt(args[2]); + String newName = args[3]; + + Department department = departmentDAO.findById(id); + if (department == null) { + System.out.println("No department found with ID: " + id); + return; + } + + department.setName(newName); + departmentDAO.update(department); + + System.out.println("Successfully updated department:"); + System.out.println(department); + } + + private static void handleDelete(String[] args, DepartmentDAO departmentDAO) throws SQLException { + if (args.length < 3) { + System.err.println("Missing department ID for delete command"); + System.err.println("Usage: java ManageDepartments delete "); + return; + } + + int id = Integer.parseInt(args[2]); + Department department = departmentDAO.findById(id); + + if (department == null) { + System.out.println("No department found with ID: " + id); + return; + } + + departmentDAO.delete(id); + System.out.println("Successfully deleted department:"); + System.out.println(department); + } + private static void handleList(DepartmentDAO departmentDAO) throws SQLException { + List allDepartments = departmentDAO.findAll(); + System.out.println("Found " + allDepartments.size() + " department(s)"); + for (Department dept : allDepartments) { + System.out.println(" " + dept); } } } From 98325b59361b501e00cd299ded9c7a5baf3bf7b3 Mon Sep 17 00:00:00 2001 From: David Whitlock Date: Sun, 21 Dec 2025 10:15:54 -0800 Subject: [PATCH 14/36] Run the integration tests as part of the Maven build. --- examples/pom.xml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/examples/pom.xml b/examples/pom.xml index dfe7ec84d..eb8dbffce 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -55,4 +55,18 @@ test + + + + org.codehaus.mojo + build-helper-maven-plugin + ${build-helper-maven-plugin.version} + + + org.apache.maven.plugins + maven-failsafe-plugin + ${surefire.version} + + + From 82b6ae426f7555a55d1f6dd1c0411622351ffc22 Mon Sep 17 00:00:00 2001 From: David Whitlock Date: Wed, 24 Dec 2025 07:27:47 -0800 Subject: [PATCH 15/36] Added a "credit" property to the Course class. --- .../main/java/edu/pdx/cs/joy/jdbc/Course.java | 30 ++++++++++++-- .../java/edu/pdx/cs/joy/jdbc/CourseDAO.java | 11 ++++-- .../edu/pdx/cs/joy/jdbc/CourseDAOTest.java | 39 ++++++++++++++++--- 3 files changed, 68 insertions(+), 12 deletions(-) diff --git a/examples/src/main/java/edu/pdx/cs/joy/jdbc/Course.java b/examples/src/main/java/edu/pdx/cs/joy/jdbc/Course.java index 2fcecf5ff..1dd682440 100644 --- a/examples/src/main/java/edu/pdx/cs/joy/jdbc/Course.java +++ b/examples/src/main/java/edu/pdx/cs/joy/jdbc/Course.java @@ -2,21 +2,24 @@ /** * Represents a course in a college course catalog. - * Each course has a title and is associated with a department. + * Each course has a title, is associated with a department, and has a number of credits. */ public class Course { private String title; private int departmentId; + private int credits; /** - * Creates a new Course with the specified title and department ID. + * Creates a new Course with the specified title, department ID, and credits. * * @param title the title of the course * @param departmentId the numeric ID of the department offering this course + * @param credits the number of credits for this course */ - public Course(String title, int departmentId) { + public Course(String title, int departmentId, int credits) { this.title = title; this.departmentId = departmentId; + this.credits = credits; } /** @@ -62,11 +65,30 @@ public void setDepartmentId(int departmentId) { this.departmentId = departmentId; } + /** + * Returns the number of credits for this course. + * + * @return the number of credits + */ + public int getCredits() { + return credits; + } + + /** + * Sets the number of credits for this course. + * + * @param credits the number of credits + */ + public void setCredits(int credits) { + this.credits = credits; + } + @Override public String toString() { return "Course{" + "title='" + title + '\'' + ", departmentId=" + departmentId + + ", credits=" + credits + '}'; } @@ -78,6 +100,7 @@ public boolean equals(Object o) { Course course = (Course) o; if (departmentId != course.departmentId) return false; + if (credits != course.credits) return false; return title != null ? title.equals(course.title) : course.title == null; } @@ -85,6 +108,7 @@ public boolean equals(Object o) { public int hashCode() { int result = title != null ? title.hashCode() : 0; result = 31 * result + departmentId; + result = 31 * result + credits; return result; } } diff --git a/examples/src/main/java/edu/pdx/cs/joy/jdbc/CourseDAO.java b/examples/src/main/java/edu/pdx/cs/joy/jdbc/CourseDAO.java index 3101a1cca..d5adddaf2 100644 --- a/examples/src/main/java/edu/pdx/cs/joy/jdbc/CourseDAO.java +++ b/examples/src/main/java/edu/pdx/cs/joy/jdbc/CourseDAO.java @@ -46,6 +46,7 @@ public static void createTable(Connection connection) throws SQLException { " id IDENTITY PRIMARY KEY," + " title VARCHAR(255) NOT NULL," + " department_id INTEGER NOT NULL," + + " credits INTEGER NOT NULL," + " FOREIGN KEY (department_id) REFERENCES departments(id)" + ")" ); @@ -59,11 +60,12 @@ public static void createTable(Connection connection) throws SQLException { * @throws SQLException if a database error occurs */ public void save(Course course) throws SQLException { - String sql = "INSERT INTO courses (title, department_id) VALUES (?, ?)"; + String sql = "INSERT INTO courses (title, department_id, credits) VALUES (?, ?, ?)"; try (PreparedStatement statement = connection.prepareStatement(sql)) { statement.setString(1, course.getTitle()); statement.setInt(2, course.getDepartmentId()); + statement.setInt(3, course.getCredits()); statement.executeUpdate(); } } @@ -76,7 +78,7 @@ public void save(Course course) throws SQLException { * @throws SQLException if a database error occurs */ public Course findByTitle(String title) throws SQLException { - String sql = "SELECT title, department_id FROM courses WHERE title = ?"; + String sql = "SELECT title, department_id, credits FROM courses WHERE title = ?"; try (PreparedStatement statement = connection.prepareStatement(sql)) { statement.setString(1, title); @@ -100,7 +102,7 @@ public Course findByTitle(String title) throws SQLException { */ public List findByDepartmentId(int departmentId) throws SQLException { List courses = new ArrayList<>(); - String sql = "SELECT title, department_id FROM courses WHERE department_id = ?"; + String sql = "SELECT title, department_id, credits FROM courses WHERE department_id = ?"; try (PreparedStatement statement = connection.prepareStatement(sql)) { statement.setInt(1, departmentId); @@ -125,7 +127,8 @@ public List findByDepartmentId(int departmentId) throws SQLException { private Course extractCourseFromResultSet(ResultSet resultSet) throws SQLException { String title = resultSet.getString("title"); int departmentId = resultSet.getInt("department_id"); - return new Course(title, departmentId); + int credits = resultSet.getInt("credits"); + return new Course(title, departmentId, credits); } } diff --git a/examples/src/test/java/edu/pdx/cs/joy/jdbc/CourseDAOTest.java b/examples/src/test/java/edu/pdx/cs/joy/jdbc/CourseDAOTest.java index 63a55f2f5..91f2958a3 100644 --- a/examples/src/test/java/edu/pdx/cs/joy/jdbc/CourseDAOTest.java +++ b/examples/src/test/java/edu/pdx/cs/joy/jdbc/CourseDAOTest.java @@ -59,7 +59,8 @@ public void testPersistAndFetchCourse() throws SQLException { // Create a course String javaCourseName = "Introduction to Java"; - Course course = new Course(javaCourseName, csDepartmentId); + int credits = 4; + Course course = new Course(javaCourseName, csDepartmentId, credits); // Persist the course courseDAO.save(course); @@ -71,6 +72,7 @@ public void testPersistAndFetchCourse() throws SQLException { assertThat(fetchedCourse, is(notNullValue())); assertThat(fetchedCourse.getTitle(), is(equalTo(javaCourseName))); assertThat(fetchedCourse.getDepartmentId(), is(equalTo(csDepartmentId))); + assertThat(fetchedCourse.getCredits(), is(equalTo(credits))); } @Test @@ -100,9 +102,9 @@ public void testPersistMultipleCourses() throws SQLException { String algorithmsName = "Algorithms"; String calculusName = "Calculus"; - Course course1 = new Course(dataStructuresName, csDepartmentId); - Course course2 = new Course(algorithmsName, csDepartmentId); - Course course3 = new Course(calculusName, mathDepartmentId); + Course course1 = new Course(dataStructuresName, csDepartmentId, 4); + Course course2 = new Course(algorithmsName, csDepartmentId, 3); + Course course3 = new Course(calculusName, mathDepartmentId, 4); // Persist all courses courseDAO.save(course1); @@ -125,7 +127,7 @@ public void testPersistMultipleCourses() throws SQLException { @Test public void testForeignKeyConstraintPreventsInvalidDepartmentId() { // Try to create a course with a non-existent department ID - Course course = new Course("Database Systems", 999); + Course course = new Course("Database Systems", 999, 3); // Attempting to save should throw an SQLException due to foreign key constraint SQLException exception = assertThrows(SQLException.class, () -> { @@ -134,4 +136,31 @@ public void testForeignKeyConstraintPreventsInvalidDepartmentId() { assertThat(exception.getMessage(), containsString("Referential integrity")); } + @Test + public void testCreditsArePersisted() throws SQLException { + // Create and persist a department first (required for foreign key) + Department department = new Department("Mathematics"); + departmentDAO.save(department); + int deptId = department.getId(); + + // Create courses with different credit values + Course threeCredits = new Course("Statistics", deptId, 3); + Course fourCredits = new Course("Linear Algebra", deptId, 4); + Course fiveCredits = new Course("Abstract Algebra", deptId, 5); + + // Persist all courses + courseDAO.save(threeCredits); + courseDAO.save(fourCredits); + courseDAO.save(fiveCredits); + + // Fetch the courses and verify credits + Course fetchedThree = courseDAO.findByTitle("Statistics"); + Course fetchedFour = courseDAO.findByTitle("Linear Algebra"); + Course fetchedFive = courseDAO.findByTitle("Abstract Algebra"); + + assertThat(fetchedThree.getCredits(), is(equalTo(3))); + assertThat(fetchedFour.getCredits(), is(equalTo(4))); + assertThat(fetchedFive.getCredits(), is(equalTo(5))); + } + } From 716b0766c1d43cc8af95b4dae7f438ace0d31f92 Mon Sep 17 00:00:00 2001 From: David Whitlock Date: Wed, 24 Dec 2025 07:38:30 -0800 Subject: [PATCH 16/36] Added an "id" property to the Course object that is populated from the id generated in the database. --- .../main/java/edu/pdx/cs/joy/jdbc/Course.java | 28 +++++++++-- .../java/edu/pdx/cs/joy/jdbc/CourseDAO.java | 46 +++++++++++++++++-- .../edu/pdx/cs/joy/jdbc/CourseDAOTest.java | 36 +++++++++++++++ 3 files changed, 103 insertions(+), 7 deletions(-) diff --git a/examples/src/main/java/edu/pdx/cs/joy/jdbc/Course.java b/examples/src/main/java/edu/pdx/cs/joy/jdbc/Course.java index 1dd682440..fc673450c 100644 --- a/examples/src/main/java/edu/pdx/cs/joy/jdbc/Course.java +++ b/examples/src/main/java/edu/pdx/cs/joy/jdbc/Course.java @@ -2,9 +2,10 @@ /** * Represents a course in a college course catalog. - * Each course has a title, is associated with a department, and has a number of credits. + * Each course has a unique ID, a title, is associated with a department, and has a number of credits. */ public class Course { + private int id; private String title; private int departmentId; private int credits; @@ -29,6 +30,24 @@ public Course(String title, int departmentId, int credits) { public Course() { } + /** + * Returns the unique ID of this course. + * + * @return the course ID + */ + public int getId() { + return id; + } + + /** + * Sets the unique ID of this course. + * + * @param id the course ID + */ + public void setId(int id) { + this.id = id; + } + /** * Returns the title of this course. * @@ -86,7 +105,8 @@ public void setCredits(int credits) { @Override public String toString() { return "Course{" + - "title='" + title + '\'' + + "id=" + id + + ", title='" + title + '\'' + ", departmentId=" + departmentId + ", credits=" + credits + '}'; @@ -99,6 +119,7 @@ public boolean equals(Object o) { Course course = (Course) o; + if (id != course.id) return false; if (departmentId != course.departmentId) return false; if (credits != course.credits) return false; return title != null ? title.equals(course.title) : course.title == null; @@ -106,7 +127,8 @@ public boolean equals(Object o) { @Override public int hashCode() { - int result = title != null ? title.hashCode() : 0; + int result = id; + result = 31 * result + (title != null ? title.hashCode() : 0); result = 31 * result + departmentId; result = 31 * result + credits; return result; diff --git a/examples/src/main/java/edu/pdx/cs/joy/jdbc/CourseDAO.java b/examples/src/main/java/edu/pdx/cs/joy/jdbc/CourseDAO.java index d5adddaf2..04d4ba61b 100644 --- a/examples/src/main/java/edu/pdx/cs/joy/jdbc/CourseDAO.java +++ b/examples/src/main/java/edu/pdx/cs/joy/jdbc/CourseDAO.java @@ -55,6 +55,7 @@ public static void createTable(Connection connection) throws SQLException { /** * Saves a course to the database. + * The course's ID will be automatically generated by the database and set on the object. * * @param course the course to save * @throws SQLException if a database error occurs @@ -62,11 +63,21 @@ public static void createTable(Connection connection) throws SQLException { public void save(Course course) throws SQLException { String sql = "INSERT INTO courses (title, department_id, credits) VALUES (?, ?, ?)"; - try (PreparedStatement statement = connection.prepareStatement(sql)) { + try (PreparedStatement statement = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { statement.setString(1, course.getTitle()); statement.setInt(2, course.getDepartmentId()); statement.setInt(3, course.getCredits()); statement.executeUpdate(); + + // Retrieve the auto-generated ID and set it on the course object + try (ResultSet generatedKeys = statement.getGeneratedKeys()) { + if (generatedKeys.next()) { + int generatedId = generatedKeys.getInt(1); + course.setId(generatedId); + } else { + throw new SQLException("Creating course failed, no ID obtained."); + } + } } } @@ -78,7 +89,7 @@ public void save(Course course) throws SQLException { * @throws SQLException if a database error occurs */ public Course findByTitle(String title) throws SQLException { - String sql = "SELECT title, department_id, credits FROM courses WHERE title = ?"; + String sql = "SELECT id, title, department_id, credits FROM courses WHERE title = ?"; try (PreparedStatement statement = connection.prepareStatement(sql)) { statement.setString(1, title); @@ -102,7 +113,7 @@ public Course findByTitle(String title) throws SQLException { */ public List findByDepartmentId(int departmentId) throws SQLException { List courses = new ArrayList<>(); - String sql = "SELECT title, department_id, credits FROM courses WHERE department_id = ?"; + String sql = "SELECT id, title, department_id, credits FROM courses WHERE department_id = ?"; try (PreparedStatement statement = connection.prepareStatement(sql)) { statement.setInt(1, departmentId); @@ -125,10 +136,37 @@ public List findByDepartmentId(int departmentId) throws SQLException { * @throws SQLException if a database error occurs */ private Course extractCourseFromResultSet(ResultSet resultSet) throws SQLException { + int id = resultSet.getInt("id"); String title = resultSet.getString("title"); int departmentId = resultSet.getInt("department_id"); int credits = resultSet.getInt("credits"); - return new Course(title, departmentId, credits); + + Course course = new Course(title, departmentId, credits); + course.setId(id); + return course; + } + + /** + * Updates an existing course in the database. + * Uses the course's ID to identify which record to update. + * + * @param course the course to update + * @throws SQLException if a database error occurs + */ + public void update(Course course) throws SQLException { + String sql = "UPDATE courses SET title = ?, department_id = ?, credits = ? WHERE id = ?"; + + try (PreparedStatement statement = connection.prepareStatement(sql)) { + statement.setString(1, course.getTitle()); + statement.setInt(2, course.getDepartmentId()); + statement.setInt(3, course.getCredits()); + statement.setInt(4, course.getId()); + + int rowsAffected = statement.executeUpdate(); + if (rowsAffected == 0) { + throw new SQLException("Update failed, no course found with ID: " + course.getId()); + } + } } } diff --git a/examples/src/test/java/edu/pdx/cs/joy/jdbc/CourseDAOTest.java b/examples/src/test/java/edu/pdx/cs/joy/jdbc/CourseDAOTest.java index 91f2958a3..4cdddeca3 100644 --- a/examples/src/test/java/edu/pdx/cs/joy/jdbc/CourseDAOTest.java +++ b/examples/src/test/java/edu/pdx/cs/joy/jdbc/CourseDAOTest.java @@ -65,11 +65,16 @@ public void testPersistAndFetchCourse() throws SQLException { // Persist the course courseDAO.save(course); + // Verify that an ID was auto-generated + int generatedId = course.getId(); + assertThat(generatedId, is(greaterThan(0))); + // Fetch the course by title Course fetchedCourse = courseDAO.findByTitle(javaCourseName); // Validate the fetched course using Hamcrest assertions assertThat(fetchedCourse, is(notNullValue())); + assertThat(fetchedCourse.getId(), is(equalTo(generatedId))); assertThat(fetchedCourse.getTitle(), is(equalTo(javaCourseName))); assertThat(fetchedCourse.getDepartmentId(), is(equalTo(csDepartmentId))); assertThat(fetchedCourse.getCredits(), is(equalTo(credits))); @@ -163,4 +168,35 @@ public void testCreditsArePersisted() throws SQLException { assertThat(fetchedFive.getCredits(), is(equalTo(5))); } + @Test + public void testUpdateCourse() throws SQLException { + // Create and persist a department first (required for foreign key) + Department department = new Department("Computer Science"); + departmentDAO.save(department); + int deptId = department.getId(); + + // Create and persist a course + Course course = new Course("Database Systems", deptId, 3); + courseDAO.save(course); + + int courseId = course.getId(); + assertThat(courseId, is(greaterThan(0))); + + // Update the course + course.setTitle("Advanced Database Systems"); + course.setCredits(4); + courseDAO.update(course); + + // Fetch the course and verify it was updated + Course updatedCourse = courseDAO.findByTitle("Advanced Database Systems"); + assertThat(updatedCourse, is(notNullValue())); + assertThat(updatedCourse.getId(), is(equalTo(courseId))); + assertThat(updatedCourse.getTitle(), is(equalTo("Advanced Database Systems"))); + assertThat(updatedCourse.getCredits(), is(equalTo(4))); + + // Verify the old title doesn't exist anymore + Course oldCourse = courseDAO.findByTitle("Database Systems"); + assertThat(oldCourse, is(nullValue())); + } + } From 283b37d50c6352408800816f63b00cb0e39fd6ca Mon Sep 17 00:00:00 2001 From: David Whitlock Date: Wed, 24 Dec 2025 13:10:30 -0800 Subject: [PATCH 17/36] Added an AcademicTerm to show off how dates are persisted to the database. --- .../edu/pdx/cs/joy/jdbc/AcademicTerm.java | 154 +++++++++++++ .../edu/pdx/cs/joy/jdbc/AcademicTermDAO.java | 209 ++++++++++++++++++ .../pdx/cs/joy/jdbc/AcademicTermDAOTest.java | 187 ++++++++++++++++ 3 files changed, 550 insertions(+) create mode 100644 examples/src/main/java/edu/pdx/cs/joy/jdbc/AcademicTerm.java create mode 100644 examples/src/main/java/edu/pdx/cs/joy/jdbc/AcademicTermDAO.java create mode 100644 examples/src/test/java/edu/pdx/cs/joy/jdbc/AcademicTermDAOTest.java diff --git a/examples/src/main/java/edu/pdx/cs/joy/jdbc/AcademicTerm.java b/examples/src/main/java/edu/pdx/cs/joy/jdbc/AcademicTerm.java new file mode 100644 index 000000000..de65125d4 --- /dev/null +++ b/examples/src/main/java/edu/pdx/cs/joy/jdbc/AcademicTerm.java @@ -0,0 +1,154 @@ +package edu.pdx.cs.joy.jdbc; + +import java.time.LocalDate; + +/** + * Represents an academic term (such as Fall 2024 or Spring 2025) during which courses are offered. + * Each term has a unique ID, a name, and start and end dates. + */ +public class AcademicTerm { + private int id; + private String name; + private LocalDate startDate; + private LocalDate endDate; + + /** + * Creates a new AcademicTerm with the specified name, start date, and end date. + * + * @param name the name of the term (e.g., "Fall 2024") + * @param startDate the start date of the term + * @param endDate the end date of the term + */ + public AcademicTerm(String name, LocalDate startDate, LocalDate endDate) { + this.name = name; + this.startDate = startDate; + this.endDate = endDate; + } + + /** + * Creates a new AcademicTerm with the specified ID, name, start date, and end date. + * + * @param id the unique identifier for the term + * @param name the name of the term (e.g., "Fall 2024") + * @param startDate the start date of the term + * @param endDate the end date of the term + */ + public AcademicTerm(int id, String name, LocalDate startDate, LocalDate endDate) { + this.id = id; + this.name = name; + this.startDate = startDate; + this.endDate = endDate; + } + + /** + * Creates a new AcademicTerm with no initial values. + * Useful for frameworks that use reflection. + */ + public AcademicTerm() { + } + + /** + * Returns the unique ID of this academic term. + * + * @return the term ID + */ + public int getId() { + return id; + } + + /** + * Sets the unique ID of this academic term. + * + * @param id the term ID + */ + public void setId(int id) { + this.id = id; + } + + /** + * Returns the name of this academic term. + * + * @return the term name + */ + public String getName() { + return name; + } + + /** + * Sets the name of this academic term. + * + * @param name the term name + */ + public void setName(String name) { + this.name = name; + } + + /** + * Returns the start date of this academic term. + * + * @return the start date + */ + public LocalDate getStartDate() { + return startDate; + } + + /** + * Sets the start date of this academic term. + * + * @param startDate the start date + */ + public void setStartDate(LocalDate startDate) { + this.startDate = startDate; + } + + /** + * Returns the end date of this academic term. + * + * @return the end date + */ + public LocalDate getEndDate() { + return endDate; + } + + /** + * Sets the end date of this academic term. + * + * @param endDate the end date + */ + public void setEndDate(LocalDate endDate) { + this.endDate = endDate; + } + + @Override + public String toString() { + return "AcademicTerm{" + + "id=" + id + + ", name='" + name + '\'' + + ", startDate=" + startDate + + ", endDate=" + endDate + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + AcademicTerm that = (AcademicTerm) o; + + if (id != that.id) return false; + if (name != null ? !name.equals(that.name) : that.name != null) return false; + if (startDate != null ? !startDate.equals(that.startDate) : that.startDate != null) return false; + return endDate != null ? endDate.equals(that.endDate) : that.endDate == null; + } + + @Override + public int hashCode() { + int result = id; + result = 31 * result + (name != null ? name.hashCode() : 0); + result = 31 * result + (startDate != null ? startDate.hashCode() : 0); + result = 31 * result + (endDate != null ? endDate.hashCode() : 0); + return result; + } +} + diff --git a/examples/src/main/java/edu/pdx/cs/joy/jdbc/AcademicTermDAO.java b/examples/src/main/java/edu/pdx/cs/joy/jdbc/AcademicTermDAO.java new file mode 100644 index 000000000..7b93aacfc --- /dev/null +++ b/examples/src/main/java/edu/pdx/cs/joy/jdbc/AcademicTermDAO.java @@ -0,0 +1,209 @@ +package edu.pdx.cs.joy.jdbc; + +import java.sql.*; +import java.util.ArrayList; +import java.util.List; + +/** + * Data Access Object for managing AcademicTerm entities in the database. + * Demonstrates JDBC operations with date fields: CREATE, READ, UPDATE, DELETE. + */ +public class AcademicTermDAO { + + private final Connection connection; + + /** + * Creates a new AcademicTermDAO with the specified database connection. + * + * @param connection the database connection to use + */ + public AcademicTermDAO(Connection connection) { + this.connection = connection; + } + + /** + * Drops the academic_terms table from the database if it exists. + * + * @param connection the database connection to use + * @throws SQLException if a database error occurs + */ + public static void dropTable(Connection connection) throws SQLException { + try (Statement statement = connection.createStatement()) { + statement.execute("DROP TABLE IF EXISTS academic_terms"); + } + } + + /** + * Creates the academic_terms table in the database if it does not already exist. + * + * @param connection the database connection to use + * @throws SQLException if a database error occurs + */ + public static void createTable(Connection connection) throws SQLException { + try (Statement statement = connection.createStatement()) { + statement.execute( + "CREATE TABLE IF NOT EXISTS academic_terms (" + + " id IDENTITY PRIMARY KEY," + + " name VARCHAR(255) NOT NULL," + + " start_date DATE NOT NULL," + + " end_date DATE NOT NULL" + + ")" + ); + } + } + + /** + * Saves an academic term to the database. + * The term's ID will be automatically generated by the database and set on the object. + * + * @param term the academic term to save + * @throws SQLException if a database error occurs + */ + public void save(AcademicTerm term) throws SQLException { + String sql = "INSERT INTO academic_terms (name, start_date, end_date) VALUES (?, ?, ?)"; + + try (PreparedStatement statement = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { + statement.setString(1, term.getName()); + statement.setDate(2, Date.valueOf(term.getStartDate())); + statement.setDate(3, Date.valueOf(term.getEndDate())); + statement.executeUpdate(); + + // Retrieve the auto-generated ID and set it on the term object + try (ResultSet generatedKeys = statement.getGeneratedKeys()) { + if (generatedKeys.next()) { + int generatedId = generatedKeys.getInt(1); + term.setId(generatedId); + } else { + throw new SQLException("Creating academic term failed, no ID obtained."); + } + } + } + } + + /** + * Finds an academic term by its ID. + * + * @param id the ID to search for + * @return the academic term with the given ID, or null if not found + * @throws SQLException if a database error occurs + */ + public AcademicTerm findById(int id) throws SQLException { + String sql = "SELECT id, name, start_date, end_date FROM academic_terms WHERE id = ?"; + + try (PreparedStatement statement = connection.prepareStatement(sql)) { + statement.setInt(1, id); + + try (ResultSet resultSet = statement.executeQuery()) { + if (resultSet.next()) { + return extractAcademicTermFromResultSet(resultSet); + } + } + } + + return null; + } + + /** + * Finds an academic term by its name. + * + * @param name the name to search for + * @return the academic term with the given name, or null if not found + * @throws SQLException if a database error occurs + */ + public AcademicTerm findByName(String name) throws SQLException { + String sql = "SELECT id, name, start_date, end_date FROM academic_terms WHERE name = ?"; + + try (PreparedStatement statement = connection.prepareStatement(sql)) { + statement.setString(1, name); + + try (ResultSet resultSet = statement.executeQuery()) { + if (resultSet.next()) { + return extractAcademicTermFromResultSet(resultSet); + } + } + } + + return null; + } + + /** + * Finds all academic terms in the database. + * + * @return a list of all academic terms + * @throws SQLException if a database error occurs + */ + public List findAll() throws SQLException { + List terms = new ArrayList<>(); + String sql = "SELECT id, name, start_date, end_date FROM academic_terms ORDER BY start_date"; + + try (Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery(sql)) { + while (resultSet.next()) { + terms.add(extractAcademicTermFromResultSet(resultSet)); + } + } + + return terms; + } + + /** + * Updates an existing academic term in the database. + * Uses the term's ID to identify which record to update. + * + * @param term the academic term to update + * @throws SQLException if a database error occurs + */ + public void update(AcademicTerm term) throws SQLException { + String sql = "UPDATE academic_terms SET name = ?, start_date = ?, end_date = ? WHERE id = ?"; + + try (PreparedStatement statement = connection.prepareStatement(sql)) { + statement.setString(1, term.getName()); + statement.setDate(2, Date.valueOf(term.getStartDate())); + statement.setDate(3, Date.valueOf(term.getEndDate())); + statement.setInt(4, term.getId()); + + int rowsAffected = statement.executeUpdate(); + if (rowsAffected == 0) { + throw new SQLException("Update failed, no academic term found with ID: " + term.getId()); + } + } + } + + /** + * Deletes an academic term from the database by ID. + * + * @param id the ID of the academic term to delete + * @throws SQLException if a database error occurs + */ + public void delete(int id) throws SQLException { + String sql = "DELETE FROM academic_terms WHERE id = ?"; + + try (PreparedStatement statement = connection.prepareStatement(sql)) { + statement.setInt(1, id); + int rowsAffected = statement.executeUpdate(); + + if (rowsAffected == 0) { + throw new SQLException("Delete failed, no academic term found with ID: " + id); + } + } + } + + /** + * Extracts an AcademicTerm object from the current row of a ResultSet. + * + * @param resultSet the result set positioned at an academic term row + * @return an AcademicTerm object with data from the result set + * @throws SQLException if a database error occurs + */ + private AcademicTerm extractAcademicTermFromResultSet(ResultSet resultSet) throws SQLException { + int id = resultSet.getInt("id"); + String name = resultSet.getString("name"); + Date startDate = resultSet.getDate("start_date"); + Date endDate = resultSet.getDate("end_date"); + + AcademicTerm term = new AcademicTerm(name, startDate.toLocalDate(), endDate.toLocalDate()); + term.setId(id); + return term; + } +} + diff --git a/examples/src/test/java/edu/pdx/cs/joy/jdbc/AcademicTermDAOTest.java b/examples/src/test/java/edu/pdx/cs/joy/jdbc/AcademicTermDAOTest.java new file mode 100644 index 000000000..64f14fb54 --- /dev/null +++ b/examples/src/test/java/edu/pdx/cs/joy/jdbc/AcademicTermDAOTest.java @@ -0,0 +1,187 @@ +package edu.pdx.cs.joy.jdbc; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.sql.Connection; +import java.sql.SQLException; +import java.time.LocalDate; +import java.util.List; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; + +public class AcademicTermDAOTest { + + private Connection connection; + private AcademicTermDAO termDAO; + + @BeforeEach + public void setUp() throws SQLException { + // Create an in-memory H2 database + connection = H2DatabaseHelper.createInMemoryConnection("test"); + + // Drop and create the academic_terms table + AcademicTermDAO.dropTable(connection); + AcademicTermDAO.createTable(connection); + + // Initialize the DAO with the connection + termDAO = new AcademicTermDAO(connection); + } + + @AfterEach + public void tearDown() throws SQLException { + if (connection != null && !connection.isClosed()) { + AcademicTermDAO.dropTable(connection); + connection.close(); + } + } + + @Test + public void testPersistAndFetchAcademicTerm() throws SQLException { + // Create an academic term + String termName = "Fall 2024"; + LocalDate startDate = LocalDate.of(2024, 9, 1); + LocalDate endDate = LocalDate.of(2024, 12, 15); + AcademicTerm term = new AcademicTerm(termName, startDate, endDate); + + // Persist the term + termDAO.save(term); + + // Verify that an ID was auto-generated + int generatedId = term.getId(); + assertThat(generatedId, is(greaterThan(0))); + + // Fetch the term by ID + AcademicTerm fetchedTerm = termDAO.findById(generatedId); + + // Validate the fetched term using Hamcrest assertions + assertThat(fetchedTerm, is(notNullValue())); + assertThat(fetchedTerm.getId(), is(equalTo(generatedId))); + assertThat(fetchedTerm.getName(), is(equalTo(termName))); + assertThat(fetchedTerm.getStartDate(), is(equalTo(startDate))); + assertThat(fetchedTerm.getEndDate(), is(equalTo(endDate))); + } + + @Test + public void testFindByName() throws SQLException { + // Create and persist an academic term + String termName = "Spring 2025"; + LocalDate startDate = LocalDate.of(2025, 1, 15); + LocalDate endDate = LocalDate.of(2025, 5, 30); + AcademicTerm term = new AcademicTerm(termName, startDate, endDate); + termDAO.save(term); + + // Fetch the term by name + AcademicTerm fetchedTerm = termDAO.findByName(termName); + + // Validate the fetched term + assertThat(fetchedTerm, is(notNullValue())); + assertThat(fetchedTerm.getName(), is(equalTo(termName))); + assertThat(fetchedTerm.getStartDate(), is(equalTo(startDate))); + assertThat(fetchedTerm.getEndDate(), is(equalTo(endDate))); + } + + @Test + public void testFindNonExistentTerm() throws SQLException { + // Try to fetch a term that doesn't exist + AcademicTerm fetchedTerm = termDAO.findById(999); + assertThat(fetchedTerm, is(nullValue())); + + AcademicTerm fetchedByName = termDAO.findByName("Nonexistent Term"); + assertThat(fetchedByName, is(nullValue())); + } + + @Test + public void testFindAllTerms() throws SQLException { + // Create and persist multiple academic terms + AcademicTerm fall2024 = new AcademicTerm("Fall 2024", + LocalDate.of(2024, 9, 1), LocalDate.of(2024, 12, 15)); + AcademicTerm spring2025 = new AcademicTerm("Spring 2025", + LocalDate.of(2025, 1, 15), LocalDate.of(2025, 5, 30)); + AcademicTerm summer2025 = new AcademicTerm("Summer 2025", + LocalDate.of(2025, 6, 15), LocalDate.of(2025, 8, 30)); + + termDAO.save(fall2024); + termDAO.save(spring2025); + termDAO.save(summer2025); + + // Fetch all terms + List allTerms = termDAO.findAll(); + + // Validate using Hamcrest matchers + assertThat(allTerms, hasSize(3)); + assertThat(allTerms, hasItem(hasProperty("name", is("Fall 2024")))); + assertThat(allTerms, hasItem(hasProperty("name", is("Spring 2025")))); + assertThat(allTerms, hasItem(hasProperty("name", is("Summer 2025")))); + + // Verify terms are ordered by start date + assertThat(allTerms.get(0).getName(), is(equalTo("Fall 2024"))); + assertThat(allTerms.get(1).getName(), is(equalTo("Spring 2025"))); + assertThat(allTerms.get(2).getName(), is(equalTo("Summer 2025"))); + } + + @Test + public void testUpdateAcademicTerm() throws SQLException { + // Create and persist an academic term + AcademicTerm term = new AcademicTerm("Fall 2024", + LocalDate.of(2024, 9, 1), LocalDate.of(2024, 12, 15)); + termDAO.save(term); + int termId = term.getId(); + + // Update the term + term.setName("Fall 2024 (Extended)"); + term.setEndDate(LocalDate.of(2024, 12, 20)); + termDAO.update(term); + + // Fetch the updated term + AcademicTerm updatedTerm = termDAO.findById(termId); + + // Validate the update + assertThat(updatedTerm, is(notNullValue())); + assertThat(updatedTerm.getId(), is(equalTo(termId))); + assertThat(updatedTerm.getName(), is(equalTo("Fall 2024 (Extended)"))); + assertThat(updatedTerm.getEndDate(), is(equalTo(LocalDate.of(2024, 12, 20)))); + } + + @Test + public void testDeleteAcademicTerm() throws SQLException { + // Create and persist an academic term + AcademicTerm term = new AcademicTerm("Fall 2024", + LocalDate.of(2024, 9, 1), LocalDate.of(2024, 12, 15)); + termDAO.save(term); + int termId = term.getId(); + + // Verify the term exists + assertThat(termDAO.findById(termId), is(notNullValue())); + + // Delete the term + termDAO.delete(termId); + + // Verify the term no longer exists + assertThat(termDAO.findById(termId), is(nullValue())); + } + + @Test + public void testDatesArePersisted() throws SQLException { + // Create terms with different dates + AcademicTerm term1 = new AcademicTerm("Term 1", + LocalDate.of(2024, 1, 1), LocalDate.of(2024, 3, 31)); + AcademicTerm term2 = new AcademicTerm("Term 2", + LocalDate.of(2024, 6, 1), LocalDate.of(2024, 8, 31)); + + termDAO.save(term1); + termDAO.save(term2); + + // Fetch and verify dates + AcademicTerm fetched1 = termDAO.findByName("Term 1"); + AcademicTerm fetched2 = termDAO.findByName("Term 2"); + + assertThat(fetched1.getStartDate(), is(equalTo(LocalDate.of(2024, 1, 1)))); + assertThat(fetched1.getEndDate(), is(equalTo(LocalDate.of(2024, 3, 31)))); + assertThat(fetched2.getStartDate(), is(equalTo(LocalDate.of(2024, 6, 1)))); + assertThat(fetched2.getEndDate(), is(equalTo(LocalDate.of(2024, 8, 31)))); + } +} + From a897fab47c553206f0eb39640765f7e4649fb43e Mon Sep 17 00:00:00 2001 From: David Whitlock Date: Sun, 28 Dec 2025 07:47:39 -0800 Subject: [PATCH 18/36] An example of how prepared statements can prevent SQL injection attacks. --- .../pdx/cs/joy/jdbc/SQLInjectionExample.java | 214 ++++++++++++++++++ 1 file changed, 214 insertions(+) create mode 100644 examples/src/main/java/edu/pdx/cs/joy/jdbc/SQLInjectionExample.java diff --git a/examples/src/main/java/edu/pdx/cs/joy/jdbc/SQLInjectionExample.java b/examples/src/main/java/edu/pdx/cs/joy/jdbc/SQLInjectionExample.java new file mode 100644 index 000000000..89e3be4c7 --- /dev/null +++ b/examples/src/main/java/edu/pdx/cs/joy/jdbc/SQLInjectionExample.java @@ -0,0 +1,214 @@ +package edu.pdx.cs.joy.jdbc; + +import java.math.BigDecimal; +import java.sql.*; + +/** + * Demonstrates the security vulnerability of using Statement versus PreparedStatement + * for database queries. This example shows how SQL injection attacks work and how + * PreparedStatement protects against them. + */ +public class SQLInjectionExample { + + /** + * Simple Employee class to hold employee data. + */ + static class Employee { + private final String name; + private final String email; + private final BigDecimal salary; + private final String password; + + public Employee(String name, String email, BigDecimal salary, String password) { + this.name = name; + this.email = email; + this.salary = salary; + this.password = password; + } + + public String getName() { + return name; + } + + public String getEmail() { + return email; + } + + public BigDecimal getSalary() { + return salary; + } + + public String getPassword() { + return password; + } + + @Override + public String toString() { + return "Employee{" + + "name='" + name + '\'' + + ", email='" + email + '\'' + + ", salary=" + salary + + ", password='" + password + '\'' + + '}'; + } + } + + /** + * Creates the employees table in the database. + */ + private static void createTable(Connection connection) throws SQLException { + try (Statement statement = connection.createStatement()) { + statement.execute( + "CREATE TABLE employees (" + + " id IDENTITY PRIMARY KEY," + + " name VARCHAR(255) NOT NULL," + + " email VARCHAR(255) NOT NULL," + + " salary DECIMAL(10, 2) NOT NULL," + + " password VARCHAR(255) NOT NULL" + + ")" + ); + } + } + + /** + * Inserts an employee into the database. + */ + private static void insertEmployee(Connection connection, Employee employee) throws SQLException { + String sql = "INSERT INTO employees (name, email, salary, password) VALUES (?, ?, ?, ?)"; + try (PreparedStatement statement = connection.prepareStatement(sql)) { + statement.setString(1, employee.getName()); + statement.setString(2, employee.getEmail()); + statement.setBigDecimal(3, employee.getSalary()); + statement.setString(4, employee.getPassword()); + statement.executeUpdate(); + } + } + + /** + * VULNERABLE: Uses Statement with string concatenation, allowing SQL injection. + * + * This method is intentionally vulnerable to demonstrate the security risk. + * A malicious user can use the username "Dave --'" to comment out the password check, + * gaining unauthorized access to the data. + */ + private static Employee getEmployeeDataWithStatement(Connection connection, String name, String password) throws SQLException { + // SECURITY VULNERABILITY: Building SQL with string concatenation + String sql = "SELECT name, email, salary, password FROM employees WHERE name = '" + name + "' AND password = '" + password + "'"; + + System.out.println("\nExecuting SQL with Statement:"); + System.out.println(" SQL: " + sql); + + try (Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery(sql)) { + + if (resultSet.next()) { + return createEmployee(resultSet); + } + } + + return null; + } + + /** + * SECURE: Uses PreparedStatement with parameter binding, preventing SQL injection. + * + * This method properly uses PreparedStatement, which treats user input as data + * rather than SQL code, preventing SQL injection attacks. + */ + private static Employee getEmployeeDataWithPreparedStatement(Connection connection, String name, String password) throws SQLException { + String sql = "SELECT name, email, salary, password FROM employees WHERE name = ? AND password = ?"; + + System.out.println("\nExecuting SQL with PreparedStatement:"); + System.out.println(" SQL: " + sql); + System.out.println(" Parameter 1 (name): " + name); + System.out.println(" Parameter 2 (password): " + password); + + try (PreparedStatement statement = connection.prepareStatement(sql)) { + statement.setString(1, name); + statement.setString(2, password); + + try (ResultSet resultSet = statement.executeQuery()) { + if (resultSet.next()) { + return createEmployee(resultSet); + } + } + } + + return null; + } + + private static Employee createEmployee(ResultSet resultSet) throws SQLException { + return new Employee( + resultSet.getString("name"), + resultSet.getString("email"), + resultSet.getBigDecimal("salary"), + resultSet.getString("password") + ); + } + + public static void main(String[] args) throws SQLException { + System.out.println("=== SQL Injection Demonstration ===\n"); + + // 1) Create an in-memory H2 database + Connection connection = H2DatabaseHelper.createInMemoryConnection("sqlInjectionDemo"); + + // Create the employees table + createTable(connection); + + // 2) Persist four employees with unique data + insertEmployee(connection, new Employee("Dave", "dave@example.com", new BigDecimal("85000.00"), "securePass123")); + insertEmployee(connection, new Employee("Alice", "alice@example.com", new BigDecimal("90000.00"), "aliceSecret")); + insertEmployee(connection, new Employee("Bob", "bob@example.com", new BigDecimal("78000.00"), "bobPassword")); + insertEmployee(connection, new Employee("Carol", "carol@example.com", new BigDecimal("92000.00"), "carolPass456")); + + System.out.println("Created employees table and inserted 4 employees.\n"); + + // Malicious input: Using SQL injection to bypass password check + String maliciousUsername = "Dave --'"; + String incorrectPassword = "wrongPassword"; + + System.out.println("Attempting to access Dave's data with:"); + System.out.println(" Username: \"" + maliciousUsername + "\""); + System.out.println(" Password: \"" + incorrectPassword + "\" (incorrect)"); + + // 3) Demonstrate SQL injection vulnerability with Statement + System.out.println("\n--- Using Statement (VULNERABLE) ---"); + try { + Employee employee = getEmployeeDataWithStatement(connection, maliciousUsername, incorrectPassword); + if (employee != null) { + System.out.println("\n️SQL INJECTION SUCCESSFUL! Unauthorized access granted:"); + System.out.println(" Name: " + employee.getName()); + System.out.println(" Email: " + employee.getEmail()); + System.out.println(" Salary: $" + String.format("%.2f", employee.getSalary())); + System.out.println(" Password: " + employee.getPassword()); + System.out.println("\nThe SQL comment '--' caused the password check to be ignored!"); + } else { + System.out.println("\nAccess denied (unexpected)"); + } + } catch (SQLException e) { + System.out.println("\nError: " + e.getMessage()); + } + + // 4) Demonstrate protection with PreparedStatement + System.out.println("\n--- Using PreparedStatement (SECURE) ---"); + try { + Employee employee = getEmployeeDataWithPreparedStatement(connection, maliciousUsername, incorrectPassword); + if (employee != null) { + System.out.println("\nAccess granted (unexpected):"); + System.out.println(" " + employee); + } else { + System.out.println("\nAccess denied - No employee found with that name and password combination."); + System.out.println("PreparedStatement treated 'Dave --'' as a literal username, not SQL code."); + } + } catch (SQLException e) { + System.out.println("\nError: " + e.getMessage()); + } + + // Clean up + connection.close(); + + System.out.println("\n=== Summary ==="); + System.out.println("Statement with string concatenation: VULNERABLE to SQL injection"); + System.out.println("PreparedStatement with parameters: SECURE against SQL injection"); + } +} From 8be03478a2e1db1c976f6a1351c19f6ce2b06044 Mon Sep 17 00:00:00 2001 From: David Whitlock Date: Sun, 28 Dec 2025 08:03:12 -0800 Subject: [PATCH 19/36] Use the JDBC Meta Data API to --- .../cs/joy/jdbc/PrintH2DatabaseSchemaIT.java | 186 ++++++++++++++++ .../cs/joy/jdbc/PrintH2DatabaseSchema.java | 201 ++++++++++++++++++ 2 files changed, 387 insertions(+) create mode 100644 examples/src/it/java/edu/pdx/cs/joy/jdbc/PrintH2DatabaseSchemaIT.java create mode 100644 examples/src/main/java/edu/pdx/cs/joy/jdbc/PrintH2DatabaseSchema.java diff --git a/examples/src/it/java/edu/pdx/cs/joy/jdbc/PrintH2DatabaseSchemaIT.java b/examples/src/it/java/edu/pdx/cs/joy/jdbc/PrintH2DatabaseSchemaIT.java new file mode 100644 index 000000000..731ba4c08 --- /dev/null +++ b/examples/src/it/java/edu/pdx/cs/joy/jdbc/PrintH2DatabaseSchemaIT.java @@ -0,0 +1,186 @@ +package edu.pdx.cs.joy.jdbc; + +import edu.pdx.cs.joy.InvokeMainTestCase; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.sql.Connection; +import java.sql.SQLException; +import java.time.LocalDate; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; + +/** + * Integration test for PrintH2DatabaseSchema that validates the program correctly + * displays database schema information using the JDBC DatabaseMetaData API. + */ +public class PrintH2DatabaseSchemaIT extends InvokeMainTestCase { + + private static File tempDbFile; + private static String dbFilePath; + + @BeforeAll + public static void setUp() throws IOException, SQLException { + // Create a temporary file for the database + tempDbFile = Files.createTempFile("PrintH2DatabaseSchemaIT", ".db").toFile(); + dbFilePath = tempDbFile.getAbsolutePath(); + + // Remove the .db extension since H2 will add .mv.db + if (dbFilePath.endsWith(".db")) { + dbFilePath = dbFilePath.substring(0, dbFilePath.length() - 3); + } + + // Create a test database with Department, AcademicTerm, and Course tables + createTestDatabase(dbFilePath); + } + + @AfterAll + public static void tearDown() { + // Clean up database files + deleteIfExists(new File(dbFilePath + ".mv.db")); + deleteIfExists(new File(dbFilePath + ".trace.db")); + deleteIfExists(tempDbFile); + } + + private static void deleteIfExists(File file) { + if (file.exists()) { + file.delete(); + } + } + + /** + * Creates a test database with Department, AcademicTerm, and Course tables + * and populates them with sample data. + */ + private static void createTestDatabase(String dbPath) throws SQLException { + File dbFile = new File(dbPath); + + try (Connection connection = H2DatabaseHelper.createFileBasedConnection(dbFile)) { + // Create tables + DepartmentDAO.createTable(connection); + AcademicTermDAO.createTable(connection); + CourseDAO.createTable(connection); + + // Create DAOs + DepartmentDAO departmentDAO = new DepartmentDAO(connection); + AcademicTermDAO termDAO = new AcademicTermDAO(connection); + CourseDAO courseDAO = new CourseDAO(connection); + + // Insert sample departments + Department csDept = new Department("Computer Science"); + departmentDAO.save(csDept); + + Department mathDept = new Department("Mathematics"); + departmentDAO.save(mathDept); + + // Insert sample academic terms + AcademicTerm fall2024 = new AcademicTerm("Fall 2024", + LocalDate.of(2024, 9, 1), LocalDate.of(2024, 12, 15)); + termDAO.save(fall2024); + + AcademicTerm spring2025 = new AcademicTerm("Spring 2025", + LocalDate.of(2025, 1, 15), LocalDate.of(2025, 5, 30)); + termDAO.save(spring2025); + + // Insert sample courses + Course javaCourse = new Course("Introduction to Java", csDept.getId(), 4); + courseDAO.save(javaCourse); + + Course dataStructures = new Course("Data Structures", csDept.getId(), 4); + courseDAO.save(dataStructures); + + Course calculus = new Course("Calculus I", mathDept.getId(), 4); + courseDAO.save(calculus); + } + } + + @Test + public void testPrintDatabaseSchema() { + // Invoke the main method with the database file path + MainMethodResult result = invokeMain(PrintH2DatabaseSchema.class, dbFilePath); + + String output = result.getTextWrittenToStandardOut(); + + // Validate database information is printed + assertThat(output, containsString("=== Database Information ===")); + assertThat(output, containsString("Database Product: H2")); + assertThat(output, containsString("Driver Name: H2 JDBC Driver")); + + // Validate tables section is printed + assertThat(output, containsString("=== Tables ===")); + + // Validate DEPARTMENTS table is shown + assertThat(output, containsString("Table: DEPARTMENTS")); + assertThat(output, containsString("ID")); + assertThat(output, containsString("NAME")); + assertThat(output, containsString("Primary Keys:")); + + // Validate ACADEMIC_TERMS table is shown + assertThat(output, containsString("Table: ACADEMIC_TERMS")); + assertThat(output, containsString("START_DATE")); + assertThat(output, containsString("END_DATE")); + + // Validate COURSES table is shown + assertThat(output, containsString("Table: COURSES")); + assertThat(output, containsString("TITLE")); + assertThat(output, containsString("DEPARTMENT_ID")); + assertThat(output, containsString("CREDITS")); + + // Validate foreign key relationship is shown + assertThat(output, containsString("Foreign Keys:")); + assertThat(output, containsString("DEPARTMENTS")); + } + + @Test + public void testColumnsAreDisplayed() { + MainMethodResult result = invokeMain(PrintH2DatabaseSchema.class, dbFilePath); + String output = result.getTextWrittenToStandardOut(); + + // Verify that column details are displayed + assertThat(output, containsString("Columns:")); + assertThat(output, containsString("NOT NULL")); + + // Check for specific column types + assertThat(output, containsString("BIGINT")); + assertThat(output, containsString("CHARACTER VARYING") ); + assertThat(output, containsString("DATE")); + assertThat(output, containsString("INTEGER")); + } + + @Test + public void testIndexesAreDisplayed() { + MainMethodResult result = invokeMain(PrintH2DatabaseSchema.class, dbFilePath); + String output = result.getTextWrittenToStandardOut(); + + // Verify that index information is displayed + assertThat(output, containsString("Indexes:")); + assertThat(output, containsString("PRIMARY_KEY")); + } + + @Test + public void testMissingArgumentShowsUsage() { + // Invoke without arguments + MainMethodResult result = invokeMain(PrintH2DatabaseSchema.class); + + String errorOutput = result.getTextWrittenToStandardError(); + + // Validate error message and usage are shown + assertThat(errorOutput, containsString("Missing database file path argument")); + assertThat(errorOutput, containsString("Usage: java PrintH2DatabaseSchema")); + } + + @Test + public void testDatabaseFilePathIsDisplayed() { + MainMethodResult result = invokeMain(PrintH2DatabaseSchema.class, dbFilePath); + String output = result.getTextWrittenToStandardOut(); + + // Verify the database file path is shown + assertThat(output, containsString("Reading schema from H2 database:")); + assertThat(output, containsString(dbFilePath)); + } +} diff --git a/examples/src/main/java/edu/pdx/cs/joy/jdbc/PrintH2DatabaseSchema.java b/examples/src/main/java/edu/pdx/cs/joy/jdbc/PrintH2DatabaseSchema.java new file mode 100644 index 000000000..5e21ec49b --- /dev/null +++ b/examples/src/main/java/edu/pdx/cs/joy/jdbc/PrintH2DatabaseSchema.java @@ -0,0 +1,201 @@ +package edu.pdx.cs.joy.jdbc; + +import java.io.File; +import java.sql.*; + +/** + * A command-line program that uses the JDBC DatabaseMetaData API to print + * information about the tables in an H2 database file. + */ +public class PrintH2DatabaseSchema { + + /** + * Prints information about all tables in the database. + * + * @param connection the database connection + * @throws SQLException if a database error occurs + */ + private static void printDatabaseSchema(Connection connection) throws SQLException { + DatabaseMetaData metaData = connection.getMetaData(); + + System.out.println("=== Database Information ==="); + System.out.println("Database Product: " + metaData.getDatabaseProductName()); + System.out.println("Database Version: " + metaData.getDatabaseProductVersion()); + System.out.println("Driver Name: " + metaData.getDriverName()); + System.out.println("Driver Version: " + metaData.getDriverVersion()); + System.out.println(); + + // Get all tables + System.out.println("=== Tables ==="); + try (ResultSet tables = metaData.getTables(null, null, "%", new String[]{"TABLE"})) { + boolean foundTables = false; + + while (tables.next()) { + foundTables = true; + String tableName = tables.getString("TABLE_NAME"); + String tableType = tables.getString("TABLE_TYPE"); + String remarks = tables.getString("REMARKS"); + + System.out.println("\nTable: " + tableName); + System.out.println(" Type: " + tableType); + if (remarks != null && !remarks.isEmpty()) { + System.out.println(" Remarks: " + remarks); + } + + // Print columns for this table + printTableColumns(metaData, tableName); + + // Print primary keys + printPrimaryKeys(metaData, tableName); + + // Print foreign keys + printForeignKeys(metaData, tableName); + + // Print indexes + printIndexes(metaData, tableName); + } + + if (!foundTables) { + System.out.println("No tables found in the database."); + } + } + } + + /** + * Prints information about columns in a table. + */ + private static void printTableColumns(DatabaseMetaData metaData, String tableName) throws SQLException { + System.out.println(" Columns:"); + try (ResultSet columns = metaData.getColumns(null, null, tableName, "%")) { + while (columns.next()) { + String columnName = columns.getString("COLUMN_NAME"); + String columnType = columns.getString("TYPE_NAME"); + int columnSize = columns.getInt("COLUMN_SIZE"); + String nullable = columns.getString("IS_NULLABLE"); + String defaultValue = columns.getString("COLUMN_DEF"); + + System.out.print(" - " + columnName + " " + columnType); + if (columnSize > 0) { + System.out.print("(" + columnSize + ")"); + } + System.out.print(" [" + (nullable.equals("YES") ? "NULL" : "NOT NULL") + "]"); + if (defaultValue != null) { + System.out.print(" DEFAULT " + defaultValue); + } + System.out.println(); + } + } + } + + /** + * Prints information about primary keys in a table. + */ + private static void printPrimaryKeys(DatabaseMetaData metaData, String tableName) throws SQLException { + System.out.println(" Primary Keys:"); + try (ResultSet primaryKeys = metaData.getPrimaryKeys(null, null, tableName)) { + boolean foundKeys = false; + while (primaryKeys.next()) { + foundKeys = true; + String columnName = primaryKeys.getString("COLUMN_NAME"); + String pkName = primaryKeys.getString("PK_NAME"); + int keySeq = primaryKeys.getInt("KEY_SEQ"); + + System.out.println(" - " + columnName + " (Key: " + pkName + ", Sequence: " + keySeq + ")"); + } + if (!foundKeys) { + System.out.println(" None"); + } + } + } + + /** + * Prints information about foreign keys in a table. + */ + private static void printForeignKeys(DatabaseMetaData metaData, String tableName) throws SQLException { + System.out.println(" Foreign Keys:"); + try (ResultSet foreignKeys = metaData.getImportedKeys(null, null, tableName)) { + boolean foundKeys = false; + while (foreignKeys.next()) { + foundKeys = true; + String fkColumnName = foreignKeys.getString("FKCOLUMN_NAME"); + String pkTableName = foreignKeys.getString("PKTABLE_NAME"); + String pkColumnName = foreignKeys.getString("PKCOLUMN_NAME"); + String fkName = foreignKeys.getString("FK_NAME"); + + System.out.println(" - " + fkColumnName + " -> " + pkTableName + "(" + pkColumnName + ")" + + (fkName != null ? " [" + fkName + "]" : "")); + } + if (!foundKeys) { + System.out.println(" None"); + } + } + } + + /** + * Prints information about indexes in a table. + */ + private static void printIndexes(DatabaseMetaData metaData, String tableName) throws SQLException { + System.out.println(" Indexes:"); + try (ResultSet indexes = metaData.getIndexInfo(null, null, tableName, false, false)) { + boolean foundIndexes = false; + String lastIndexName = null; + StringBuilder indexColumns = new StringBuilder(); + + while (indexes.next()) { + String indexName = indexes.getString("INDEX_NAME"); + String columnName = indexes.getString("COLUMN_NAME"); + boolean nonUnique = indexes.getBoolean("NON_UNIQUE"); + + if (indexName == null) { + continue; // Skip table statistics + } + + if (lastIndexName != null && !lastIndexName.equals(indexName)) { + // Print the previous index + System.out.println(" - " + lastIndexName + " (" + indexColumns + ")"); + indexColumns.setLength(0); + } + + if (indexColumns.length() > 0) { + indexColumns.append(", "); + } + indexColumns.append(columnName); + lastIndexName = indexName; + foundIndexes = true; + } + + // Print the last index + if (lastIndexName != null) { + System.out.println(" - " + lastIndexName + " (" + indexColumns + ")"); + } + + if (!foundIndexes) { + System.out.println(" None"); + } + } + } + + /** + * Main method that takes a database file path and prints the schema. + * + * @param args command line arguments where args[0] is the path to the H2 database file + * @throws SQLException if a database error occurs + */ + public static void main(String[] args) throws SQLException { + if (args.length < 1) { + System.err.println("Missing database file path argument"); + System.err.println("Usage: java PrintH2DatabaseSchema "); + return; + } + + String dbFilePath = args[0]; + File dbFile = new File(dbFilePath); + + System.out.println("Reading schema from H2 database: " + dbFile.getAbsolutePath()); + System.out.println(); + + try (Connection connection = H2DatabaseHelper.createFileBasedConnection(dbFile)) { + printDatabaseSchema(connection); + } + } +} From 4056e4e7852859f49296cd5053772aad2cb126e2 Mon Sep 17 00:00:00 2001 From: David Whitlock Date: Sun, 28 Dec 2025 10:31:29 -0800 Subject: [PATCH 20/36] Introduce DAO interfaces --- .../edu/pdx/cs/joy/jdbc/DepartmentDAOIT.java | 2 +- .../cs/joy/jdbc/PrintH2DatabaseSchemaIT.java | 6 +- .../edu/pdx/cs/joy/jdbc/AcademicTermDAO.java | 153 ++----------- .../pdx/cs/joy/jdbc/AcademicTermDAOImpl.java | 216 ++++++++++++++++++ .../java/edu/pdx/cs/joy/jdbc/CourseDAO.java | 128 +---------- .../edu/pdx/cs/joy/jdbc/CourseDAOImpl.java | 177 ++++++++++++++ .../edu/pdx/cs/joy/jdbc/DepartmentDAO.java | 142 ++---------- .../pdx/cs/joy/jdbc/DepartmentDAOImpl.java | 204 +++++++++++++++++ .../pdx/cs/joy/jdbc/ManageDepartments.java | 2 +- .../pdx/cs/joy/jdbc/AcademicTermDAOTest.java | 2 +- .../edu/pdx/cs/joy/jdbc/CourseDAOTest.java | 4 +- .../pdx/cs/joy/jdbc/DepartmentDAOTest.java | 2 +- 12 files changed, 646 insertions(+), 392 deletions(-) create mode 100644 examples/src/main/java/edu/pdx/cs/joy/jdbc/AcademicTermDAOImpl.java create mode 100644 examples/src/main/java/edu/pdx/cs/joy/jdbc/CourseDAOImpl.java create mode 100644 examples/src/main/java/edu/pdx/cs/joy/jdbc/DepartmentDAOImpl.java diff --git a/examples/src/it/java/edu/pdx/cs/joy/jdbc/DepartmentDAOIT.java b/examples/src/it/java/edu/pdx/cs/joy/jdbc/DepartmentDAOIT.java index 113db8ca6..10853867f 100644 --- a/examples/src/it/java/edu/pdx/cs/joy/jdbc/DepartmentDAOIT.java +++ b/examples/src/it/java/edu/pdx/cs/joy/jdbc/DepartmentDAOIT.java @@ -38,7 +38,7 @@ public static void createTable() throws SQLException { public void setUp() throws SQLException { // Connect to the existing database file connection = H2DatabaseHelper.createFileBasedConnection(new File(dbFilePath)); - departmentDAO = new DepartmentDAO(connection); + departmentDAO = new DepartmentDAOImpl(connection); } @AfterEach diff --git a/examples/src/it/java/edu/pdx/cs/joy/jdbc/PrintH2DatabaseSchemaIT.java b/examples/src/it/java/edu/pdx/cs/joy/jdbc/PrintH2DatabaseSchemaIT.java index 731ba4c08..08e8b6605 100644 --- a/examples/src/it/java/edu/pdx/cs/joy/jdbc/PrintH2DatabaseSchemaIT.java +++ b/examples/src/it/java/edu/pdx/cs/joy/jdbc/PrintH2DatabaseSchemaIT.java @@ -67,9 +67,9 @@ private static void createTestDatabase(String dbPath) throws SQLException { CourseDAO.createTable(connection); // Create DAOs - DepartmentDAO departmentDAO = new DepartmentDAO(connection); - AcademicTermDAO termDAO = new AcademicTermDAO(connection); - CourseDAO courseDAO = new CourseDAO(connection); + DepartmentDAO departmentDAO = new DepartmentDAOImpl(connection); + AcademicTermDAO termDAO = new AcademicTermDAOImpl(connection); + CourseDAO courseDAO = new CourseDAOImpl(connection); // Insert sample departments Department csDept = new Department("Computer Science"); diff --git a/examples/src/main/java/edu/pdx/cs/joy/jdbc/AcademicTermDAO.java b/examples/src/main/java/edu/pdx/cs/joy/jdbc/AcademicTermDAO.java index 7b93aacfc..ab4d96a16 100644 --- a/examples/src/main/java/edu/pdx/cs/joy/jdbc/AcademicTermDAO.java +++ b/examples/src/main/java/edu/pdx/cs/joy/jdbc/AcademicTermDAO.java @@ -1,25 +1,13 @@ package edu.pdx.cs.joy.jdbc; -import java.sql.*; -import java.util.ArrayList; +import java.sql.Connection; +import java.sql.SQLException; import java.util.List; /** - * Data Access Object for managing AcademicTerm entities in the database. - * Demonstrates JDBC operations with date fields: CREATE, READ, UPDATE, DELETE. + * Data Access Object interface for managing AcademicTerm entities in the database. */ -public class AcademicTermDAO { - - private final Connection connection; - - /** - * Creates a new AcademicTermDAO with the specified database connection. - * - * @param connection the database connection to use - */ - public AcademicTermDAO(Connection connection) { - this.connection = connection; - } +public interface AcademicTermDAO { /** * Drops the academic_terms table from the database if it exists. @@ -27,10 +15,8 @@ public AcademicTermDAO(Connection connection) { * @param connection the database connection to use * @throws SQLException if a database error occurs */ - public static void dropTable(Connection connection) throws SQLException { - try (Statement statement = connection.createStatement()) { - statement.execute("DROP TABLE IF EXISTS academic_terms"); - } + static void dropTable(Connection connection) throws SQLException { + AcademicTermDAOImpl.dropTable(connection); } /** @@ -39,17 +25,8 @@ public static void dropTable(Connection connection) throws SQLException { * @param connection the database connection to use * @throws SQLException if a database error occurs */ - public static void createTable(Connection connection) throws SQLException { - try (Statement statement = connection.createStatement()) { - statement.execute( - "CREATE TABLE IF NOT EXISTS academic_terms (" + - " id IDENTITY PRIMARY KEY," + - " name VARCHAR(255) NOT NULL," + - " start_date DATE NOT NULL," + - " end_date DATE NOT NULL" + - ")" - ); - } + static void createTable(Connection connection) throws SQLException { + AcademicTermDAOImpl.createTable(connection); } /** @@ -59,26 +36,7 @@ public static void createTable(Connection connection) throws SQLException { * @param term the academic term to save * @throws SQLException if a database error occurs */ - public void save(AcademicTerm term) throws SQLException { - String sql = "INSERT INTO academic_terms (name, start_date, end_date) VALUES (?, ?, ?)"; - - try (PreparedStatement statement = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { - statement.setString(1, term.getName()); - statement.setDate(2, Date.valueOf(term.getStartDate())); - statement.setDate(3, Date.valueOf(term.getEndDate())); - statement.executeUpdate(); - - // Retrieve the auto-generated ID and set it on the term object - try (ResultSet generatedKeys = statement.getGeneratedKeys()) { - if (generatedKeys.next()) { - int generatedId = generatedKeys.getInt(1); - term.setId(generatedId); - } else { - throw new SQLException("Creating academic term failed, no ID obtained."); - } - } - } - } + void save(AcademicTerm term) throws SQLException; /** * Finds an academic term by its ID. @@ -87,21 +45,7 @@ public void save(AcademicTerm term) throws SQLException { * @return the academic term with the given ID, or null if not found * @throws SQLException if a database error occurs */ - public AcademicTerm findById(int id) throws SQLException { - String sql = "SELECT id, name, start_date, end_date FROM academic_terms WHERE id = ?"; - - try (PreparedStatement statement = connection.prepareStatement(sql)) { - statement.setInt(1, id); - - try (ResultSet resultSet = statement.executeQuery()) { - if (resultSet.next()) { - return extractAcademicTermFromResultSet(resultSet); - } - } - } - - return null; - } + AcademicTerm findById(int id) throws SQLException; /** * Finds an academic term by its name. @@ -110,21 +54,7 @@ public AcademicTerm findById(int id) throws SQLException { * @return the academic term with the given name, or null if not found * @throws SQLException if a database error occurs */ - public AcademicTerm findByName(String name) throws SQLException { - String sql = "SELECT id, name, start_date, end_date FROM academic_terms WHERE name = ?"; - - try (PreparedStatement statement = connection.prepareStatement(sql)) { - statement.setString(1, name); - - try (ResultSet resultSet = statement.executeQuery()) { - if (resultSet.next()) { - return extractAcademicTermFromResultSet(resultSet); - } - } - } - - return null; - } + AcademicTerm findByName(String name) throws SQLException; /** * Finds all academic terms in the database. @@ -132,19 +62,7 @@ public AcademicTerm findByName(String name) throws SQLException { * @return a list of all academic terms * @throws SQLException if a database error occurs */ - public List findAll() throws SQLException { - List terms = new ArrayList<>(); - String sql = "SELECT id, name, start_date, end_date FROM academic_terms ORDER BY start_date"; - - try (Statement statement = connection.createStatement(); - ResultSet resultSet = statement.executeQuery(sql)) { - while (resultSet.next()) { - terms.add(extractAcademicTermFromResultSet(resultSet)); - } - } - - return terms; - } + List findAll() throws SQLException; /** * Updates an existing academic term in the database. @@ -153,21 +71,7 @@ public List findAll() throws SQLException { * @param term the academic term to update * @throws SQLException if a database error occurs */ - public void update(AcademicTerm term) throws SQLException { - String sql = "UPDATE academic_terms SET name = ?, start_date = ?, end_date = ? WHERE id = ?"; - - try (PreparedStatement statement = connection.prepareStatement(sql)) { - statement.setString(1, term.getName()); - statement.setDate(2, Date.valueOf(term.getStartDate())); - statement.setDate(3, Date.valueOf(term.getEndDate())); - statement.setInt(4, term.getId()); - - int rowsAffected = statement.executeUpdate(); - if (rowsAffected == 0) { - throw new SQLException("Update failed, no academic term found with ID: " + term.getId()); - } - } - } + void update(AcademicTerm term) throws SQLException; /** * Deletes an academic term from the database by ID. @@ -175,35 +79,6 @@ public void update(AcademicTerm term) throws SQLException { * @param id the ID of the academic term to delete * @throws SQLException if a database error occurs */ - public void delete(int id) throws SQLException { - String sql = "DELETE FROM academic_terms WHERE id = ?"; - - try (PreparedStatement statement = connection.prepareStatement(sql)) { - statement.setInt(1, id); - int rowsAffected = statement.executeUpdate(); - - if (rowsAffected == 0) { - throw new SQLException("Delete failed, no academic term found with ID: " + id); - } - } - } - - /** - * Extracts an AcademicTerm object from the current row of a ResultSet. - * - * @param resultSet the result set positioned at an academic term row - * @return an AcademicTerm object with data from the result set - * @throws SQLException if a database error occurs - */ - private AcademicTerm extractAcademicTermFromResultSet(ResultSet resultSet) throws SQLException { - int id = resultSet.getInt("id"); - String name = resultSet.getString("name"); - Date startDate = resultSet.getDate("start_date"); - Date endDate = resultSet.getDate("end_date"); - - AcademicTerm term = new AcademicTerm(name, startDate.toLocalDate(), endDate.toLocalDate()); - term.setId(id); - return term; - } + void delete(int id) throws SQLException; } diff --git a/examples/src/main/java/edu/pdx/cs/joy/jdbc/AcademicTermDAOImpl.java b/examples/src/main/java/edu/pdx/cs/joy/jdbc/AcademicTermDAOImpl.java new file mode 100644 index 000000000..eccd56ea4 --- /dev/null +++ b/examples/src/main/java/edu/pdx/cs/joy/jdbc/AcademicTermDAOImpl.java @@ -0,0 +1,216 @@ +package edu.pdx.cs.joy.jdbc; + +import java.sql.*; +import java.util.ArrayList; +import java.util.List; + +/** + * Data Access Object implementation for managing AcademicTerm entities in the database. + * Demonstrates JDBC operations with date fields: CREATE, READ, UPDATE, DELETE. + */ +public class AcademicTermDAOImpl implements AcademicTermDAO { + + private final Connection connection; + + /** + * Creates a new AcademicTermDAOImpl with the specified database connection. + * + * @param connection the database connection to use + */ + public AcademicTermDAOImpl(Connection connection) { + this.connection = connection; + } + + /** + * Drops the academic_terms table from the database if it exists. + * + * @param connection the database connection to use + * @throws SQLException if a database error occurs + */ + public static void dropTable(Connection connection) throws SQLException { + try (Statement statement = connection.createStatement()) { + statement.execute("DROP TABLE IF EXISTS academic_terms"); + } + } + + /** + * Creates the academic_terms table in the database if it does not already exist. + * + * @param connection the database connection to use + * @throws SQLException if a database error occurs + */ + public static void createTable(Connection connection) throws SQLException { + try (Statement statement = connection.createStatement()) { + statement.execute( + "CREATE TABLE IF NOT EXISTS academic_terms (" + + " id IDENTITY PRIMARY KEY," + + " name VARCHAR(255) NOT NULL," + + " start_date DATE NOT NULL," + + " end_date DATE NOT NULL" + + ")" + ); + } + } + + /** + * Saves an academic term to the database. + * The term's ID will be automatically generated by the database and set on the object. + * + * @param term the academic term to save + * @throws SQLException if a database error occurs + */ + @Override + public void save(AcademicTerm term) throws SQLException { + String sql = "INSERT INTO academic_terms (name, start_date, end_date) VALUES (?, ?, ?)"; + + try (PreparedStatement statement = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { + statement.setString(1, term.getName()); + statement.setDate(2, Date.valueOf(term.getStartDate())); + statement.setDate(3, Date.valueOf(term.getEndDate())); + statement.executeUpdate(); + + // Retrieve the auto-generated ID and set it on the term object + try (ResultSet generatedKeys = statement.getGeneratedKeys()) { + if (generatedKeys.next()) { + int generatedId = generatedKeys.getInt(1); + term.setId(generatedId); + } else { + throw new SQLException("Creating academic term failed, no ID obtained."); + } + } + } + } + + /** + * Finds an academic term by its ID. + * + * @param id the ID to search for + * @return the academic term with the given ID, or null if not found + * @throws SQLException if a database error occurs + */ + @Override + public AcademicTerm findById(int id) throws SQLException { + String sql = "SELECT id, name, start_date, end_date FROM academic_terms WHERE id = ?"; + + try (PreparedStatement statement = connection.prepareStatement(sql)) { + statement.setInt(1, id); + + try (ResultSet resultSet = statement.executeQuery()) { + if (resultSet.next()) { + return extractAcademicTermFromResultSet(resultSet); + } + } + } + + return null; + } + + /** + * Finds an academic term by its name. + * + * @param name the name to search for + * @return the academic term with the given name, or null if not found + * @throws SQLException if a database error occurs + */ + @Override + public AcademicTerm findByName(String name) throws SQLException { + String sql = "SELECT id, name, start_date, end_date FROM academic_terms WHERE name = ?"; + + try (PreparedStatement statement = connection.prepareStatement(sql)) { + statement.setString(1, name); + + try (ResultSet resultSet = statement.executeQuery()) { + if (resultSet.next()) { + return extractAcademicTermFromResultSet(resultSet); + } + } + } + + return null; + } + + /** + * Finds all academic terms in the database. + * + * @return a list of all academic terms + * @throws SQLException if a database error occurs + */ + @Override + public List findAll() throws SQLException { + List terms = new ArrayList<>(); + String sql = "SELECT id, name, start_date, end_date FROM academic_terms ORDER BY start_date"; + + try (Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery(sql)) { + while (resultSet.next()) { + terms.add(extractAcademicTermFromResultSet(resultSet)); + } + } + + return terms; + } + + /** + * Updates an existing academic term in the database. + * Uses the term's ID to identify which record to update. + * + * @param term the academic term to update + * @throws SQLException if a database error occurs + */ + @Override + public void update(AcademicTerm term) throws SQLException { + String sql = "UPDATE academic_terms SET name = ?, start_date = ?, end_date = ? WHERE id = ?"; + + try (PreparedStatement statement = connection.prepareStatement(sql)) { + statement.setString(1, term.getName()); + statement.setDate(2, Date.valueOf(term.getStartDate())); + statement.setDate(3, Date.valueOf(term.getEndDate())); + statement.setInt(4, term.getId()); + + int rowsAffected = statement.executeUpdate(); + if (rowsAffected == 0) { + throw new SQLException("Update failed, no academic term found with ID: " + term.getId()); + } + } + } + + /** + * Deletes an academic term from the database by ID. + * + * @param id the ID of the academic term to delete + * @throws SQLException if a database error occurs + */ + @Override + public void delete(int id) throws SQLException { + String sql = "DELETE FROM academic_terms WHERE id = ?"; + + try (PreparedStatement statement = connection.prepareStatement(sql)) { + statement.setInt(1, id); + int rowsAffected = statement.executeUpdate(); + + if (rowsAffected == 0) { + throw new SQLException("Delete failed, no academic term found with ID: " + id); + } + } + } + + /** + * Extracts an AcademicTerm object from the current row of a ResultSet. + * + * @param resultSet the result set positioned at an academic term row + * @return an AcademicTerm object with data from the result set + * @throws SQLException if a database error occurs + */ + private AcademicTerm extractAcademicTermFromResultSet(ResultSet resultSet) throws SQLException { + int id = resultSet.getInt("id"); + String name = resultSet.getString("name"); + Date startDate = resultSet.getDate("start_date"); + Date endDate = resultSet.getDate("end_date"); + + AcademicTerm term = new AcademicTerm(name, startDate.toLocalDate(), endDate.toLocalDate()); + term.setId(id); + return term; + } +} + + diff --git a/examples/src/main/java/edu/pdx/cs/joy/jdbc/CourseDAO.java b/examples/src/main/java/edu/pdx/cs/joy/jdbc/CourseDAO.java index 04d4ba61b..6e261bc80 100644 --- a/examples/src/main/java/edu/pdx/cs/joy/jdbc/CourseDAO.java +++ b/examples/src/main/java/edu/pdx/cs/joy/jdbc/CourseDAO.java @@ -1,25 +1,13 @@ package edu.pdx.cs.joy.jdbc; -import java.sql.*; -import java.util.ArrayList; +import java.sql.Connection; +import java.sql.SQLException; import java.util.List; /** - * Data Access Object for managing Course entities in the database. - * Demonstrates basic JDBC operations: CREATE, READ. + * Data Access Object interface for managing Course entities in the database. */ -public class CourseDAO { - - private final Connection connection; - - /** - * Creates a new CourseDAO with the specified database connection. - * - * @param connection the database connection to use - */ - public CourseDAO(Connection connection) { - this.connection = connection; - } +public interface CourseDAO { /** * Drops the courses table from the database if it exists. @@ -27,10 +15,8 @@ public CourseDAO(Connection connection) { * @param connection the database connection to use * @throws SQLException if a database error occurs */ - public static void dropTable(Connection connection) throws SQLException { - try (Statement statement = connection.createStatement()) { - statement.execute("DROP TABLE IF EXISTS courses"); - } + static void dropTable(Connection connection) throws SQLException { + CourseDAOImpl.dropTable(connection); } /** @@ -39,18 +25,8 @@ public static void dropTable(Connection connection) throws SQLException { * @param connection the database connection to use * @throws SQLException if a database error occurs */ - public static void createTable(Connection connection) throws SQLException { - try (Statement statement = connection.createStatement()) { - statement.execute( - "CREATE TABLE courses (" + - " id IDENTITY PRIMARY KEY," + - " title VARCHAR(255) NOT NULL," + - " department_id INTEGER NOT NULL," + - " credits INTEGER NOT NULL," + - " FOREIGN KEY (department_id) REFERENCES departments(id)" + - ")" - ); - } + static void createTable(Connection connection) throws SQLException { + CourseDAOImpl.createTable(connection); } /** @@ -60,26 +36,7 @@ public static void createTable(Connection connection) throws SQLException { * @param course the course to save * @throws SQLException if a database error occurs */ - public void save(Course course) throws SQLException { - String sql = "INSERT INTO courses (title, department_id, credits) VALUES (?, ?, ?)"; - - try (PreparedStatement statement = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { - statement.setString(1, course.getTitle()); - statement.setInt(2, course.getDepartmentId()); - statement.setInt(3, course.getCredits()); - statement.executeUpdate(); - - // Retrieve the auto-generated ID and set it on the course object - try (ResultSet generatedKeys = statement.getGeneratedKeys()) { - if (generatedKeys.next()) { - int generatedId = generatedKeys.getInt(1); - course.setId(generatedId); - } else { - throw new SQLException("Creating course failed, no ID obtained."); - } - } - } - } + void save(Course course) throws SQLException; /** * Finds a course by its title. @@ -88,21 +45,7 @@ public void save(Course course) throws SQLException { * @return the course with the given title, or null if not found * @throws SQLException if a database error occurs */ - public Course findByTitle(String title) throws SQLException { - String sql = "SELECT id, title, department_id, credits FROM courses WHERE title = ?"; - - try (PreparedStatement statement = connection.prepareStatement(sql)) { - statement.setString(1, title); - - try (ResultSet resultSet = statement.executeQuery()) { - if (resultSet.next()) { - return extractCourseFromResultSet(resultSet); - } - } - } - - return null; - } + Course findByTitle(String title) throws SQLException; /** * Finds all courses associated with a specific department. @@ -111,40 +54,7 @@ public Course findByTitle(String title) throws SQLException { * @return a list of courses in the department * @throws SQLException if a database error occurs */ - public List findByDepartmentId(int departmentId) throws SQLException { - List courses = new ArrayList<>(); - String sql = "SELECT id, title, department_id, credits FROM courses WHERE department_id = ?"; - - try (PreparedStatement statement = connection.prepareStatement(sql)) { - statement.setInt(1, departmentId); - - try (ResultSet resultSet = statement.executeQuery()) { - while (resultSet.next()) { - courses.add(extractCourseFromResultSet(resultSet)); - } - } - } - - return courses; - } - - /** - * Extracts a Course object from the current row of a ResultSet. - * - * @param resultSet the result set positioned at a course row - * @return a Course object with data from the result set - * @throws SQLException if a database error occurs - */ - private Course extractCourseFromResultSet(ResultSet resultSet) throws SQLException { - int id = resultSet.getInt("id"); - String title = resultSet.getString("title"); - int departmentId = resultSet.getInt("department_id"); - int credits = resultSet.getInt("credits"); - - Course course = new Course(title, departmentId, credits); - course.setId(id); - return course; - } + List findByDepartmentId(int departmentId) throws SQLException; /** * Updates an existing course in the database. @@ -153,20 +63,6 @@ private Course extractCourseFromResultSet(ResultSet resultSet) throws SQLExcepti * @param course the course to update * @throws SQLException if a database error occurs */ - public void update(Course course) throws SQLException { - String sql = "UPDATE courses SET title = ?, department_id = ?, credits = ? WHERE id = ?"; - - try (PreparedStatement statement = connection.prepareStatement(sql)) { - statement.setString(1, course.getTitle()); - statement.setInt(2, course.getDepartmentId()); - statement.setInt(3, course.getCredits()); - statement.setInt(4, course.getId()); - - int rowsAffected = statement.executeUpdate(); - if (rowsAffected == 0) { - throw new SQLException("Update failed, no course found with ID: " + course.getId()); - } - } - } + void update(Course course) throws SQLException; } diff --git a/examples/src/main/java/edu/pdx/cs/joy/jdbc/CourseDAOImpl.java b/examples/src/main/java/edu/pdx/cs/joy/jdbc/CourseDAOImpl.java new file mode 100644 index 000000000..e93997426 --- /dev/null +++ b/examples/src/main/java/edu/pdx/cs/joy/jdbc/CourseDAOImpl.java @@ -0,0 +1,177 @@ +package edu.pdx.cs.joy.jdbc; + +import java.sql.*; +import java.util.ArrayList; +import java.util.List; + +/** + * Data Access Object implementation for managing Course entities in the database. + * Demonstrates basic JDBC operations: CREATE, READ, UPDATE. + */ +public class CourseDAOImpl implements CourseDAO { + + private final Connection connection; + + /** + * Creates a new CourseDAOImpl with the specified database connection. + * + * @param connection the database connection to use + */ + public CourseDAOImpl(Connection connection) { + this.connection = connection; + } + + /** + * Drops the courses table from the database if it exists. + * + * @param connection the database connection to use + * @throws SQLException if a database error occurs + */ + public static void dropTable(Connection connection) throws SQLException { + try (Statement statement = connection.createStatement()) { + statement.execute("DROP TABLE IF EXISTS courses"); + } + } + + /** + * Creates the courses table in the database. + * + * @param connection the database connection to use + * @throws SQLException if a database error occurs + */ + public static void createTable(Connection connection) throws SQLException { + try (Statement statement = connection.createStatement()) { + statement.execute( + "CREATE TABLE courses (" + + " id IDENTITY PRIMARY KEY," + + " title VARCHAR(255) NOT NULL," + + " department_id INTEGER NOT NULL," + + " credits INTEGER NOT NULL," + + " FOREIGN KEY (department_id) REFERENCES departments(id)" + + ")" + ); + } + } + + /** + * Saves a course to the database. + * The course's ID will be automatically generated by the database and set on the object. + * + * @param course the course to save + * @throws SQLException if a database error occurs + */ + @Override + public void save(Course course) throws SQLException { + String sql = "INSERT INTO courses (title, department_id, credits) VALUES (?, ?, ?)"; + + try (PreparedStatement statement = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { + statement.setString(1, course.getTitle()); + statement.setInt(2, course.getDepartmentId()); + statement.setInt(3, course.getCredits()); + statement.executeUpdate(); + + // Retrieve the auto-generated ID and set it on the course object + try (ResultSet generatedKeys = statement.getGeneratedKeys()) { + if (generatedKeys.next()) { + int generatedId = generatedKeys.getInt(1); + course.setId(generatedId); + } else { + throw new SQLException("Creating course failed, no ID obtained."); + } + } + } + } + + /** + * Finds a course by its title. + * + * @param title the title to search for + * @return the course with the given title, or null if not found + * @throws SQLException if a database error occurs + */ + @Override + public Course findByTitle(String title) throws SQLException { + String sql = "SELECT id, title, department_id, credits FROM courses WHERE title = ?"; + + try (PreparedStatement statement = connection.prepareStatement(sql)) { + statement.setString(1, title); + + try (ResultSet resultSet = statement.executeQuery()) { + if (resultSet.next()) { + return extractCourseFromResultSet(resultSet); + } + } + } + + return null; + } + + /** + * Finds all courses associated with a specific department. + * + * @param departmentId the department ID to search for + * @return a list of courses in the department + * @throws SQLException if a database error occurs + */ + @Override + public List findByDepartmentId(int departmentId) throws SQLException { + List courses = new ArrayList<>(); + String sql = "SELECT id, title, department_id, credits FROM courses WHERE department_id = ?"; + + try (PreparedStatement statement = connection.prepareStatement(sql)) { + statement.setInt(1, departmentId); + + try (ResultSet resultSet = statement.executeQuery()) { + while (resultSet.next()) { + courses.add(extractCourseFromResultSet(resultSet)); + } + } + } + + return courses; + } + + /** + * Extracts a Course object from the current row of a ResultSet. + * + * @param resultSet the result set positioned at a course row + * @return a Course object with data from the result set + * @throws SQLException if a database error occurs + */ + private Course extractCourseFromResultSet(ResultSet resultSet) throws SQLException { + int id = resultSet.getInt("id"); + String title = resultSet.getString("title"); + int departmentId = resultSet.getInt("department_id"); + int credits = resultSet.getInt("credits"); + + Course course = new Course(title, departmentId, credits); + course.setId(id); + return course; + } + + /** + * Updates an existing course in the database. + * Uses the course's ID to identify which record to update. + * + * @param course the course to update + * @throws SQLException if a database error occurs + */ + @Override + public void update(Course course) throws SQLException { + String sql = "UPDATE courses SET title = ?, department_id = ?, credits = ? WHERE id = ?"; + + try (PreparedStatement statement = connection.prepareStatement(sql)) { + statement.setString(1, course.getTitle()); + statement.setInt(2, course.getDepartmentId()); + statement.setInt(3, course.getCredits()); + statement.setInt(4, course.getId()); + + int rowsAffected = statement.executeUpdate(); + if (rowsAffected == 0) { + throw new SQLException("Update failed, no course found with ID: " + course.getId()); + } + } + } +} + + diff --git a/examples/src/main/java/edu/pdx/cs/joy/jdbc/DepartmentDAO.java b/examples/src/main/java/edu/pdx/cs/joy/jdbc/DepartmentDAO.java index 0014e77d5..caf405473 100644 --- a/examples/src/main/java/edu/pdx/cs/joy/jdbc/DepartmentDAO.java +++ b/examples/src/main/java/edu/pdx/cs/joy/jdbc/DepartmentDAO.java @@ -1,25 +1,13 @@ package edu.pdx.cs.joy.jdbc; -import java.sql.*; -import java.util.ArrayList; +import java.sql.Connection; +import java.sql.SQLException; import java.util.List; /** - * Data Access Object for managing Department entities in the database. - * Demonstrates basic JDBC operations: CREATE, READ. + * Data Access Object interface for managing Department entities in the database. */ -public class DepartmentDAO { - - private final Connection connection; - - /** - * Creates a new DepartmentDAO with the specified database connection. - * - * @param connection the database connection to use - */ - public DepartmentDAO(Connection connection) { - this.connection = connection; - } +public interface DepartmentDAO { /** * Drops the departments table from the database if it exists. @@ -27,10 +15,8 @@ public DepartmentDAO(Connection connection) { * @param connection the database connection to use * @throws SQLException if a database error occurs */ - public static void dropTable(Connection connection) throws SQLException { - try (Statement statement = connection.createStatement()) { - statement.execute("DROP TABLE IF EXISTS departments"); - } + static void dropTable(Connection connection) throws SQLException { + DepartmentDAOImpl.dropTable(connection); } /** @@ -39,15 +25,8 @@ public static void dropTable(Connection connection) throws SQLException { * @param connection the database connection to use * @throws SQLException if a database error occurs */ - public static void createTable(Connection connection) throws SQLException { - try (Statement statement = connection.createStatement()) { - statement.execute( - "CREATE TABLE IF NOT EXISTS departments (" + - " id IDENTITY PRIMARY KEY," + - " name VARCHAR(255) NOT NULL" + - ")" - ); - } + static void createTable(Connection connection) throws SQLException { + DepartmentDAOImpl.createTable(connection); } /** @@ -57,24 +36,7 @@ public static void createTable(Connection connection) throws SQLException { * @param department the department to save * @throws SQLException if a database error occurs */ - public void save(Department department) throws SQLException { - String sql = "INSERT INTO departments (name) VALUES (?)"; - - try (PreparedStatement statement = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { - statement.setString(1, department.getName()); - statement.executeUpdate(); - - // Retrieve the auto-generated ID and set it on the department object - try (ResultSet generatedKeys = statement.getGeneratedKeys()) { - if (generatedKeys.next()) { - int generatedId = generatedKeys.getInt(1); - department.setId(generatedId); - } else { - throw new SQLException("Creating department failed, no ID obtained."); - } - } - } - } + void save(Department department) throws SQLException; /** * Finds a department by its ID. @@ -83,21 +45,7 @@ public void save(Department department) throws SQLException { * @return the department with the given ID, or null if not found * @throws SQLException if a database error occurs */ - public Department findById(int id) throws SQLException { - String sql = "SELECT id, name FROM departments WHERE id = ?"; - - try (PreparedStatement statement = connection.prepareStatement(sql)) { - statement.setInt(1, id); - - try (ResultSet resultSet = statement.executeQuery()) { - if (resultSet.next()) { - return extractDepartmentFromResultSet(resultSet); - } - } - } - - return null; - } + Department findById(int id) throws SQLException; /** * Finds a department by its name. @@ -106,21 +54,7 @@ public Department findById(int id) throws SQLException { * @return the department with the given name, or null if not found * @throws SQLException if a database error occurs */ - public Department findByName(String name) throws SQLException { - String sql = "SELECT id, name FROM departments WHERE name = ?"; - - try (PreparedStatement statement = connection.prepareStatement(sql)) { - statement.setString(1, name); - - try (ResultSet resultSet = statement.executeQuery()) { - if (resultSet.next()) { - return extractDepartmentFromResultSet(resultSet); - } - } - } - - return null; - } + Department findByName(String name) throws SQLException; /** * Finds all departments in the database. @@ -128,32 +62,7 @@ public Department findByName(String name) throws SQLException { * @return a list of all departments * @throws SQLException if a database error occurs */ - public List findAll() throws SQLException { - List departments = new ArrayList<>(); - String sql = "SELECT id, name FROM departments ORDER BY id"; - - try (Statement statement = connection.createStatement(); - ResultSet resultSet = statement.executeQuery(sql)) { - while (resultSet.next()) { - departments.add(extractDepartmentFromResultSet(resultSet)); - } - } - - return departments; - } - - /** - * Extracts a Department object from the current row of a ResultSet. - * - * @param resultSet the result set positioned at a department row - * @return a Department object with data from the result set - * @throws SQLException if a database error occurs - */ - private Department extractDepartmentFromResultSet(ResultSet resultSet) throws SQLException { - int id = resultSet.getInt("id"); - String name = resultSet.getString("name"); - return new Department(id, name); - } + List findAll() throws SQLException; /** * Updates an existing department in the database. @@ -161,19 +70,7 @@ private Department extractDepartmentFromResultSet(ResultSet resultSet) throws SQ * @param department the department to update * @throws SQLException if a database error occurs */ - public void update(Department department) throws SQLException { - String sql = "UPDATE departments SET name = ? WHERE id = ?"; - - try (PreparedStatement statement = connection.prepareStatement(sql)) { - statement.setString(1, department.getName()); - statement.setInt(2, department.getId()); - int rowsAffected = statement.executeUpdate(); - - if (rowsAffected == 0) { - throw new SQLException("Update failed, no department found with ID: " + department.getId()); - } - } - } + void update(Department department) throws SQLException; /** * Deletes a department from the database by ID. @@ -181,17 +78,6 @@ public void update(Department department) throws SQLException { * @param id the ID of the department to delete * @throws SQLException if a database error occurs */ - public void delete(int id) throws SQLException { - String sql = "DELETE FROM departments WHERE id = ?"; - - try (PreparedStatement statement = connection.prepareStatement(sql)) { - statement.setInt(1, id); - int rowsAffected = statement.executeUpdate(); - - if (rowsAffected == 0) { - throw new SQLException("Delete failed, no department found with ID: " + id); - } - } - } + void delete(int id) throws SQLException; } diff --git a/examples/src/main/java/edu/pdx/cs/joy/jdbc/DepartmentDAOImpl.java b/examples/src/main/java/edu/pdx/cs/joy/jdbc/DepartmentDAOImpl.java new file mode 100644 index 000000000..3d233b255 --- /dev/null +++ b/examples/src/main/java/edu/pdx/cs/joy/jdbc/DepartmentDAOImpl.java @@ -0,0 +1,204 @@ +package edu.pdx.cs.joy.jdbc; + +import java.sql.*; +import java.util.ArrayList; +import java.util.List; + +/** + * Data Access Object implementation for managing Department entities in the database. + * Demonstrates JDBC operations with auto-generated keys: CREATE, READ, UPDATE, DELETE. + */ +public class DepartmentDAOImpl implements DepartmentDAO { + + private final Connection connection; + + /** + * Creates a new DepartmentDAOImpl with the specified database connection. + * + * @param connection the database connection to use + */ + public DepartmentDAOImpl(Connection connection) { + this.connection = connection; + } + + /** + * Drops the departments table from the database if it exists. + * + * @param connection the database connection to use + * @throws SQLException if a database error occurs + */ + public static void dropTable(Connection connection) throws SQLException { + try (Statement statement = connection.createStatement()) { + statement.execute("DROP TABLE IF EXISTS departments"); + } + } + + /** + * Creates the departments table in the database if it does not already exist. + * + * @param connection the database connection to use + * @throws SQLException if a database error occurs + */ + public static void createTable(Connection connection) throws SQLException { + try (Statement statement = connection.createStatement()) { + statement.execute( + "CREATE TABLE IF NOT EXISTS departments (" + + " id IDENTITY PRIMARY KEY," + + " name VARCHAR(255) NOT NULL" + + ")" + ); + } + } + + /** + * Saves a department to the database. + * The department's ID will be automatically generated by the database. + * + * @param department the department to save + * @throws SQLException if a database error occurs + */ + @Override + public void save(Department department) throws SQLException { + String sql = "INSERT INTO departments (name) VALUES (?)"; + + try (PreparedStatement statement = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { + statement.setString(1, department.getName()); + statement.executeUpdate(); + + // Retrieve the auto-generated ID and set it on the department object + try (ResultSet generatedKeys = statement.getGeneratedKeys()) { + if (generatedKeys.next()) { + int generatedId = generatedKeys.getInt(1); + department.setId(generatedId); + } else { + throw new SQLException("Creating department failed, no ID obtained."); + } + } + } + } + + /** + * Finds a department by its ID. + * + * @param id the ID to search for + * @return the department with the given ID, or null if not found + * @throws SQLException if a database error occurs + */ + @Override + public Department findById(int id) throws SQLException { + String sql = "SELECT id, name FROM departments WHERE id = ?"; + + try (PreparedStatement statement = connection.prepareStatement(sql)) { + statement.setInt(1, id); + + try (ResultSet resultSet = statement.executeQuery()) { + if (resultSet.next()) { + return extractDepartmentFromResultSet(resultSet); + } + } + } + + return null; + } + + /** + * Finds a department by its name. + * + * @param name the name to search for + * @return the department with the given name, or null if not found + * @throws SQLException if a database error occurs + */ + @Override + public Department findByName(String name) throws SQLException { + String sql = "SELECT id, name FROM departments WHERE name = ?"; + + try (PreparedStatement statement = connection.prepareStatement(sql)) { + statement.setString(1, name); + + try (ResultSet resultSet = statement.executeQuery()) { + if (resultSet.next()) { + return extractDepartmentFromResultSet(resultSet); + } + } + } + + return null; + } + + /** + * Finds all departments in the database. + * + * @return a list of all departments + * @throws SQLException if a database error occurs + */ + @Override + public List findAll() throws SQLException { + List departments = new ArrayList<>(); + String sql = "SELECT id, name FROM departments ORDER BY id"; + + try (Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery(sql)) { + while (resultSet.next()) { + departments.add(extractDepartmentFromResultSet(resultSet)); + } + } + + return departments; + } + + /** + * Extracts a Department object from the current row of a ResultSet. + * + * @param resultSet the result set positioned at a department row + * @return a Department object with data from the result set + * @throws SQLException if a database error occurs + */ + private Department extractDepartmentFromResultSet(ResultSet resultSet) throws SQLException { + int id = resultSet.getInt("id"); + String name = resultSet.getString("name"); + return new Department(id, name); + } + + /** + * Updates an existing department in the database. + * + * @param department the department to update + * @throws SQLException if a database error occurs + */ + @Override + public void update(Department department) throws SQLException { + String sql = "UPDATE departments SET name = ? WHERE id = ?"; + + try (PreparedStatement statement = connection.prepareStatement(sql)) { + statement.setString(1, department.getName()); + statement.setInt(2, department.getId()); + int rowsAffected = statement.executeUpdate(); + + if (rowsAffected == 0) { + throw new SQLException("Update failed, no department found with ID: " + department.getId()); + } + } + } + + /** + * Deletes a department from the database by ID. + * + * @param id the ID of the department to delete + * @throws SQLException if a database error occurs + */ + @Override + public void delete(int id) throws SQLException { + String sql = "DELETE FROM departments WHERE id = ?"; + + try (PreparedStatement statement = connection.prepareStatement(sql)) { + statement.setInt(1, id); + int rowsAffected = statement.executeUpdate(); + + if (rowsAffected == 0) { + throw new SQLException("Delete failed, no department found with ID: " + id); + } + } + } +} + + diff --git a/examples/src/main/java/edu/pdx/cs/joy/jdbc/ManageDepartments.java b/examples/src/main/java/edu/pdx/cs/joy/jdbc/ManageDepartments.java index 7579deb86..f26d73ccd 100644 --- a/examples/src/main/java/edu/pdx/cs/joy/jdbc/ManageDepartments.java +++ b/examples/src/main/java/edu/pdx/cs/joy/jdbc/ManageDepartments.java @@ -34,7 +34,7 @@ public static void main(String[] args) throws SQLException { DepartmentDAO.createTable(connection); // Create a new DepartmentDAO - DepartmentDAO departmentDAO = new DepartmentDAO(connection); + DepartmentDAO departmentDAO = new DepartmentDAOImpl(connection); switch (command) { case "create": diff --git a/examples/src/test/java/edu/pdx/cs/joy/jdbc/AcademicTermDAOTest.java b/examples/src/test/java/edu/pdx/cs/joy/jdbc/AcademicTermDAOTest.java index 64f14fb54..9f4ececa9 100644 --- a/examples/src/test/java/edu/pdx/cs/joy/jdbc/AcademicTermDAOTest.java +++ b/examples/src/test/java/edu/pdx/cs/joy/jdbc/AcademicTermDAOTest.java @@ -27,7 +27,7 @@ public void setUp() throws SQLException { AcademicTermDAO.createTable(connection); // Initialize the DAO with the connection - termDAO = new AcademicTermDAO(connection); + termDAO = new AcademicTermDAOImpl(connection); } @AfterEach diff --git a/examples/src/test/java/edu/pdx/cs/joy/jdbc/CourseDAOTest.java b/examples/src/test/java/edu/pdx/cs/joy/jdbc/CourseDAOTest.java index 4cdddeca3..e1b4251b1 100644 --- a/examples/src/test/java/edu/pdx/cs/joy/jdbc/CourseDAOTest.java +++ b/examples/src/test/java/edu/pdx/cs/joy/jdbc/CourseDAOTest.java @@ -33,8 +33,8 @@ public void setUp() throws SQLException { CourseDAO.createTable(connection); // Initialize the DAOs with the connection - courseDAO = new CourseDAO(connection); - departmentDAO = new DepartmentDAO(connection); + courseDAO = new CourseDAOImpl(connection); + departmentDAO = new DepartmentDAOImpl(connection); } @AfterEach diff --git a/examples/src/test/java/edu/pdx/cs/joy/jdbc/DepartmentDAOTest.java b/examples/src/test/java/edu/pdx/cs/joy/jdbc/DepartmentDAOTest.java index 6dcdaf10e..0caf4db46 100644 --- a/examples/src/test/java/edu/pdx/cs/joy/jdbc/DepartmentDAOTest.java +++ b/examples/src/test/java/edu/pdx/cs/joy/jdbc/DepartmentDAOTest.java @@ -26,7 +26,7 @@ public void setUp() throws SQLException { DepartmentDAO.createTable(connection); // Initialize the DAO with the connection - departmentDAO = new DepartmentDAO(connection); + departmentDAO = new DepartmentDAOImpl(connection); } @AfterEach From 8e1220b34f845b3aad2775094c8e81c759c9a316 Mon Sep 17 00:00:00 2001 From: David Whitlock Date: Sun, 28 Dec 2025 10:35:40 -0800 Subject: [PATCH 21/36] Inline the createTable() and dropTable() methods from the DAO interfaces so that the DAO interface doesn't know about JDBC Connection objects. --- .../edu/pdx/cs/joy/jdbc/DepartmentDAOIT.java | 4 ++-- .../cs/joy/jdbc/PrintH2DatabaseSchemaIT.java | 6 +++--- .../edu/pdx/cs/joy/jdbc/AcademicTermDAO.java | 21 ------------------- .../java/edu/pdx/cs/joy/jdbc/CourseDAO.java | 21 ------------------- .../edu/pdx/cs/joy/jdbc/DepartmentDAO.java | 21 ------------------- .../pdx/cs/joy/jdbc/ManageDepartments.java | 2 +- .../pdx/cs/joy/jdbc/AcademicTermDAOTest.java | 6 +++--- .../edu/pdx/cs/joy/jdbc/CourseDAOTest.java | 12 +++++------ .../pdx/cs/joy/jdbc/DepartmentDAOTest.java | 6 +++--- 9 files changed, 18 insertions(+), 81 deletions(-) diff --git a/examples/src/it/java/edu/pdx/cs/joy/jdbc/DepartmentDAOIT.java b/examples/src/it/java/edu/pdx/cs/joy/jdbc/DepartmentDAOIT.java index 10853867f..24290e3b2 100644 --- a/examples/src/it/java/edu/pdx/cs/joy/jdbc/DepartmentDAOIT.java +++ b/examples/src/it/java/edu/pdx/cs/joy/jdbc/DepartmentDAOIT.java @@ -29,7 +29,7 @@ public static void createTable() throws SQLException { Connection connection = H2DatabaseHelper.createFileBasedConnection(new File(dbFilePath)); // Create the departments table - DepartmentDAO.createTable(connection); + DepartmentDAOImpl.createTable(connection); connection.close(); } @@ -52,7 +52,7 @@ public void tearDown() throws SQLException { public static void cleanUp() throws SQLException { // Connect one final time to drop the table and clean up Connection connection = H2DatabaseHelper.createFileBasedConnection(new File(dbFilePath)); - DepartmentDAO.dropTable(connection); + DepartmentDAOImpl.dropTable(connection); connection.close(); // Delete the database files diff --git a/examples/src/it/java/edu/pdx/cs/joy/jdbc/PrintH2DatabaseSchemaIT.java b/examples/src/it/java/edu/pdx/cs/joy/jdbc/PrintH2DatabaseSchemaIT.java index 08e8b6605..6d2fdadcb 100644 --- a/examples/src/it/java/edu/pdx/cs/joy/jdbc/PrintH2DatabaseSchemaIT.java +++ b/examples/src/it/java/edu/pdx/cs/joy/jdbc/PrintH2DatabaseSchemaIT.java @@ -62,9 +62,9 @@ private static void createTestDatabase(String dbPath) throws SQLException { try (Connection connection = H2DatabaseHelper.createFileBasedConnection(dbFile)) { // Create tables - DepartmentDAO.createTable(connection); - AcademicTermDAO.createTable(connection); - CourseDAO.createTable(connection); + DepartmentDAOImpl.createTable(connection); + AcademicTermDAOImpl.createTable(connection); + CourseDAOImpl.createTable(connection); // Create DAOs DepartmentDAO departmentDAO = new DepartmentDAOImpl(connection); diff --git a/examples/src/main/java/edu/pdx/cs/joy/jdbc/AcademicTermDAO.java b/examples/src/main/java/edu/pdx/cs/joy/jdbc/AcademicTermDAO.java index ab4d96a16..6b041ad01 100644 --- a/examples/src/main/java/edu/pdx/cs/joy/jdbc/AcademicTermDAO.java +++ b/examples/src/main/java/edu/pdx/cs/joy/jdbc/AcademicTermDAO.java @@ -1,6 +1,5 @@ package edu.pdx.cs.joy.jdbc; -import java.sql.Connection; import java.sql.SQLException; import java.util.List; @@ -9,26 +8,6 @@ */ public interface AcademicTermDAO { - /** - * Drops the academic_terms table from the database if it exists. - * - * @param connection the database connection to use - * @throws SQLException if a database error occurs - */ - static void dropTable(Connection connection) throws SQLException { - AcademicTermDAOImpl.dropTable(connection); - } - - /** - * Creates the academic_terms table in the database if it does not already exist. - * - * @param connection the database connection to use - * @throws SQLException if a database error occurs - */ - static void createTable(Connection connection) throws SQLException { - AcademicTermDAOImpl.createTable(connection); - } - /** * Saves an academic term to the database. * The term's ID will be automatically generated by the database and set on the object. diff --git a/examples/src/main/java/edu/pdx/cs/joy/jdbc/CourseDAO.java b/examples/src/main/java/edu/pdx/cs/joy/jdbc/CourseDAO.java index 6e261bc80..32d05bf60 100644 --- a/examples/src/main/java/edu/pdx/cs/joy/jdbc/CourseDAO.java +++ b/examples/src/main/java/edu/pdx/cs/joy/jdbc/CourseDAO.java @@ -1,6 +1,5 @@ package edu.pdx.cs.joy.jdbc; -import java.sql.Connection; import java.sql.SQLException; import java.util.List; @@ -9,26 +8,6 @@ */ public interface CourseDAO { - /** - * Drops the courses table from the database if it exists. - * - * @param connection the database connection to use - * @throws SQLException if a database error occurs - */ - static void dropTable(Connection connection) throws SQLException { - CourseDAOImpl.dropTable(connection); - } - - /** - * Creates the courses table in the database. - * - * @param connection the database connection to use - * @throws SQLException if a database error occurs - */ - static void createTable(Connection connection) throws SQLException { - CourseDAOImpl.createTable(connection); - } - /** * Saves a course to the database. * The course's ID will be automatically generated by the database and set on the object. diff --git a/examples/src/main/java/edu/pdx/cs/joy/jdbc/DepartmentDAO.java b/examples/src/main/java/edu/pdx/cs/joy/jdbc/DepartmentDAO.java index caf405473..bcdc18f9e 100644 --- a/examples/src/main/java/edu/pdx/cs/joy/jdbc/DepartmentDAO.java +++ b/examples/src/main/java/edu/pdx/cs/joy/jdbc/DepartmentDAO.java @@ -1,6 +1,5 @@ package edu.pdx.cs.joy.jdbc; -import java.sql.Connection; import java.sql.SQLException; import java.util.List; @@ -9,26 +8,6 @@ */ public interface DepartmentDAO { - /** - * Drops the departments table from the database if it exists. - * - * @param connection the database connection to use - * @throws SQLException if a database error occurs - */ - static void dropTable(Connection connection) throws SQLException { - DepartmentDAOImpl.dropTable(connection); - } - - /** - * Creates the departments table in the database if it does not already exist. - * - * @param connection the database connection to use - * @throws SQLException if a database error occurs - */ - static void createTable(Connection connection) throws SQLException { - DepartmentDAOImpl.createTable(connection); - } - /** * Saves a department to the database. * The department's ID will be automatically generated by the database. diff --git a/examples/src/main/java/edu/pdx/cs/joy/jdbc/ManageDepartments.java b/examples/src/main/java/edu/pdx/cs/joy/jdbc/ManageDepartments.java index f26d73ccd..2762cd775 100644 --- a/examples/src/main/java/edu/pdx/cs/joy/jdbc/ManageDepartments.java +++ b/examples/src/main/java/edu/pdx/cs/joy/jdbc/ManageDepartments.java @@ -31,7 +31,7 @@ public static void main(String[] args) throws SQLException { try (Connection connection = H2DatabaseHelper.createFileBasedConnection(dbFile)) { // Create the departments table if it doesn't exist - DepartmentDAO.createTable(connection); + DepartmentDAOImpl.createTable(connection); // Create a new DepartmentDAO DepartmentDAO departmentDAO = new DepartmentDAOImpl(connection); diff --git a/examples/src/test/java/edu/pdx/cs/joy/jdbc/AcademicTermDAOTest.java b/examples/src/test/java/edu/pdx/cs/joy/jdbc/AcademicTermDAOTest.java index 9f4ececa9..a5a4b1956 100644 --- a/examples/src/test/java/edu/pdx/cs/joy/jdbc/AcademicTermDAOTest.java +++ b/examples/src/test/java/edu/pdx/cs/joy/jdbc/AcademicTermDAOTest.java @@ -23,8 +23,8 @@ public void setUp() throws SQLException { connection = H2DatabaseHelper.createInMemoryConnection("test"); // Drop and create the academic_terms table - AcademicTermDAO.dropTable(connection); - AcademicTermDAO.createTable(connection); + AcademicTermDAOImpl.dropTable(connection); + AcademicTermDAOImpl.createTable(connection); // Initialize the DAO with the connection termDAO = new AcademicTermDAOImpl(connection); @@ -33,7 +33,7 @@ public void setUp() throws SQLException { @AfterEach public void tearDown() throws SQLException { if (connection != null && !connection.isClosed()) { - AcademicTermDAO.dropTable(connection); + AcademicTermDAOImpl.dropTable(connection); connection.close(); } } diff --git a/examples/src/test/java/edu/pdx/cs/joy/jdbc/CourseDAOTest.java b/examples/src/test/java/edu/pdx/cs/joy/jdbc/CourseDAOTest.java index e1b4251b1..23c4a75a6 100644 --- a/examples/src/test/java/edu/pdx/cs/joy/jdbc/CourseDAOTest.java +++ b/examples/src/test/java/edu/pdx/cs/joy/jdbc/CourseDAOTest.java @@ -25,12 +25,12 @@ public void setUp() throws SQLException { // Drop tables if they exist from a previous test, then create them // Note: Must drop courses first due to foreign key constraint - CourseDAO.dropTable(connection); - DepartmentDAO.dropTable(connection); + CourseDAOImpl.dropTable(connection); + DepartmentDAOImpl.dropTable(connection); // Create departments table first, then courses (due to foreign key) - DepartmentDAO.createTable(connection); - CourseDAO.createTable(connection); + DepartmentDAOImpl.createTable(connection); + CourseDAOImpl.createTable(connection); // Initialize the DAOs with the connection courseDAO = new CourseDAOImpl(connection); @@ -42,8 +42,8 @@ public void tearDown() throws SQLException { if (connection != null && !connection.isClosed()) { // Drop tables and close the connection // Note: Must drop courses first due to foreign key constraint - CourseDAO.dropTable(connection); - DepartmentDAO.dropTable(connection); + CourseDAOImpl.dropTable(connection); + DepartmentDAOImpl.dropTable(connection); connection.close(); } } diff --git a/examples/src/test/java/edu/pdx/cs/joy/jdbc/DepartmentDAOTest.java b/examples/src/test/java/edu/pdx/cs/joy/jdbc/DepartmentDAOTest.java index 0caf4db46..abd3b6ad9 100644 --- a/examples/src/test/java/edu/pdx/cs/joy/jdbc/DepartmentDAOTest.java +++ b/examples/src/test/java/edu/pdx/cs/joy/jdbc/DepartmentDAOTest.java @@ -22,8 +22,8 @@ public void setUp() throws SQLException { connection = H2DatabaseHelper.createInMemoryConnection("test"); // Drop the table if it exists from a previous test, then create it - DepartmentDAO.dropTable(connection); - DepartmentDAO.createTable(connection); + DepartmentDAOImpl.dropTable(connection); + DepartmentDAOImpl.createTable(connection); // Initialize the DAO with the connection departmentDAO = new DepartmentDAOImpl(connection); @@ -33,7 +33,7 @@ public void setUp() throws SQLException { public void tearDown() throws SQLException { if (connection != null && !connection.isClosed()) { // Drop the table and close the connection - DepartmentDAO.dropTable(connection); + DepartmentDAOImpl.dropTable(connection); connection.close(); } } From 916abffbbe62b57415d806e9bab0e2b1664c9ba2 Mon Sep 17 00:00:00 2001 From: David Whitlock Date: Mon, 29 Dec 2025 19:18:28 -0800 Subject: [PATCH 22/36] Data Access Objects (DAOs) for persisting FamilyTree objects to a relational database. --- .../edu/pdx/cs/joy/jdbc/FamilyTreeDAO.java | 51 +++ .../pdx/cs/joy/jdbc/FamilyTreeDAOImpl.java | 193 ++++++++++++ .../java/edu/pdx/cs/joy/jdbc/MarriageDAO.java | 68 ++++ .../edu/pdx/cs/joy/jdbc/MarriageDAOImpl.java | 193 ++++++++++++ .../java/edu/pdx/cs/joy/jdbc/PersonDAO.java | 75 +++++ .../edu/pdx/cs/joy/jdbc/PersonDAOImpl.java | 292 ++++++++++++++++++ .../pdx/cs/joy/jdbc/FamilyTreeDAOTest.java | 231 ++++++++++++++ 7 files changed, 1103 insertions(+) create mode 100644 examples/src/main/java/edu/pdx/cs/joy/jdbc/FamilyTreeDAO.java create mode 100644 examples/src/main/java/edu/pdx/cs/joy/jdbc/FamilyTreeDAOImpl.java create mode 100644 examples/src/main/java/edu/pdx/cs/joy/jdbc/MarriageDAO.java create mode 100644 examples/src/main/java/edu/pdx/cs/joy/jdbc/MarriageDAOImpl.java create mode 100644 examples/src/main/java/edu/pdx/cs/joy/jdbc/PersonDAO.java create mode 100644 examples/src/main/java/edu/pdx/cs/joy/jdbc/PersonDAOImpl.java create mode 100644 examples/src/test/java/edu/pdx/cs/joy/jdbc/FamilyTreeDAOTest.java diff --git a/examples/src/main/java/edu/pdx/cs/joy/jdbc/FamilyTreeDAO.java b/examples/src/main/java/edu/pdx/cs/joy/jdbc/FamilyTreeDAO.java new file mode 100644 index 000000000..51b4be725 --- /dev/null +++ b/examples/src/main/java/edu/pdx/cs/joy/jdbc/FamilyTreeDAO.java @@ -0,0 +1,51 @@ +package edu.pdx.cs.joy.jdbc; + +import edu.pdx.cs.joy.family.FamilyTree; + +import java.sql.Connection; +import java.sql.SQLException; + +/** + * Data Access Object interface for managing FamilyTree entities in the database. + */ +public interface FamilyTreeDAO { + + /** + * Drops all family tree related tables from the database if they exist. + * + * @param connection the database connection to use + * @throws SQLException if a database error occurs + */ + static void dropTables(Connection connection) throws SQLException { + FamilyTreeDAOImpl.dropTables(connection); + } + + /** + * Creates all family tree related tables in the database. + * + * @param connection the database connection to use + * @throws SQLException if a database error occurs + */ + static void createTables(Connection connection) throws SQLException { + FamilyTreeDAOImpl.createTables(connection); + } + + /** + * Saves a complete family tree to the database. + * This includes all persons and marriages in the tree. + * + * @param familyTree the family tree to save + * @throws SQLException if a database error occurs + */ + void save(FamilyTree familyTree) throws SQLException; + + /** + * Loads a complete family tree from the database. + * This includes all persons and marriages, with relationships properly resolved. + * + * @return the family tree loaded from the database + * @throws SQLException if a database error occurs + */ + FamilyTree load() throws SQLException; +} + diff --git a/examples/src/main/java/edu/pdx/cs/joy/jdbc/FamilyTreeDAOImpl.java b/examples/src/main/java/edu/pdx/cs/joy/jdbc/FamilyTreeDAOImpl.java new file mode 100644 index 000000000..a8affe868 --- /dev/null +++ b/examples/src/main/java/edu/pdx/cs/joy/jdbc/FamilyTreeDAOImpl.java @@ -0,0 +1,193 @@ +package edu.pdx.cs.joy.jdbc; + +import edu.pdx.cs.joy.family.FamilyTree; +import edu.pdx.cs.joy.family.Marriage; +import edu.pdx.cs.joy.family.Person; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Data Access Object implementation for managing FamilyTree entities in the database. + * Coordinates persistence of Person and Marriage objects. + */ +public class FamilyTreeDAOImpl implements FamilyTreeDAO { + + private final Connection connection; + private final PersonDAO personDAO; + + /** + * Creates a new FamilyTreeDAOImpl with the specified database connection. + * + * @param connection the database connection to use + */ + public FamilyTreeDAOImpl(Connection connection) { + this.connection = connection; + this.personDAO = new PersonDAOImpl(connection); + } + + /** + * Drops all family tree related tables from the database if they exist. + * Note: Must drop marriages first due to foreign key constraints. + * + * @param connection the database connection to use + * @throws SQLException if a database error occurs + */ + public static void dropTables(Connection connection) throws SQLException { + MarriageDAO.dropTable(connection); + PersonDAO.dropTable(connection); + } + + /** + * Creates all family tree related tables in the database. + * Note: Must create persons first due to foreign key constraints. + * + * @param connection the database connection to use + * @throws SQLException if a database error occurs + */ + public static void createTables(Connection connection) throws SQLException { + PersonDAO.createTable(connection); + MarriageDAO.createTable(connection); + } + + /** + * Saves a complete family tree to the database. + * This includes all persons and marriages in the tree. + * + * @param familyTree the family tree to save + * @throws SQLException if a database error occurs + */ + @Override + public void save(FamilyTree familyTree) throws SQLException { + // First, save all persons + for (Person person : familyTree.getPeople()) { + personDAO.save(person); + } + + // Build a person cache for marriage persistence + Map personCache = new HashMap<>(); + for (Person person : familyTree.getPeople()) { + personCache.put(person.getId(), person); + } + + // Then, save all marriages + MarriageDAO marriageDAO = new MarriageDAOImpl(connection, personCache); + for (Person person : familyTree.getPeople()) { + for (Marriage marriage : person.getMarriages()) { + // Only save each marriage once (from husband's perspective to avoid duplicates) + if (person.equals(marriage.getHusband())) { + marriageDAO.save(marriage); + } + } + } + } + + /** + * Loads a complete family tree from the database. + * This includes all persons and marriages, with relationships properly resolved. + * + * @return the family tree loaded from the database + * @throws SQLException if a database error occurs + */ + @Override + public FamilyTree load() throws SQLException { + FamilyTree familyTree = new FamilyTree(); + + // First pass: Load all persons and build cache + Map personCache = new HashMap<>(); + Map parentIdsMap = new HashMap<>(); + + String sql = "SELECT id, gender, first_name, middle_name, last_name, " + + "father_id, mother_id, date_of_birth, date_of_death FROM persons ORDER BY id"; + + try (java.sql.Statement statement = connection.createStatement(); + java.sql.ResultSet resultSet = statement.executeQuery(sql)) { + + while (resultSet.next()) { + int id = resultSet.getInt("id"); + String genderStr = resultSet.getString("gender"); + Person.Gender gender = Person.Gender.valueOf(genderStr); + + Person person = new Person(id, gender); + person.setFirstName(resultSet.getString("first_name")); + person.setMiddleName(resultSet.getString("middle_name")); + person.setLastName(resultSet.getString("last_name")); + + // Store parent IDs for later resolution + int fatherId = resultSet.getInt("father_id"); + boolean fatherIsNull = resultSet.wasNull(); + int motherId = resultSet.getInt("mother_id"); + boolean motherIsNull = resultSet.wasNull(); + + if (!fatherIsNull || !motherIsNull) { + parentIdsMap.put(id, new ParentIds( + fatherIsNull ? Person.UNKNOWN : fatherId, + motherIsNull ? Person.UNKNOWN : motherId + )); + } + + java.sql.Timestamp dob = resultSet.getTimestamp("date_of_birth"); + if (dob != null) { + person.setDateOfBirth(new java.util.Date(dob.getTime())); + } + + java.sql.Timestamp dod = resultSet.getTimestamp("date_of_death"); + if (dod != null) { + person.setDateOfDeath(new java.util.Date(dod.getTime())); + } + + familyTree.addPerson(person); + personCache.put(id, person); + } + } + + // Second pass: Resolve parent relationships + for (Map.Entry entry : parentIdsMap.entrySet()) { + Person person = personCache.get(entry.getKey()); + ParentIds parentIds = entry.getValue(); + + if (parentIds.fatherId != Person.UNKNOWN) { + Person father = personCache.get(parentIds.fatherId); + if (father != null) { + person.setFather(father); + } + } + + if (parentIds.motherId != Person.UNKNOWN) { + Person mother = personCache.get(parentIds.motherId); + if (mother != null) { + person.setMother(mother); + } + } + } + + // Third pass: Load all marriages + MarriageDAO marriageDAO = new MarriageDAOImpl(connection, personCache); + List marriages = marriageDAO.findAll(); + + for (Marriage marriage : marriages) { + // Add marriage to both spouses + marriage.getHusband().addMarriage(marriage); + marriage.getWife().addMarriage(marriage); + } + + return familyTree; + } + + /** + * Helper class to store parent IDs during loading. + */ + private static class ParentIds { + final int fatherId; + final int motherId; + + ParentIds(int fatherId, int motherId) { + this.fatherId = fatherId; + this.motherId = motherId; + } + } +} + diff --git a/examples/src/main/java/edu/pdx/cs/joy/jdbc/MarriageDAO.java b/examples/src/main/java/edu/pdx/cs/joy/jdbc/MarriageDAO.java new file mode 100644 index 000000000..d86dae1f9 --- /dev/null +++ b/examples/src/main/java/edu/pdx/cs/joy/jdbc/MarriageDAO.java @@ -0,0 +1,68 @@ +package edu.pdx.cs.joy.jdbc; + +import edu.pdx.cs.joy.family.Marriage; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.List; + +/** + * Data Access Object interface for managing Marriage entities in the database. + */ +public interface MarriageDAO { + + /** + * Drops the marriages table from the database if it exists. + * + * @param connection the database connection to use + * @throws SQLException if a database error occurs + */ + static void dropTable(Connection connection) throws SQLException { + MarriageDAOImpl.dropTable(connection); + } + + /** + * Creates the marriages table in the database. + * + * @param connection the database connection to use + * @throws SQLException if a database error occurs + */ + static void createTable(Connection connection) throws SQLException { + MarriageDAOImpl.createTable(connection); + } + + /** + * Saves a marriage to the database. + * + * @param marriage the marriage to save + * @throws SQLException if a database error occurs + */ + void save(Marriage marriage) throws SQLException; + + /** + * Finds all marriages for a specific person ID. + * + * @param personId the person ID + * @return a list of marriages involving the person + * @throws SQLException if a database error occurs + */ + List findByPersonId(int personId) throws SQLException; + + /** + * Finds all marriages in the database. + * + * @return a list of all marriages + * @throws SQLException if a database error occurs + */ + List findAll() throws SQLException; + + /** + * Deletes a marriage from the database. + * + * @param husbandId the husband's ID + * @param wifeId the wife's ID + * @throws SQLException if a database error occurs + */ + void delete(int husbandId, int wifeId) throws SQLException; +} + diff --git a/examples/src/main/java/edu/pdx/cs/joy/jdbc/MarriageDAOImpl.java b/examples/src/main/java/edu/pdx/cs/joy/jdbc/MarriageDAOImpl.java new file mode 100644 index 000000000..492d0f717 --- /dev/null +++ b/examples/src/main/java/edu/pdx/cs/joy/jdbc/MarriageDAOImpl.java @@ -0,0 +1,193 @@ +package edu.pdx.cs.joy.jdbc; + +import edu.pdx.cs.joy.family.Marriage; +import edu.pdx.cs.joy.family.Person; + +import java.sql.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * Data Access Object implementation for managing Marriage entities in the database. + */ +public class MarriageDAOImpl implements MarriageDAO { + + private final Connection connection; + private final Map personCache; + + /** + * Creates a new MarriageDAOImpl with the specified database connection. + * + * @param connection the database connection to use + * @param personCache a cache of persons to resolve marriage partners + */ + public MarriageDAOImpl(Connection connection, Map personCache) { + this.connection = connection; + this.personCache = personCache; + } + + /** + * Drops the marriages table from the database if it exists. + * + * @param connection the database connection to use + * @throws SQLException if a database error occurs + */ + public static void dropTable(Connection connection) throws SQLException { + try (Statement statement = connection.createStatement()) { + statement.execute("DROP TABLE IF EXISTS marriages"); + } + } + + /** + * Creates the marriages table in the database. + * + * @param connection the database connection to use + * @throws SQLException if a database error occurs + */ + public static void createTable(Connection connection) throws SQLException { + try (Statement statement = connection.createStatement()) { + statement.execute( + "CREATE TABLE IF NOT EXISTS marriages (" + + " husband_id INTEGER NOT NULL," + + " wife_id INTEGER NOT NULL," + + " marriage_date TIMESTAMP," + + " location VARCHAR(255)," + + " PRIMARY KEY (husband_id, wife_id)," + + " FOREIGN KEY (husband_id) REFERENCES persons(id)," + + " FOREIGN KEY (wife_id) REFERENCES persons(id)" + + ")" + ); + } + } + + /** + * Saves a marriage to the database. + * + * @param marriage the marriage to save + * @throws SQLException if a database error occurs + */ + @Override + public void save(Marriage marriage) throws SQLException { + String sql = "INSERT INTO marriages (husband_id, wife_id, marriage_date, location) " + + "VALUES (?, ?, ?, ?)"; + + try (PreparedStatement statement = connection.prepareStatement(sql)) { + statement.setInt(1, marriage.getHusband().getId()); + statement.setInt(2, marriage.getWife().getId()); + + if (marriage.getDate() == null) { + statement.setNull(3, Types.TIMESTAMP); + } else { + statement.setTimestamp(3, new Timestamp(marriage.getDate().getTime())); + } + + statement.setString(4, marriage.getLocation()); + + statement.executeUpdate(); + } + } + + /** + * Finds all marriages for a specific person ID. + * + * @param personId the person ID + * @return a list of marriages involving the person + * @throws SQLException if a database error occurs + */ + @Override + public List findByPersonId(int personId) throws SQLException { + List marriages = new ArrayList<>(); + String sql = "SELECT husband_id, wife_id, marriage_date, location " + + "FROM marriages WHERE husband_id = ? OR wife_id = ?"; + + try (PreparedStatement statement = connection.prepareStatement(sql)) { + statement.setInt(1, personId); + statement.setInt(2, personId); + + try (ResultSet resultSet = statement.executeQuery()) { + while (resultSet.next()) { + marriages.add(extractMarriageFromResultSet(resultSet)); + } + } + } + + return marriages; + } + + /** + * Finds all marriages in the database. + * + * @return a list of all marriages + * @throws SQLException if a database error occurs + */ + @Override + public List findAll() throws SQLException { + List marriages = new ArrayList<>(); + String sql = "SELECT husband_id, wife_id, marriage_date, location FROM marriages"; + + try (Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery(sql)) { + while (resultSet.next()) { + marriages.add(extractMarriageFromResultSet(resultSet)); + } + } + + return marriages; + } + + /** + * Deletes a marriage from the database. + * + * @param husbandId the husband's ID + * @param wifeId the wife's ID + * @throws SQLException if a database error occurs + */ + @Override + public void delete(int husbandId, int wifeId) throws SQLException { + String sql = "DELETE FROM marriages WHERE husband_id = ? AND wife_id = ?"; + + try (PreparedStatement statement = connection.prepareStatement(sql)) { + statement.setInt(1, husbandId); + statement.setInt(2, wifeId); + int rowsAffected = statement.executeUpdate(); + + if (rowsAffected == 0) { + throw new SQLException("Delete failed, no marriage found for husband ID: " + + husbandId + " and wife ID: " + wifeId); + } + } + } + + /** + * Extracts a Marriage object from the current row of a ResultSet. + * + * @param resultSet the result set positioned at a marriage row + * @return a Marriage object with data from the result set + * @throws SQLException if a database error occurs + */ + private Marriage extractMarriageFromResultSet(ResultSet resultSet) throws SQLException { + int husbandId = resultSet.getInt("husband_id"); + int wifeId = resultSet.getInt("wife_id"); + + Person husband = personCache.get(husbandId); + Person wife = personCache.get(wifeId); + + if (husband == null || wife == null) { + throw new SQLException("Cannot create marriage: Person not found in cache. " + + "Husband ID: " + husbandId + ", Wife ID: " + wifeId); + } + + Marriage marriage = new Marriage(husband, wife); + + Timestamp marriageDate = resultSet.getTimestamp("marriage_date"); + if (marriageDate != null) { + marriage.setDate(new java.util.Date(marriageDate.getTime())); + } + + marriage.setLocation(resultSet.getString("location")); + + return marriage; + } +} + diff --git a/examples/src/main/java/edu/pdx/cs/joy/jdbc/PersonDAO.java b/examples/src/main/java/edu/pdx/cs/joy/jdbc/PersonDAO.java new file mode 100644 index 000000000..ef8e79724 --- /dev/null +++ b/examples/src/main/java/edu/pdx/cs/joy/jdbc/PersonDAO.java @@ -0,0 +1,75 @@ +package edu.pdx.cs.joy.jdbc; + +import edu.pdx.cs.joy.family.Person; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.List; + +/** + * Data Access Object interface for managing Person entities in the database. + */ +public interface PersonDAO { + + /** + * Drops the persons table from the database if it exists. + * + * @param connection the database connection to use + * @throws SQLException if a database error occurs + */ + static void dropTable(Connection connection) throws SQLException { + PersonDAOImpl.dropTable(connection); + } + + /** + * Creates the persons table in the database. + * + * @param connection the database connection to use + * @throws SQLException if a database error occurs + */ + static void createTable(Connection connection) throws SQLException { + PersonDAOImpl.createTable(connection); + } + + /** + * Saves a person to the database. + * + * @param person the person to save + * @throws SQLException if a database error occurs + */ + void save(Person person) throws SQLException; + + /** + * Finds a person by their ID. + * + * @param id the ID to search for + * @return the person with the given ID, or null if not found + * @throws SQLException if a database error occurs + */ + Person findById(int id) throws SQLException; + + /** + * Finds all persons in the database. + * + * @return a list of all persons + * @throws SQLException if a database error occurs + */ + List findAll() throws SQLException; + + /** + * Updates an existing person in the database. + * + * @param person the person to update + * @throws SQLException if a database error occurs + */ + void update(Person person) throws SQLException; + + /** + * Deletes a person from the database by ID. + * + * @param id the ID of the person to delete + * @throws SQLException if a database error occurs + */ + void delete(int id) throws SQLException; +} + diff --git a/examples/src/main/java/edu/pdx/cs/joy/jdbc/PersonDAOImpl.java b/examples/src/main/java/edu/pdx/cs/joy/jdbc/PersonDAOImpl.java new file mode 100644 index 000000000..1238259ae --- /dev/null +++ b/examples/src/main/java/edu/pdx/cs/joy/jdbc/PersonDAOImpl.java @@ -0,0 +1,292 @@ +package edu.pdx.cs.joy.jdbc; + +import edu.pdx.cs.joy.family.Person; + +import java.sql.*; +import java.util.ArrayList; +import java.util.List; + +/** + * Data Access Object implementation for managing Person entities in the database. + */ +public class PersonDAOImpl implements PersonDAO { + + private final Connection connection; + + /** + * Creates a new PersonDAOImpl with the specified database connection. + * + * @param connection the database connection to use + */ + public PersonDAOImpl(Connection connection) { + this.connection = connection; + } + + /** + * Drops the persons table from the database if it exists. + * + * @param connection the database connection to use + * @throws SQLException if a database error occurs + */ + public static void dropTable(Connection connection) throws SQLException { + try (Statement statement = connection.createStatement()) { + statement.execute("DROP TABLE IF EXISTS persons"); + } + } + + /** + * Creates the persons table in the database. + * + * @param connection the database connection to use + * @throws SQLException if a database error occurs + */ + public static void createTable(Connection connection) throws SQLException { + try (Statement statement = connection.createStatement()) { + statement.execute( + "CREATE TABLE IF NOT EXISTS persons (" + + " id INTEGER PRIMARY KEY," + + " gender VARCHAR(10) NOT NULL," + + " first_name VARCHAR(255)," + + " middle_name VARCHAR(255)," + + " last_name VARCHAR(255)," + + " father_id INTEGER," + + " mother_id INTEGER," + + " date_of_birth TIMESTAMP," + + " date_of_death TIMESTAMP," + + " FOREIGN KEY (father_id) REFERENCES persons(id)," + + " FOREIGN KEY (mother_id) REFERENCES persons(id)" + + ")" + ); + } + } + + /** + * Saves a person to the database. + * + * @param person the person to save + * @throws SQLException if a database error occurs + */ + @Override + public void save(Person person) throws SQLException { + String sql = "INSERT INTO persons (id, gender, first_name, middle_name, last_name, " + + "father_id, mother_id, date_of_birth, date_of_death) " + + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"; + + try (PreparedStatement statement = connection.prepareStatement(sql)) { + statement.setInt(1, person.getId()); + statement.setString(2, person.getGender().name()); + statement.setString(3, person.getFirstName()); + statement.setString(4, person.getMiddleName()); + statement.setString(5, person.getLastName()); + + if (person.getFatherId() == Person.UNKNOWN) { + statement.setNull(6, Types.INTEGER); + } else { + statement.setInt(6, person.getFatherId()); + } + + if (person.getMotherId() == Person.UNKNOWN) { + statement.setNull(7, Types.INTEGER); + } else { + statement.setInt(7, person.getMotherId()); + } + + if (person.getDateOfBirth() == null) { + statement.setNull(8, Types.TIMESTAMP); + } else { + statement.setTimestamp(8, new Timestamp(person.getDateOfBirth().getTime())); + } + + if (person.getDateOfDeath() == null) { + statement.setNull(9, Types.TIMESTAMP); + } else { + statement.setTimestamp(9, new Timestamp(person.getDateOfDeath().getTime())); + } + + statement.executeUpdate(); + } + } + + /** + * Finds a person by their ID. + * + * @param id the ID to search for + * @return the person with the given ID, or null if not found + * @throws SQLException if a database error occurs + */ + @Override + public Person findById(int id) throws SQLException { + String sql = "SELECT id, gender, first_name, middle_name, last_name, " + + "father_id, mother_id, date_of_birth, date_of_death " + + "FROM persons WHERE id = ?"; + + try (PreparedStatement statement = connection.prepareStatement(sql)) { + statement.setInt(1, id); + + try (ResultSet resultSet = statement.executeQuery()) { + if (resultSet.next()) { + return extractPersonFromResultSet(resultSet); + } + } + } + + return null; + } + + /** + * Finds all persons in the database. + * + * @return a list of all persons + * @throws SQLException if a database error occurs + */ + @Override + public List findAll() throws SQLException { + List persons = new ArrayList<>(); + String sql = "SELECT id, gender, first_name, middle_name, last_name, " + + "father_id, mother_id, date_of_birth, date_of_death " + + "FROM persons ORDER BY id"; + + try (Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery(sql)) { + while (resultSet.next()) { + persons.add(extractPersonFromResultSet(resultSet)); + } + } + + return persons; + } + + /** + * Updates an existing person in the database. + * + * @param person the person to update + * @throws SQLException if a database error occurs + */ + @Override + public void update(Person person) throws SQLException { + String sql = "UPDATE persons SET gender = ?, first_name = ?, middle_name = ?, " + + "last_name = ?, father_id = ?, mother_id = ?, " + + "date_of_birth = ?, date_of_death = ? WHERE id = ?"; + + try (PreparedStatement statement = connection.prepareStatement(sql)) { + statement.setString(1, person.getGender().name()); + statement.setString(2, person.getFirstName()); + statement.setString(3, person.getMiddleName()); + statement.setString(4, person.getLastName()); + + if (person.getFatherId() == Person.UNKNOWN) { + statement.setNull(5, Types.INTEGER); + } else { + statement.setInt(5, person.getFatherId()); + } + + if (person.getMotherId() == Person.UNKNOWN) { + statement.setNull(6, Types.INTEGER); + } else { + statement.setInt(6, person.getMotherId()); + } + + if (person.getDateOfBirth() == null) { + statement.setNull(7, Types.TIMESTAMP); + } else { + statement.setTimestamp(7, new Timestamp(person.getDateOfBirth().getTime())); + } + + if (person.getDateOfDeath() == null) { + statement.setNull(8, Types.TIMESTAMP); + } else { + statement.setTimestamp(8, new Timestamp(person.getDateOfDeath().getTime())); + } + + statement.setInt(9, person.getId()); + + int rowsAffected = statement.executeUpdate(); + if (rowsAffected == 0) { + throw new SQLException("Update failed, no person found with ID: " + person.getId()); + } + } + } + + /** + * Deletes a person from the database by ID. + * + * @param id the ID of the person to delete + * @throws SQLException if a database error occurs + */ + @Override + public void delete(int id) throws SQLException { + String sql = "DELETE FROM persons WHERE id = ?"; + + try (PreparedStatement statement = connection.prepareStatement(sql)) { + statement.setInt(1, id); + int rowsAffected = statement.executeUpdate(); + + if (rowsAffected == 0) { + throw new SQLException("Delete failed, no person found with ID: " + id); + } + } + } + + /** + * Extracts a Person object from the current row of a ResultSet. + * Note: This creates a Person without resolving parent references. + * Parent IDs are stored in the database but parent Person objects + * must be resolved by the caller. + * + * @param resultSet the result set positioned at a person row + * @return a Person object with data from the result set + * @throws SQLException if a database error occurs + */ + private Person extractPersonFromResultSet(ResultSet resultSet) throws SQLException { + int id = resultSet.getInt("id"); + String genderStr = resultSet.getString("gender"); + Person.Gender gender = Person.Gender.valueOf(genderStr); + + Person person = new Person(id, gender); + person.setFirstName(resultSet.getString("first_name")); + person.setMiddleName(resultSet.getString("middle_name")); + person.setLastName(resultSet.getString("last_name")); + + // Note: Parent relationships will be resolved by FamilyTreeDAO + // We cannot call package-protected setFatherId/setMotherId from here + + Timestamp dob = resultSet.getTimestamp("date_of_birth"); + if (dob != null) { + person.setDateOfBirth(new java.util.Date(dob.getTime())); + } + + Timestamp dod = resultSet.getTimestamp("date_of_death"); + if (dod != null) { + person.setDateOfDeath(new java.util.Date(dod.getTime())); + } + + return person; + } + + /** + * Helper method to get father ID from result set. + * Package-protected to allow FamilyTreeDAO to resolve relationships. + * + * @param resultSet the result set + * @return the father ID or Person.UNKNOWN if null + * @throws SQLException if a database error occurs + */ + int getFatherIdFromResultSet(ResultSet resultSet) throws SQLException { + int fatherId = resultSet.getInt("father_id"); + return resultSet.wasNull() ? Person.UNKNOWN : fatherId; + } + + /** + * Helper method to get mother ID from result set. + * Package-protected to allow FamilyTreeDAO to resolve relationships. + * + * @param resultSet the result set + * @return the mother ID or Person.UNKNOWN if null + * @throws SQLException if a database error occurs + */ + int getMotherIdFromResultSet(ResultSet resultSet) throws SQLException { + int motherId = resultSet.getInt("mother_id"); + return resultSet.wasNull() ? Person.UNKNOWN : motherId; + } +} + diff --git a/examples/src/test/java/edu/pdx/cs/joy/jdbc/FamilyTreeDAOTest.java b/examples/src/test/java/edu/pdx/cs/joy/jdbc/FamilyTreeDAOTest.java new file mode 100644 index 000000000..0cb6a6999 --- /dev/null +++ b/examples/src/test/java/edu/pdx/cs/joy/jdbc/FamilyTreeDAOTest.java @@ -0,0 +1,231 @@ +package edu.pdx.cs.joy.jdbc; + +import edu.pdx.cs.joy.family.FamilyTree; +import edu.pdx.cs.joy.family.Marriage; +import edu.pdx.cs.joy.family.Person; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.Calendar; +import java.util.Date; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; + +/** + * Unit test for FamilyTree DAO classes. + * Tests persistence of Person, Marriage, and FamilyTree objects to an H2 database. + */ +public class FamilyTreeDAOTest { + + private Connection connection; + private FamilyTreeDAO familyTreeDAO; + + @BeforeEach + public void setUp() throws SQLException { + // Create an in-memory H2 database + connection = H2DatabaseHelper.createInMemoryConnection("familyTreeTest"); + + // Drop and create tables + FamilyTreeDAO.dropTables(connection); + FamilyTreeDAO.createTables(connection); + + // Initialize the DAO + familyTreeDAO = new FamilyTreeDAOImpl(connection); + } + + @AfterEach + public void tearDown() throws SQLException { + if (connection != null && !connection.isClosed()) { + FamilyTreeDAO.dropTables(connection); + connection.close(); + } + } + + @Test + public void testPersistAndLoadSimpleFamilyTree() throws SQLException { + // Create a simple family tree + FamilyTree familyTree = new FamilyTree(); + + Person father = new Person(1, Person.MALE); + father.setFirstName("John"); + father.setLastName("Doe"); + father.setDateOfBirth(createDate(1970, 1, 15)); + + Person mother = new Person(2, Person.FEMALE); + mother.setFirstName("Jane"); + mother.setLastName("Smith"); + mother.setDateOfBirth(createDate(1972, 3, 20)); + + Person child = new Person(3, Person.MALE); + child.setFirstName("Jack"); + child.setLastName("Doe"); + child.setDateOfBirth(createDate(2000, 6, 10)); + child.setFather(father); + child.setMother(mother); + + familyTree.addPerson(father); + familyTree.addPerson(mother); + familyTree.addPerson(child); + + // Add marriage + Marriage marriage = new Marriage(father, mother); + marriage.setDate(createDate(1998, 5, 15)); + marriage.setLocation("Portland, OR"); + father.addMarriage(marriage); + mother.addMarriage(marriage); + + // Save to database + familyTreeDAO.save(familyTree); + + // Load from database + FamilyTree loadedTree = familyTreeDAO.load(); + + // Validate + assertThat(loadedTree.getPeople(), hasSize(3)); + assertThat(loadedTree.containsPerson(1), is(true)); + assertThat(loadedTree.containsPerson(2), is(true)); + assertThat(loadedTree.containsPerson(3), is(true)); + + // Validate father + Person loadedFather = loadedTree.getPerson(1); + assertThat(loadedFather.getFirstName(), is(equalTo("John"))); + assertThat(loadedFather.getLastName(), is(equalTo("Doe"))); + assertThat(loadedFather.getGender(), is(equalTo(Person.MALE))); + assertThat(loadedFather.getMarriages(), hasSize(1)); + + // Validate mother + Person loadedMother = loadedTree.getPerson(2); + assertThat(loadedMother.getFirstName(), is(equalTo("Jane"))); + assertThat(loadedMother.getLastName(), is(equalTo("Smith"))); + assertThat(loadedMother.getGender(), is(equalTo(Person.FEMALE))); + + // Validate child + Person loadedChild = loadedTree.getPerson(3); + assertThat(loadedChild.getFirstName(), is(equalTo("Jack"))); + assertThat(loadedChild.getFather(), is(notNullValue())); + assertThat(loadedChild.getFather().getId(), is(equalTo(1))); + assertThat(loadedChild.getMother(), is(notNullValue())); + assertThat(loadedChild.getMother().getId(), is(equalTo(2))); + + // Validate marriage + Marriage loadedMarriage = loadedFather.getMarriages().iterator().next(); + assertThat(loadedMarriage.getHusband().getId(), is(equalTo(1))); + assertThat(loadedMarriage.getWife().getId(), is(equalTo(2))); + assertThat(loadedMarriage.getLocation(), is(equalTo("Portland, OR"))); + } + + @Test + public void testPersistMultipleGenerations() throws SQLException { + FamilyTree familyTree = new FamilyTree(); + + // Grandparents + Person grandfather = new Person(1, Person.MALE); + grandfather.setFirstName("William"); + grandfather.setLastName("Doe"); + + Person grandmother = new Person(2, Person.FEMALE); + grandmother.setFirstName("Mary"); + grandmother.setLastName("Johnson"); + + // Parents + Person father = new Person(3, Person.MALE); + father.setFirstName("John"); + father.setLastName("Doe"); + father.setFather(grandfather); + father.setMother(grandmother); + + Person mother = new Person(4, Person.FEMALE); + mother.setFirstName("Jane"); + mother.setLastName("Smith"); + + // Child + Person child = new Person(5, Person.FEMALE); + child.setFirstName("Emily"); + child.setLastName("Doe"); + child.setFather(father); + child.setMother(mother); + + familyTree.addPerson(grandfather); + familyTree.addPerson(grandmother); + familyTree.addPerson(father); + familyTree.addPerson(mother); + familyTree.addPerson(child); + + // Save and load + familyTreeDAO.save(familyTree); + FamilyTree loadedTree = familyTreeDAO.load(); + + // Validate multi-generation relationships + assertThat(loadedTree.getPeople(), hasSize(5)); + + Person loadedChild = loadedTree.getPerson(5); + assertThat(loadedChild.getFather(), is(notNullValue())); + assertThat(loadedChild.getFather().getId(), is(equalTo(3))); + + Person loadedFather = loadedChild.getFather(); + assertThat(loadedFather.getFather(), is(notNullValue())); + assertThat(loadedFather.getFather().getId(), is(equalTo(1))); + assertThat(loadedFather.getMother(), is(notNullValue())); + assertThat(loadedFather.getMother().getId(), is(equalTo(2))); + } + + @Test + public void testPersistPersonWithDates() throws SQLException { + FamilyTree familyTree = new FamilyTree(); + + Person person = new Person(1, Person.MALE); + person.setFirstName("George"); + person.setLastName("Washington"); + person.setDateOfBirth(createDate(1732, 2, 22)); + person.setDateOfDeath(createDate(1799, 12, 14)); + + familyTree.addPerson(person); + + familyTreeDAO.save(familyTree); + FamilyTree loadedTree = familyTreeDAO.load(); + + Person loadedPerson = loadedTree.getPerson(1); + assertThat(loadedPerson.getDateOfBirth(), is(notNullValue())); + assertThat(loadedPerson.getDateOfDeath(), is(notNullValue())); + } + + @Test + public void testLoadEmptyFamilyTree() throws SQLException { + FamilyTree loadedTree = familyTreeDAO.load(); + assertThat(loadedTree.getPeople(), is(empty())); + } + + @Test + public void testPersistPersonWithoutParents() throws SQLException { + FamilyTree familyTree = new FamilyTree(); + + Person person = new Person(1, Person.FEMALE); + person.setFirstName("Alice"); + person.setLastName("Unknown"); + + familyTree.addPerson(person); + + familyTreeDAO.save(familyTree); + FamilyTree loadedTree = familyTreeDAO.load(); + + Person loadedPerson = loadedTree.getPerson(1); + assertThat(loadedPerson, is(notNullValue())); + assertThat(loadedPerson.getFather(), is(nullValue())); + assertThat(loadedPerson.getMother(), is(nullValue())); + } + + /** + * Helper method to create a Date object. + */ + private Date createDate(int year, int month, int day) { + Calendar cal = Calendar.getInstance(); + cal.set(year, month - 1, day, 0, 0, 0); + cal.set(Calendar.MILLISECOND, 0); + return cal.getTime(); + } +} + From ec12cbd439e97822d245e6aa4abe1ae28522eae4 Mon Sep 17 00:00:00 2001 From: David Whitlock Date: Mon, 29 Dec 2025 19:32:38 -0800 Subject: [PATCH 23/36] Move the FamilyTree DAO code to the family tree project. --- examples/pom.xml | 5 ----- family/pom.xml | 10 ++++++++++ .../java/edu/pdx/cs/joy/family}/FamilyTreeDAO.java | 4 +--- .../java/edu/pdx/cs/joy/family}/FamilyTreeDAOImpl.java | 6 +----- .../main/java/edu/pdx/cs/joy/family}/MarriageDAO.java | 4 +--- .../java/edu/pdx/cs/joy/family}/MarriageDAOImpl.java | 5 +---- .../main/java/edu/pdx/cs/joy/family}/PersonDAO.java | 4 +--- .../java/edu/pdx/cs/joy/family}/PersonDAOImpl.java | 4 +--- .../java/edu/pdx/cs/joy/family}/FamilyTreeDAOTest.java | 6 ++---- 9 files changed, 18 insertions(+), 30 deletions(-) rename {examples/src/main/java/edu/pdx/cs/joy/jdbc => family/src/main/java/edu/pdx/cs/joy/family}/FamilyTreeDAO.java (95%) rename {examples/src/main/java/edu/pdx/cs/joy/jdbc => family/src/main/java/edu/pdx/cs/joy/family}/FamilyTreeDAOImpl.java (97%) rename {examples/src/main/java/edu/pdx/cs/joy/jdbc => family/src/main/java/edu/pdx/cs/joy/family}/MarriageDAO.java (96%) rename {examples/src/main/java/edu/pdx/cs/joy/jdbc => family/src/main/java/edu/pdx/cs/joy/family}/MarriageDAOImpl.java (98%) rename {examples/src/main/java/edu/pdx/cs/joy/jdbc => family/src/main/java/edu/pdx/cs/joy/family}/PersonDAO.java (96%) rename {examples/src/main/java/edu/pdx/cs/joy/jdbc => family/src/main/java/edu/pdx/cs/joy/family}/PersonDAOImpl.java (99%) rename {examples/src/test/java/edu/pdx/cs/joy/jdbc => family/src/test/java/edu/pdx/cs/joy/family}/FamilyTreeDAOTest.java (98%) diff --git a/examples/pom.xml b/examples/pom.xml index eb8dbffce..cf9ed6f6d 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -11,11 +11,6 @@ 1.3.5-SNAPSHOT https://www.cs.pdx.edu/~whitlock - - io.github.davidwhitlock.joy - family - 1.1.6-SNAPSHOT - io.github.davidwhitlock.joy projects diff --git a/family/pom.xml b/family/pom.xml index 6d99c0f68..8d156a047 100644 --- a/family/pom.xml +++ b/family/pom.xml @@ -18,6 +18,16 @@ projects 3.0.4-SNAPSHOT + + io.github.davidwhitlock.joy + examples + 1.3.5-SNAPSHOT + + + com.h2database + h2 + 2.2.224 + diff --git a/examples/src/main/java/edu/pdx/cs/joy/jdbc/FamilyTreeDAO.java b/family/src/main/java/edu/pdx/cs/joy/family/FamilyTreeDAO.java similarity index 95% rename from examples/src/main/java/edu/pdx/cs/joy/jdbc/FamilyTreeDAO.java rename to family/src/main/java/edu/pdx/cs/joy/family/FamilyTreeDAO.java index 51b4be725..0d55f9b82 100644 --- a/examples/src/main/java/edu/pdx/cs/joy/jdbc/FamilyTreeDAO.java +++ b/family/src/main/java/edu/pdx/cs/joy/family/FamilyTreeDAO.java @@ -1,6 +1,4 @@ -package edu.pdx.cs.joy.jdbc; - -import edu.pdx.cs.joy.family.FamilyTree; +package edu.pdx.cs.joy.family; import java.sql.Connection; import java.sql.SQLException; diff --git a/examples/src/main/java/edu/pdx/cs/joy/jdbc/FamilyTreeDAOImpl.java b/family/src/main/java/edu/pdx/cs/joy/family/FamilyTreeDAOImpl.java similarity index 97% rename from examples/src/main/java/edu/pdx/cs/joy/jdbc/FamilyTreeDAOImpl.java rename to family/src/main/java/edu/pdx/cs/joy/family/FamilyTreeDAOImpl.java index a8affe868..35de309ee 100644 --- a/examples/src/main/java/edu/pdx/cs/joy/jdbc/FamilyTreeDAOImpl.java +++ b/family/src/main/java/edu/pdx/cs/joy/family/FamilyTreeDAOImpl.java @@ -1,8 +1,4 @@ -package edu.pdx.cs.joy.jdbc; - -import edu.pdx.cs.joy.family.FamilyTree; -import edu.pdx.cs.joy.family.Marriage; -import edu.pdx.cs.joy.family.Person; +package edu.pdx.cs.joy.family; import java.sql.Connection; import java.sql.SQLException; diff --git a/examples/src/main/java/edu/pdx/cs/joy/jdbc/MarriageDAO.java b/family/src/main/java/edu/pdx/cs/joy/family/MarriageDAO.java similarity index 96% rename from examples/src/main/java/edu/pdx/cs/joy/jdbc/MarriageDAO.java rename to family/src/main/java/edu/pdx/cs/joy/family/MarriageDAO.java index d86dae1f9..4e6da9816 100644 --- a/examples/src/main/java/edu/pdx/cs/joy/jdbc/MarriageDAO.java +++ b/family/src/main/java/edu/pdx/cs/joy/family/MarriageDAO.java @@ -1,6 +1,4 @@ -package edu.pdx.cs.joy.jdbc; - -import edu.pdx.cs.joy.family.Marriage; +package edu.pdx.cs.joy.family; import java.sql.Connection; import java.sql.SQLException; diff --git a/examples/src/main/java/edu/pdx/cs/joy/jdbc/MarriageDAOImpl.java b/family/src/main/java/edu/pdx/cs/joy/family/MarriageDAOImpl.java similarity index 98% rename from examples/src/main/java/edu/pdx/cs/joy/jdbc/MarriageDAOImpl.java rename to family/src/main/java/edu/pdx/cs/joy/family/MarriageDAOImpl.java index 492d0f717..cd6c9b3f4 100644 --- a/examples/src/main/java/edu/pdx/cs/joy/jdbc/MarriageDAOImpl.java +++ b/family/src/main/java/edu/pdx/cs/joy/family/MarriageDAOImpl.java @@ -1,7 +1,4 @@ -package edu.pdx.cs.joy.jdbc; - -import edu.pdx.cs.joy.family.Marriage; -import edu.pdx.cs.joy.family.Person; +package edu.pdx.cs.joy.family; import java.sql.*; import java.util.ArrayList; diff --git a/examples/src/main/java/edu/pdx/cs/joy/jdbc/PersonDAO.java b/family/src/main/java/edu/pdx/cs/joy/family/PersonDAO.java similarity index 96% rename from examples/src/main/java/edu/pdx/cs/joy/jdbc/PersonDAO.java rename to family/src/main/java/edu/pdx/cs/joy/family/PersonDAO.java index ef8e79724..53fb8e536 100644 --- a/examples/src/main/java/edu/pdx/cs/joy/jdbc/PersonDAO.java +++ b/family/src/main/java/edu/pdx/cs/joy/family/PersonDAO.java @@ -1,6 +1,4 @@ -package edu.pdx.cs.joy.jdbc; - -import edu.pdx.cs.joy.family.Person; +package edu.pdx.cs.joy.family; import java.sql.Connection; import java.sql.SQLException; diff --git a/examples/src/main/java/edu/pdx/cs/joy/jdbc/PersonDAOImpl.java b/family/src/main/java/edu/pdx/cs/joy/family/PersonDAOImpl.java similarity index 99% rename from examples/src/main/java/edu/pdx/cs/joy/jdbc/PersonDAOImpl.java rename to family/src/main/java/edu/pdx/cs/joy/family/PersonDAOImpl.java index 1238259ae..58e3dcb77 100644 --- a/examples/src/main/java/edu/pdx/cs/joy/jdbc/PersonDAOImpl.java +++ b/family/src/main/java/edu/pdx/cs/joy/family/PersonDAOImpl.java @@ -1,6 +1,4 @@ -package edu.pdx.cs.joy.jdbc; - -import edu.pdx.cs.joy.family.Person; +package edu.pdx.cs.joy.family; import java.sql.*; import java.util.ArrayList; diff --git a/examples/src/test/java/edu/pdx/cs/joy/jdbc/FamilyTreeDAOTest.java b/family/src/test/java/edu/pdx/cs/joy/family/FamilyTreeDAOTest.java similarity index 98% rename from examples/src/test/java/edu/pdx/cs/joy/jdbc/FamilyTreeDAOTest.java rename to family/src/test/java/edu/pdx/cs/joy/family/FamilyTreeDAOTest.java index 0cb6a6999..94218e6ff 100644 --- a/examples/src/test/java/edu/pdx/cs/joy/jdbc/FamilyTreeDAOTest.java +++ b/family/src/test/java/edu/pdx/cs/joy/family/FamilyTreeDAOTest.java @@ -1,8 +1,6 @@ -package edu.pdx.cs.joy.jdbc; +package edu.pdx.cs.joy.family; -import edu.pdx.cs.joy.family.FamilyTree; -import edu.pdx.cs.joy.family.Marriage; -import edu.pdx.cs.joy.family.Person; +import edu.pdx.cs.joy.jdbc.H2DatabaseHelper; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; From 44d9c8eac31233d8bc63cc06e796c78ec80841a7 Mon Sep 17 00:00:00 2001 From: David Whitlock Date: Mon, 29 Dec 2025 19:42:50 -0800 Subject: [PATCH 24/36] Move the Family Tree XML example to the resources directory. --- .../java => test/resources}/edu/pdx/cs/joy/family/davesFamily.xml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename family/src/{main/java => test/resources}/edu/pdx/cs/joy/family/davesFamily.xml (100%) diff --git a/family/src/main/java/edu/pdx/cs/joy/family/davesFamily.xml b/family/src/test/resources/edu/pdx/cs/joy/family/davesFamily.xml similarity index 100% rename from family/src/main/java/edu/pdx/cs/joy/family/davesFamily.xml rename to family/src/test/resources/edu/pdx/cs/joy/family/davesFamily.xml From 9000adbbc8bb237cadd1e9a1b4b9e8c8df098917 Mon Sep 17 00:00:00 2001 From: David Whitlock Date: Mon, 29 Dec 2025 20:21:59 -0800 Subject: [PATCH 25/36] Added an ExecuteH2DatabaseStatement command line program that executes an SQL statement against an H2 database read from a file on disk. --- .../jdbc/ExecuteH2DatabaseStatementIT.java | 303 ++++++++++++++++++ .../joy/jdbc/ExecuteH2DatabaseStatement.java | 168 ++++++++++ 2 files changed, 471 insertions(+) create mode 100644 examples/src/it/java/edu/pdx/cs/joy/jdbc/ExecuteH2DatabaseStatementIT.java create mode 100644 examples/src/main/java/edu/pdx/cs/joy/jdbc/ExecuteH2DatabaseStatement.java diff --git a/examples/src/it/java/edu/pdx/cs/joy/jdbc/ExecuteH2DatabaseStatementIT.java b/examples/src/it/java/edu/pdx/cs/joy/jdbc/ExecuteH2DatabaseStatementIT.java new file mode 100644 index 000000000..3a04a3acb --- /dev/null +++ b/examples/src/it/java/edu/pdx/cs/joy/jdbc/ExecuteH2DatabaseStatementIT.java @@ -0,0 +1,303 @@ +package edu.pdx.cs.joy.jdbc; + +import edu.pdx.cs.joy.InvokeMainTestCase; +import org.junit.jupiter.api.*; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.sql.Connection; +import java.sql.SQLException; +import java.time.LocalDate; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; + +/** + * Integration test for ExecuteH2DatabaseStatement that validates SQL execution + * against an H2 database and verifies output formatting. + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class ExecuteH2DatabaseStatementIT extends InvokeMainTestCase { + + private static File tempDbFile; + private static String dbFilePath; + + @BeforeAll + public static void setUp() throws IOException, SQLException { + // Create a temporary file for the database + tempDbFile = Files.createTempFile("ExecuteH2DatabaseStatementIT", ".db").toFile(); + dbFilePath = tempDbFile.getAbsolutePath(); + + // Remove the .db extension since H2 will add .mv.db + if (dbFilePath.endsWith(".db")) { + dbFilePath = dbFilePath.substring(0, dbFilePath.length() - 3); + } + + // Create and populate a test database + createAndPopulateDatabase(dbFilePath); + } + + @AfterAll + public static void tearDown() { + // Clean up database files + deleteIfExists(new File(dbFilePath + ".mv.db")); + deleteIfExists(new File(dbFilePath + ".trace.db")); + deleteIfExists(tempDbFile); + } + + private static void deleteIfExists(File file) { + if (file.exists()) { + file.delete(); + } + } + + /** + * Creates and populates a test database with departments, academic terms, and courses. + */ + private static void createAndPopulateDatabase(String dbPath) throws SQLException { + File dbFile = new File(dbPath); + + try (Connection connection = H2DatabaseHelper.createFileBasedConnection(dbFile)) { + // Create tables + DepartmentDAOImpl.createTable(connection); + AcademicTermDAOImpl.createTable(connection); + CourseDAOImpl.createTable(connection); + + // Create DAOs + DepartmentDAO departmentDAO = new DepartmentDAOImpl(connection); + AcademicTermDAO termDAO = new AcademicTermDAOImpl(connection); + CourseDAO courseDAO = new CourseDAOImpl(connection); + + // Insert departments + Department csDept = new Department("Computer Science"); + departmentDAO.save(csDept); + + Department mathDept = new Department("Mathematics"); + departmentDAO.save(mathDept); + + Department physicsDept = new Department("Physics"); + departmentDAO.save(physicsDept); + + // Insert academic terms + AcademicTerm fall2024 = new AcademicTerm("Fall 2024", + LocalDate.of(2024, 9, 1), LocalDate.of(2024, 12, 15)); + termDAO.save(fall2024); + + AcademicTerm spring2025 = new AcademicTerm("Spring 2025", + LocalDate.of(2025, 1, 15), LocalDate.of(2025, 5, 30)); + termDAO.save(spring2025); + + // Insert courses + Course javaCourse = new Course("Introduction to Java", csDept.getId(), 4); + courseDAO.save(javaCourse); + + Course dataStructures = new Course("Data Structures", csDept.getId(), 4); + courseDAO.save(dataStructures); + + Course calculus = new Course("Calculus I", mathDept.getId(), 4); + courseDAO.save(calculus); + + Course physics101 = new Course("Physics I", physicsDept.getId(), 5); + courseDAO.save(physics101); + } + } + + @Test + @Order(1) + public void testSelectAllDepartments() { + MainMethodResult result = invokeMain(ExecuteH2DatabaseStatement.class, dbFilePath, "SELECT * FROM departments ORDER BY id"); + String output = result.getTextWrittenToStandardOut(); + + // Validate output contains all departments + assertThat(output, containsString("Computer Science")); + assertThat(output, containsString("Mathematics")); + assertThat(output, containsString("Physics")); + assertThat(output, containsString("3 row(s) returned")); + + // Validate table formatting + assertThat(output, containsString("+")); + assertThat(output, containsString("|")); + assertThat(output, containsString("ID")); + assertThat(output, containsString("NAME")); + } + + @Test + @Order(2) + public void testSelectSingleDepartment() { + MainMethodResult result = invokeMain(ExecuteH2DatabaseStatement.class, dbFilePath, + "SELECT name FROM departments WHERE id = 1"); + String output = result.getTextWrittenToStandardOut(); + + assertThat(output, containsString("Computer Science")); + assertThat(output, containsString("1 row(s) returned")); + assertThat(output, containsString("NAME")); + } + + @Test + @Order(3) + public void testSelectAllCourses() { + MainMethodResult result = invokeMain(ExecuteH2DatabaseStatement.class, dbFilePath, + "SELECT * FROM courses ORDER BY id"); + String output = result.getTextWrittenToStandardOut(); + + assertThat(output, containsString("Introduction to Java")); + assertThat(output, containsString("Data Structures")); + assertThat(output, containsString("Calculus I")); + assertThat(output, containsString("Physics I")); + assertThat(output, containsString("4 row(s) returned")); + assertThat(output, containsString("TITLE")); + assertThat(output, containsString("CREDITS")); + } + + @Test + @Order(4) + public void testSelectAcademicTerms() { + MainMethodResult result = invokeMain(ExecuteH2DatabaseStatement.class, dbFilePath, + "SELECT id, name FROM academic_terms ORDER BY id"); + String output = result.getTextWrittenToStandardOut(); + + assertThat(output, containsString("Fall 2024")); + assertThat(output, containsString("Spring 2025")); + assertThat(output, containsString("2 row(s) returned")); + } + + @Test + @Order(5) + public void testSelectWithJoin() { + MainMethodResult result = invokeMain(ExecuteH2DatabaseStatement.class, dbFilePath, + "SELECT c.title, d.name FROM courses c JOIN departments d ON c.department_id = d.id WHERE d.name = 'Computer Science'"); + String output = result.getTextWrittenToStandardOut(); + + assertThat(output, containsString("Introduction to Java")); + assertThat(output, containsString("Data Structures")); + assertThat(output, containsString("2 row(s) returned")); + } + + @Test + @Order(6) + public void testInsertOperation() { + MainMethodResult result = invokeMain(ExecuteH2DatabaseStatement.class, dbFilePath, + "INSERT INTO departments (name) VALUES ('Engineering')"); + String output = result.getTextWrittenToStandardOut(); + + assertThat(output, containsString("Statement executed successfully")); + assertThat(output, containsString("Rows affected: 1")); + + // Verify the insert worked by querying + MainMethodResult queryResult = invokeMain(ExecuteH2DatabaseStatement.class, dbFilePath, + "SELECT * FROM departments WHERE name = 'Engineering'"); + String queryOutput = queryResult.getTextWrittenToStandardOut(); + assertThat(queryOutput, containsString("Engineering")); + assertThat(queryOutput, containsString("1 row(s) returned")); + } + + @Test + @Order(7) + public void testUpdateOperation() { + MainMethodResult result = invokeMain(ExecuteH2DatabaseStatement.class, dbFilePath, + "UPDATE departments SET name = 'Applied Mathematics' WHERE name = 'Mathematics'"); + String output = result.getTextWrittenToStandardOut(); + + assertThat(output, containsString("Statement executed successfully")); + assertThat(output, containsString("Rows affected: 1")); + + // Verify the update worked + MainMethodResult queryResult = invokeMain(ExecuteH2DatabaseStatement.class, dbFilePath, + "SELECT name FROM departments WHERE name = 'Applied Mathematics'"); + String queryOutput = queryResult.getTextWrittenToStandardOut(); + assertThat(queryOutput, containsString("Applied Mathematics")); + } + + @Test + @Order(8) + public void testDeleteOperation() { + // Delete the Engineering department that was added by testInsertOperation + // This avoids foreign key issues and doesn't affect other tests + MainMethodResult result = invokeMain(ExecuteH2DatabaseStatement.class, dbFilePath, + "DELETE FROM departments WHERE name = 'Engineering'"); + String output = result.getTextWrittenToStandardOut(); + + assertThat(output, containsString("Statement executed successfully")); + assertThat(output, containsString("Rows affected: 1")); + + // Verify the delete worked + MainMethodResult queryResult = invokeMain(ExecuteH2DatabaseStatement.class, dbFilePath, + "SELECT * FROM departments WHERE name = 'Engineering'"); + String queryOutput = queryResult.getTextWrittenToStandardOut(); + assertThat(queryOutput, containsString("No rows returned")); + } + + @Test + @Order(9) + public void testSelectNoResults() { + MainMethodResult result = invokeMain(ExecuteH2DatabaseStatement.class, dbFilePath, + "SELECT * FROM departments WHERE id = 999"); + String output = result.getTextWrittenToStandardOut(); + + assertThat(output, containsString("No rows returned")); + } + + @Test + @Order(10) + public void testMissingArguments() { + MainMethodResult result = invokeMain(ExecuteH2DatabaseStatement.class); + String errorOutput = result.getTextWrittenToStandardError(); + + assertThat(errorOutput, containsString("Missing required arguments")); + assertThat(errorOutput, containsString("Usage:")); + assertThat(errorOutput, containsString("SELECT:")); + assertThat(errorOutput, containsString("INSERT:")); + assertThat(errorOutput, containsString("UPDATE:")); + assertThat(errorOutput, containsString("DELETE:")); + } + + @Test + @Order(11) + public void testDatabasePathDisplayed() { + MainMethodResult result = invokeMain(ExecuteH2DatabaseStatement.class, dbFilePath, + "SELECT * FROM departments"); + String output = result.getTextWrittenToStandardOut(); + + assertThat(output, containsString("Connecting to H2 database:")); + assertThat(output, containsString(dbFilePath)); + } + + @Test + @Order(12) + public void testSqlStatementDisplayed() { + String sql = "SELECT * FROM departments"; + MainMethodResult result = invokeMain(ExecuteH2DatabaseStatement.class, dbFilePath, sql); + String output = result.getTextWrittenToStandardOut(); + + assertThat(output, containsString("Executing SQL:")); + assertThat(output, containsString(sql)); + } + + @Test + @Order(13) + public void testTableFormattingWithNullValues() { + // Insert a department with a course that has no credits (hypothetically, for null testing) + // Since our schema doesn't allow nulls, we'll just verify formatting works correctly + MainMethodResult result = invokeMain(ExecuteH2DatabaseStatement.class, dbFilePath, + "SELECT id, name FROM departments ORDER BY id"); + String output = result.getTextWrittenToStandardOut(); + + // Verify table has proper borders + assertThat(output, matchesPattern("(?s).*\\+[-+]+\\+.*")); + assertThat(output, matchesPattern("(?s).*\\|.*\\|.*")); + } + + @Test + @Order(14) + public void testCountQuery() { + MainMethodResult result = invokeMain(ExecuteH2DatabaseStatement.class, dbFilePath, + "SELECT COUNT(*) AS total FROM courses"); + String output = result.getTextWrittenToStandardOut(); + + assertThat(output, containsString("TOTAL")); + assertThat(output, containsString("4")); + assertThat(output, containsString("1 row(s) returned")); + } +} + diff --git a/examples/src/main/java/edu/pdx/cs/joy/jdbc/ExecuteH2DatabaseStatement.java b/examples/src/main/java/edu/pdx/cs/joy/jdbc/ExecuteH2DatabaseStatement.java new file mode 100644 index 000000000..6bb4c3583 --- /dev/null +++ b/examples/src/main/java/edu/pdx/cs/joy/jdbc/ExecuteH2DatabaseStatement.java @@ -0,0 +1,168 @@ +package edu.pdx.cs.joy.jdbc; + +import java.io.File; +import java.sql.*; + +/** + * A command-line program that executes SQL statements against an H2 database. + * Supports all CRUD operations (INSERT, SELECT, UPDATE, DELETE) and displays + * results in a human-readable format. + */ +public class ExecuteH2DatabaseStatement { + + /** + * Main method that takes a database file path and SQL statement as arguments. + * + * @param args command line arguments: args[0] = database file path, args[1] = SQL statement + * @throws SQLException if a database error occurs + */ + public static void main(String[] args) throws SQLException { + if (args.length < 2) { + System.err.println("Missing required arguments"); + System.err.println("Usage: java ExecuteH2DatabaseStatement "); + System.err.println(); + System.err.println("Examples:"); + System.err.println(" SELECT: java ExecuteH2DatabaseStatement mydb.db \"SELECT * FROM users\""); + System.err.println(" INSERT: java ExecuteH2DatabaseStatement mydb.db \"INSERT INTO users (name) VALUES ('John')\""); + System.err.println(" UPDATE: java ExecuteH2DatabaseStatement mydb.db \"UPDATE users SET name='Jane' WHERE id=1\""); + System.err.println(" DELETE: java ExecuteH2DatabaseStatement mydb.db \"DELETE FROM users WHERE id=1\""); + return; + } + + String dbFilePath = args[0]; + String sqlStatement = args[1]; + + File dbFile = new File(dbFilePath); + System.out.println("Connecting to H2 database: " + dbFile.getAbsolutePath()); + System.out.println("Executing SQL: " + sqlStatement); + System.out.println(); + + try (Connection connection = H2DatabaseHelper.createFileBasedConnection(dbFile)) { + executeStatement(connection, sqlStatement); + } + } + + /** + * Executes a SQL statement and displays the results. + * + * @param connection the database connection + * @param sql the SQL statement to execute + * @throws SQLException if a database error occurs + */ + private static void executeStatement(Connection connection, String sql) throws SQLException { + try (Statement statement = connection.createStatement()) { + // Determine if this is a query (SELECT) or update (INSERT/UPDATE/DELETE) + boolean isQuery = statement.execute(sql); + + if (isQuery) { + // Handle SELECT queries + try (ResultSet resultSet = statement.getResultSet()) { + displayResultSet(resultSet); + } + } else { + // Handle INSERT, UPDATE, DELETE + int rowsAffected = statement.getUpdateCount(); + System.out.println("Statement executed successfully"); + System.out.println("Rows affected: " + rowsAffected); + } + } + } + + /** + * Displays a ResultSet in a formatted table. + * + * @param resultSet the result set to display + * @throws SQLException if a database error occurs + */ + private static void displayResultSet(ResultSet resultSet) throws SQLException { + ResultSetMetaData metaData = resultSet.getMetaData(); + int columnCount = metaData.getColumnCount(); + + // Calculate column widths + int[] columnWidths = new int[columnCount]; + String[] columnNames = new String[columnCount]; + + for (int i = 1; i <= columnCount; i++) { + columnNames[i - 1] = metaData.getColumnLabel(i); + columnWidths[i - 1] = Math.max(columnNames[i - 1].length(), 10); + } + + // Collect all rows to calculate proper column widths + java.util.List rows = new java.util.ArrayList<>(); + while (resultSet.next()) { + String[] row = new String[columnCount]; + for (int i = 1; i <= columnCount; i++) { + Object value = resultSet.getObject(i); + row[i - 1] = value != null ? value.toString() : "NULL"; + columnWidths[i - 1] = Math.max(columnWidths[i - 1], row[i - 1].length()); + } + rows.add(row); + } + + // Print header + printSeparator(columnWidths); + printRow(columnNames, columnWidths); + printSeparator(columnWidths); + + // Print data rows + if (rows.isEmpty()) { + System.out.println("No rows returned"); + } else { + for (String[] row : rows) { + printRow(row, columnWidths); + } + printSeparator(columnWidths); + System.out.println(rows.size() + " row(s) returned"); + } + } + + /** + * Prints a separator line. + * + * @param columnWidths array of column widths + */ + private static void printSeparator(int[] columnWidths) { + System.out.print("+"); + for (int width : columnWidths) { + for (int i = 0; i < width + 2; i++) { + System.out.print("-"); + } + System.out.print("+"); + } + System.out.println(); + } + + /** + * Prints a data row. + * + * @param values array of values to print + * @param columnWidths array of column widths + */ + private static void printRow(String[] values, int[] columnWidths) { + System.out.print("|"); + for (int i = 0; i < values.length; i++) { + System.out.print(" "); + System.out.print(padRight(values[i], columnWidths[i])); + System.out.print(" |"); + } + System.out.println(); + } + + /** + * Pads a string to the right with spaces. + * + * @param str the string to pad + * @param length the desired length + * @return the padded string + */ + private static String padRight(String str, int length) { + if (str.length() >= length) { + return str; + } + StringBuilder sb = new StringBuilder(str); + while (sb.length() < length) { + sb.append(' '); + } + return sb.toString(); + } +} From e7295b1b353277e3d88937cdfa25d6aea243745d Mon Sep 17 00:00:00 2001 From: David Whitlock Date: Tue, 30 Dec 2025 05:30:17 -0800 Subject: [PATCH 26/36] Now that the examples jar no longer depends on the family jar, the web project needs to depend on family. --- web/pom.xml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/web/pom.xml b/web/pom.xml index 23b2ca8f0..d8b4c10f1 100644 --- a/web/pom.xml +++ b/web/pom.xml @@ -119,6 +119,11 @@ examples 1.3.5-SNAPSHOT + + io.github.davidwhitlock.joy + family + 1.1.6-SNAPSHOT + jakarta.xml.bind jakarta.xml.bind-api From dca0b449940245209219b76a7975de4dfef2c6cd Mon Sep 17 00:00:00 2001 From: David Whitlock Date: Thu, 1 Jan 2026 07:01:49 -0800 Subject: [PATCH 27/36] With the addition of JDBC content, increment the minor version of the "examples" artifact. --- examples/pom.xml | 2 +- family/pom.xml | 2 +- .../src/main/resources/archetype-resources/pom.xml | 2 +- .../src/main/resources/archetype-resources/pom.xml | 2 +- .../src/main/resources/archetype-resources/pom.xml | 2 +- .../src/main/resources/archetype-resources/pom.xml | 2 +- projects-parent/originals-parent/airline-web/pom.xml | 2 +- projects-parent/originals-parent/apptbook-web/pom.xml | 2 +- projects-parent/originals-parent/phonebill-web/pom.xml | 2 +- projects-parent/originals-parent/student/pom.xml | 2 +- web/pom.xml | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/examples/pom.xml b/examples/pom.xml index cf9ed6f6d..6e7bf44af 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -8,7 +8,7 @@ 4.0.0 examples examples - 1.3.5-SNAPSHOT + 1.4.0-SNAPSHOT https://www.cs.pdx.edu/~whitlock diff --git a/family/pom.xml b/family/pom.xml index 8d156a047..c257dba2b 100644 --- a/family/pom.xml +++ b/family/pom.xml @@ -21,7 +21,7 @@ io.github.davidwhitlock.joy examples - 1.3.5-SNAPSHOT + 1.4.0-SNAPSHOT com.h2database diff --git a/projects-parent/archetypes-parent/airline-web-archetype/src/main/resources/archetype-resources/pom.xml b/projects-parent/archetypes-parent/airline-web-archetype/src/main/resources/archetype-resources/pom.xml index 29708764b..17c92bd53 100644 --- a/projects-parent/archetypes-parent/airline-web-archetype/src/main/resources/archetype-resources/pom.xml +++ b/projects-parent/archetypes-parent/airline-web-archetype/src/main/resources/archetype-resources/pom.xml @@ -26,7 +26,7 @@ io.github.davidwhitlock.joy examples - 1.3.5-SNAPSHOT + 1.4.0-SNAPSHOT io.github.davidwhitlock.joy diff --git a/projects-parent/archetypes-parent/apptbook-web-archetype/src/main/resources/archetype-resources/pom.xml b/projects-parent/archetypes-parent/apptbook-web-archetype/src/main/resources/archetype-resources/pom.xml index 28ec5c60a..e884c3ada 100644 --- a/projects-parent/archetypes-parent/apptbook-web-archetype/src/main/resources/archetype-resources/pom.xml +++ b/projects-parent/archetypes-parent/apptbook-web-archetype/src/main/resources/archetype-resources/pom.xml @@ -26,7 +26,7 @@ io.github.davidwhitlock.joy examples - 1.3.5-SNAPSHOT + 1.4.0-SNAPSHOT io.github.davidwhitlock.joy diff --git a/projects-parent/archetypes-parent/phonebill-web-archetype/src/main/resources/archetype-resources/pom.xml b/projects-parent/archetypes-parent/phonebill-web-archetype/src/main/resources/archetype-resources/pom.xml index aa7661620..b171b32a7 100644 --- a/projects-parent/archetypes-parent/phonebill-web-archetype/src/main/resources/archetype-resources/pom.xml +++ b/projects-parent/archetypes-parent/phonebill-web-archetype/src/main/resources/archetype-resources/pom.xml @@ -27,7 +27,7 @@ io.github.davidwhitlock.joy examples - 1.3.5-SNAPSHOT + 1.4.0-SNAPSHOT io.github.davidwhitlock.joy diff --git a/projects-parent/archetypes-parent/student-archetype/src/main/resources/archetype-resources/pom.xml b/projects-parent/archetypes-parent/student-archetype/src/main/resources/archetype-resources/pom.xml index d03bc1d7b..cdbdc408c 100644 --- a/projects-parent/archetypes-parent/student-archetype/src/main/resources/archetype-resources/pom.xml +++ b/projects-parent/archetypes-parent/student-archetype/src/main/resources/archetype-resources/pom.xml @@ -102,7 +102,7 @@ io.github.davidwhitlock.joy examples - 1.3.5-SNAPSHOT + 1.4.0-SNAPSHOT io.github.davidwhitlock.joy diff --git a/projects-parent/originals-parent/airline-web/pom.xml b/projects-parent/originals-parent/airline-web/pom.xml index cf0db4c5b..c558fd073 100644 --- a/projects-parent/originals-parent/airline-web/pom.xml +++ b/projects-parent/originals-parent/airline-web/pom.xml @@ -26,7 +26,7 @@ io.github.davidwhitlock.joy examples - 1.3.5-SNAPSHOT + 1.4.0-SNAPSHOT io.github.davidwhitlock.joy diff --git a/projects-parent/originals-parent/apptbook-web/pom.xml b/projects-parent/originals-parent/apptbook-web/pom.xml index 88620f899..7e4a3871e 100644 --- a/projects-parent/originals-parent/apptbook-web/pom.xml +++ b/projects-parent/originals-parent/apptbook-web/pom.xml @@ -26,7 +26,7 @@ io.github.davidwhitlock.joy examples - 1.3.5-SNAPSHOT + 1.4.0-SNAPSHOT io.github.davidwhitlock.joy diff --git a/projects-parent/originals-parent/phonebill-web/pom.xml b/projects-parent/originals-parent/phonebill-web/pom.xml index 990426c30..e311b3736 100644 --- a/projects-parent/originals-parent/phonebill-web/pom.xml +++ b/projects-parent/originals-parent/phonebill-web/pom.xml @@ -27,7 +27,7 @@ io.github.davidwhitlock.joy examples - 1.3.5-SNAPSHOT + 1.4.0-SNAPSHOT io.github.davidwhitlock.joy diff --git a/projects-parent/originals-parent/student/pom.xml b/projects-parent/originals-parent/student/pom.xml index b1a54d6d7..67b23a190 100644 --- a/projects-parent/originals-parent/student/pom.xml +++ b/projects-parent/originals-parent/student/pom.xml @@ -88,7 +88,7 @@ io.github.davidwhitlock.joy examples - 1.3.5-SNAPSHOT + 1.4.0-SNAPSHOT io.github.davidwhitlock.joy diff --git a/web/pom.xml b/web/pom.xml index d8b4c10f1..23425e900 100644 --- a/web/pom.xml +++ b/web/pom.xml @@ -117,7 +117,7 @@ io.github.davidwhitlock.joy examples - 1.3.5-SNAPSHOT + 1.4.0-SNAPSHOT io.github.davidwhitlock.joy From eb987c9bfc87be73bccf1af7c86af3bc886937b1 Mon Sep 17 00:00:00 2001 From: David Whitlock Date: Thu, 1 Jan 2026 07:31:16 -0800 Subject: [PATCH 28/36] Extract the version of H2 into the top-level pom.xml. --- examples/pom.xml | 2 +- family/pom.xml | 2 +- pom.xml | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/examples/pom.xml b/examples/pom.xml index 6e7bf44af..711e9cd06 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -40,7 +40,7 @@ com.h2database h2 - 2.2.224 + ${h2.version} io.github.davidwhitlock.joy diff --git a/family/pom.xml b/family/pom.xml index c257dba2b..863fb3823 100644 --- a/family/pom.xml +++ b/family/pom.xml @@ -26,7 +26,7 @@ com.h2database h2 - 2.2.224 + ${h2.version} diff --git a/pom.xml b/pom.xml index 3a684b7e7..edf37f061 100644 --- a/pom.xml +++ b/pom.xml @@ -59,6 +59,7 @@ 33.5.0-jre 7.0.0 + 2.2.224 6.1.0 UTF-8 From ba1a19cd8a86639f6bec8bd570bec523809bbca1 Mon Sep 17 00:00:00 2001 From: David Whitlock Date: Thu, 1 Jan 2026 08:24:14 -0800 Subject: [PATCH 29/36] Beginnings of a DAO example for a PhoneBill. --- .../originals-parent/phonebill/pom.xml | 6 + .../cs/joy/phonebill/PhoneBillDAOTest.java | 114 ++++++++++++++++++ 2 files changed, 120 insertions(+) create mode 100644 projects-parent/originals-parent/phonebill/src/test/java/edu/pdx/cs/joy/phonebill/PhoneBillDAOTest.java diff --git a/projects-parent/originals-parent/phonebill/pom.xml b/projects-parent/originals-parent/phonebill/pom.xml index ee5222080..581828304 100644 --- a/projects-parent/originals-parent/phonebill/pom.xml +++ b/projects-parent/originals-parent/phonebill/pom.xml @@ -47,6 +47,12 @@ tests test + + com.h2database + h2 + ${h2.version} + test + diff --git a/projects-parent/originals-parent/phonebill/src/test/java/edu/pdx/cs/joy/phonebill/PhoneBillDAOTest.java b/projects-parent/originals-parent/phonebill/src/test/java/edu/pdx/cs/joy/phonebill/PhoneBillDAOTest.java new file mode 100644 index 000000000..468cfdb3f --- /dev/null +++ b/projects-parent/originals-parent/phonebill/src/test/java/edu/pdx/cs/joy/phonebill/PhoneBillDAOTest.java @@ -0,0 +1,114 @@ +package edu.pdx.cs.joy.phonebill; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.sql.*; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; + +/** + * A simple example unit test that demonstrates persisting a PhoneBill + * to an H2 in-memory database using JDBC. + * + * This is a starting point for students to understand how to use + * Data Access Objects (DAOs) to persist domain objects to a database. + */ +public class PhoneBillDAOTest { + + private Connection connection; + + @BeforeEach + public void setUp() throws SQLException { + // Create an in-memory H2 database + connection = DriverManager.getConnection("jdbc:h2:mem:phonebill_test"); + + // Create the phone_bills table + createTable(); + } + + @AfterEach + public void tearDown() throws SQLException { + if (connection != null && !connection.isClosed()) { + connection.close(); + } + } + + /** + * Creates the phone_bills table in the database. + */ + private void createTable() throws SQLException { + String createTableSQL = + "CREATE TABLE phone_bills (" + + " customer_name VARCHAR(255) PRIMARY KEY" + + ")"; + + try (Statement statement = connection.createStatement()) { + statement.execute(createTableSQL); + } + } + + @Test + public void canPersistAndFetchPhoneBillByCustomerName() throws SQLException { + String customerName = "Jane Doe"; + PhoneBill bill = new PhoneBill(customerName); + + // Persist the phone bill + savePhoneBill(bill); + + // Fetch the phone bill by customer name + PhoneBill fetchedBill = findPhoneBillByCustomer(customerName); + + // Validate that the fetched bill matches the original + assertThat(fetchedBill, is(notNullValue())); + assertThat(fetchedBill.getCustomer(), is(equalTo(customerName))); + } + + @Test + public void returnsNullWhenPhoneBillNotFound() throws SQLException { + PhoneBill fetchedBill = findPhoneBillByCustomer("Non-existent Customer"); + assertThat(fetchedBill, is(nullValue())); + } + + /** + * Saves a PhoneBill to the database. + * + * @param bill the phone bill to save + * @throws SQLException if a database error occurs + */ + private void savePhoneBill(PhoneBill bill) throws SQLException { + String insertSQL = "INSERT INTO phone_bills (customer_name) VALUES (?)"; + + try (PreparedStatement statement = connection.prepareStatement(insertSQL)) { + statement.setString(1, bill.getCustomer()); + statement.executeUpdate(); + } + } + + /** + * Finds a PhoneBill by customer name. + * + * @param customerName the customer name to search for + * @return the PhoneBill for the customer, or null if not found + * @throws SQLException if a database error occurs + */ + private PhoneBill findPhoneBillByCustomer(String customerName) throws SQLException { + String selectSQL = "SELECT customer_name FROM phone_bills WHERE customer_name = ?"; + + try (PreparedStatement statement = connection.prepareStatement(selectSQL)) { + statement.setString(1, customerName); + + try (ResultSet resultSet = statement.executeQuery()) { + if (resultSet.next()) { + String name = resultSet.getString("customer_name"); + return new PhoneBill(name); + } + } + } + + return null; + } +} + From 001b57442188cbd5f76cb4e052d0b0821f846bce Mon Sep 17 00:00:00 2001 From: David Whitlock Date: Thu, 1 Jan 2026 08:29:27 -0800 Subject: [PATCH 30/36] Extract a skeletal DAO for phone bills. --- .../pdx/cs/joy/phonebill/PhoneBillDAO.java | 81 +++++++++++++++++++ .../cs/joy/phonebill/PhoneBillDAOTest.java | 67 +++------------ 2 files changed, 90 insertions(+), 58 deletions(-) create mode 100644 projects-parent/originals-parent/phonebill/src/main/java/edu/pdx/cs/joy/phonebill/PhoneBillDAO.java diff --git a/projects-parent/originals-parent/phonebill/src/main/java/edu/pdx/cs/joy/phonebill/PhoneBillDAO.java b/projects-parent/originals-parent/phonebill/src/main/java/edu/pdx/cs/joy/phonebill/PhoneBillDAO.java new file mode 100644 index 000000000..84b3d42f4 --- /dev/null +++ b/projects-parent/originals-parent/phonebill/src/main/java/edu/pdx/cs/joy/phonebill/PhoneBillDAO.java @@ -0,0 +1,81 @@ +package edu.pdx.cs.joy.phonebill; + +import java.sql.*; + +/** + * A Data Access Object (DAO) for persisting PhoneBill instances to a database. + * + * This is a simple example to demonstrate basic JDBC operations. + * Students can expand this to include PhoneCall persistence and more + * sophisticated query capabilities. + */ +public class PhoneBillDAO { + + private final Connection connection; + + /** + * Creates a new PhoneBillDAO with the specified database connection. + * + * @param connection the database connection to use + */ + public PhoneBillDAO(Connection connection) { + this.connection = connection; + } + + /** + * Creates the phone_bills table in the database. + * + * @param connection the database connection to use + * @throws SQLException if a database error occurs + */ + public static void createTable(Connection connection) throws SQLException { + String createTableSQL = + "CREATE TABLE phone_bills (" + + " customer_name VARCHAR(255) PRIMARY KEY" + + ")"; + + try (Statement statement = connection.createStatement()) { + statement.execute(createTableSQL); + } + } + + /** + * Saves a PhoneBill to the database. + * + * @param bill the phone bill to save + * @throws SQLException if a database error occurs + */ + public void save(PhoneBill bill) throws SQLException { + String insertSQL = "INSERT INTO phone_bills (customer_name) VALUES (?)"; + + try (PreparedStatement statement = connection.prepareStatement(insertSQL)) { + statement.setString(1, bill.getCustomer()); + statement.executeUpdate(); + } + } + + /** + * Finds a PhoneBill by customer name. + * + * @param customerName the customer name to search for + * @return the PhoneBill for the customer, or null if not found + * @throws SQLException if a database error occurs + */ + public PhoneBill findByCustomer(String customerName) throws SQLException { + String selectSQL = "SELECT customer_name FROM phone_bills WHERE customer_name = ?"; + + try (PreparedStatement statement = connection.prepareStatement(selectSQL)) { + statement.setString(1, customerName); + + try (ResultSet resultSet = statement.executeQuery()) { + if (resultSet.next()) { + String name = resultSet.getString("customer_name"); + return new PhoneBill(name); + } + } + } + + return null; + } +} + diff --git a/projects-parent/originals-parent/phonebill/src/test/java/edu/pdx/cs/joy/phonebill/PhoneBillDAOTest.java b/projects-parent/originals-parent/phonebill/src/test/java/edu/pdx/cs/joy/phonebill/PhoneBillDAOTest.java index 468cfdb3f..eff5e73e8 100644 --- a/projects-parent/originals-parent/phonebill/src/test/java/edu/pdx/cs/joy/phonebill/PhoneBillDAOTest.java +++ b/projects-parent/originals-parent/phonebill/src/test/java/edu/pdx/cs/joy/phonebill/PhoneBillDAOTest.java @@ -19,6 +19,7 @@ public class PhoneBillDAOTest { private Connection connection; + private PhoneBillDAO dao; @BeforeEach public void setUp() throws SQLException { @@ -26,7 +27,10 @@ public void setUp() throws SQLException { connection = DriverManager.getConnection("jdbc:h2:mem:phonebill_test"); // Create the phone_bills table - createTable(); + PhoneBillDAO.createTable(connection); + + // Create the DAO + dao = new PhoneBillDAO(connection); } @AfterEach @@ -36,30 +40,16 @@ public void tearDown() throws SQLException { } } - /** - * Creates the phone_bills table in the database. - */ - private void createTable() throws SQLException { - String createTableSQL = - "CREATE TABLE phone_bills (" + - " customer_name VARCHAR(255) PRIMARY KEY" + - ")"; - - try (Statement statement = connection.createStatement()) { - statement.execute(createTableSQL); - } - } - @Test public void canPersistAndFetchPhoneBillByCustomerName() throws SQLException { String customerName = "Jane Doe"; PhoneBill bill = new PhoneBill(customerName); - // Persist the phone bill - savePhoneBill(bill); + // Persist the phone bill using the DAO + dao.save(bill); // Fetch the phone bill by customer name - PhoneBill fetchedBill = findPhoneBillByCustomer(customerName); + PhoneBill fetchedBill = dao.findByCustomer(customerName); // Validate that the fetched bill matches the original assertThat(fetchedBill, is(notNullValue())); @@ -68,47 +58,8 @@ public void canPersistAndFetchPhoneBillByCustomerName() throws SQLException { @Test public void returnsNullWhenPhoneBillNotFound() throws SQLException { - PhoneBill fetchedBill = findPhoneBillByCustomer("Non-existent Customer"); + PhoneBill fetchedBill = dao.findByCustomer("Non-existent Customer"); assertThat(fetchedBill, is(nullValue())); } - - /** - * Saves a PhoneBill to the database. - * - * @param bill the phone bill to save - * @throws SQLException if a database error occurs - */ - private void savePhoneBill(PhoneBill bill) throws SQLException { - String insertSQL = "INSERT INTO phone_bills (customer_name) VALUES (?)"; - - try (PreparedStatement statement = connection.prepareStatement(insertSQL)) { - statement.setString(1, bill.getCustomer()); - statement.executeUpdate(); - } - } - - /** - * Finds a PhoneBill by customer name. - * - * @param customerName the customer name to search for - * @return the PhoneBill for the customer, or null if not found - * @throws SQLException if a database error occurs - */ - private PhoneBill findPhoneBillByCustomer(String customerName) throws SQLException { - String selectSQL = "SELECT customer_name FROM phone_bills WHERE customer_name = ?"; - - try (PreparedStatement statement = connection.prepareStatement(selectSQL)) { - statement.setString(1, customerName); - - try (ResultSet resultSet = statement.executeQuery()) { - if (resultSet.next()) { - String name = resultSet.getString("customer_name"); - return new PhoneBill(name); - } - } - } - - return null; - } } From cd18d82e51ce8057dd834cd4aba97c9c320e43e7 Mon Sep 17 00:00:00 2001 From: David Whitlock Date: Thu, 1 Jan 2026 08:40:43 -0800 Subject: [PATCH 31/36] Only print "interesting" tables in PrintH2DatabaseSchema. That is, ignore the built-in tables. --- .../edu/pdx/cs/joy/jdbc/ManageDepartments.java | 1 - .../pdx/cs/joy/jdbc/PrintH2DatabaseSchema.java | 16 ++++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/examples/src/main/java/edu/pdx/cs/joy/jdbc/ManageDepartments.java b/examples/src/main/java/edu/pdx/cs/joy/jdbc/ManageDepartments.java index 2762cd775..ad66e748f 100644 --- a/examples/src/main/java/edu/pdx/cs/joy/jdbc/ManageDepartments.java +++ b/examples/src/main/java/edu/pdx/cs/joy/jdbc/ManageDepartments.java @@ -55,7 +55,6 @@ public static void main(String[] args) throws SQLException { default: System.err.println("Unknown command: " + command); printUsage(); - return; } } } diff --git a/examples/src/main/java/edu/pdx/cs/joy/jdbc/PrintH2DatabaseSchema.java b/examples/src/main/java/edu/pdx/cs/joy/jdbc/PrintH2DatabaseSchema.java index 5e21ec49b..9d55b38be 100644 --- a/examples/src/main/java/edu/pdx/cs/joy/jdbc/PrintH2DatabaseSchema.java +++ b/examples/src/main/java/edu/pdx/cs/joy/jdbc/PrintH2DatabaseSchema.java @@ -2,6 +2,7 @@ import java.io.File; import java.sql.*; +import java.util.Set; /** * A command-line program that uses the JDBC DatabaseMetaData API to print @@ -9,6 +10,12 @@ */ public class PrintH2DatabaseSchema { + private static final Set uninterestingTablePrefixes = Set.of( + "CONSTANTS", "ENUM_VALUES", "INDEXES", "INDEX_COLUMNS", "INFORMATION_SCHEMA_CATALOG_NAME", + "IN_DOUBT", "LOCKS", "QUERY_STATISTICS", "RIGHTS", "ROLES", "SESSIONS", "SESSION_STATE", + "SETTINGS", "SYNONYMS", "USERS" + ); + /** * Prints information about all tables in the database. * @@ -33,6 +40,11 @@ private static void printDatabaseSchema(Connection connection) throws SQLExcepti while (tables.next()) { foundTables = true; String tableName = tables.getString("TABLE_NAME"); + + if (tableNameIsNotInteresting(tableName)) { + continue; // Skip system or uninteresting tables + } + String tableType = tables.getString("TABLE_TYPE"); String remarks = tables.getString("REMARKS"); @@ -61,6 +73,10 @@ private static void printDatabaseSchema(Connection connection) throws SQLExcepti } } + private static boolean tableNameIsNotInteresting(String tableName) { + return uninterestingTablePrefixes.stream().anyMatch(tableName::startsWith); + } + /** * Prints information about columns in a table. */ From 2b25be1c40ea2a8b9603469a932c927b53d491b9 Mon Sep 17 00:00:00 2001 From: David Whitlock Date: Thu, 1 Jan 2026 09:03:30 -0800 Subject: [PATCH 32/36] Rename the phone bill table names. --- .../java/edu/pdx/cs/joy/phonebill/PhoneBillDAO.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/projects-parent/originals-parent/phonebill/src/main/java/edu/pdx/cs/joy/phonebill/PhoneBillDAO.java b/projects-parent/originals-parent/phonebill/src/main/java/edu/pdx/cs/joy/phonebill/PhoneBillDAO.java index 84b3d42f4..b867b24c3 100644 --- a/projects-parent/originals-parent/phonebill/src/main/java/edu/pdx/cs/joy/phonebill/PhoneBillDAO.java +++ b/projects-parent/originals-parent/phonebill/src/main/java/edu/pdx/cs/joy/phonebill/PhoneBillDAO.java @@ -23,15 +23,15 @@ public PhoneBillDAO(Connection connection) { } /** - * Creates the phone_bills table in the database. + * Creates the customers table in the database. * * @param connection the database connection to use * @throws SQLException if a database error occurs */ public static void createTable(Connection connection) throws SQLException { String createTableSQL = - "CREATE TABLE phone_bills (" + - " customer_name VARCHAR(255) PRIMARY KEY" + + "CREATE TABLE customers (" + + " name VARCHAR(255) PRIMARY KEY" + ")"; try (Statement statement = connection.createStatement()) { @@ -46,7 +46,7 @@ public static void createTable(Connection connection) throws SQLException { * @throws SQLException if a database error occurs */ public void save(PhoneBill bill) throws SQLException { - String insertSQL = "INSERT INTO phone_bills (customer_name) VALUES (?)"; + String insertSQL = "INSERT INTO customers (name) VALUES (?)"; try (PreparedStatement statement = connection.prepareStatement(insertSQL)) { statement.setString(1, bill.getCustomer()); @@ -62,14 +62,14 @@ public void save(PhoneBill bill) throws SQLException { * @throws SQLException if a database error occurs */ public PhoneBill findByCustomer(String customerName) throws SQLException { - String selectSQL = "SELECT customer_name FROM phone_bills WHERE customer_name = ?"; + String selectSQL = "SELECT name FROM customers WHERE name = ?"; try (PreparedStatement statement = connection.prepareStatement(selectSQL)) { statement.setString(1, customerName); try (ResultSet resultSet = statement.executeQuery()) { if (resultSet.next()) { - String name = resultSet.getString("customer_name"); + String name = resultSet.getString("name"); return new PhoneBill(name); } } From 84b0ec04766e325f469d7f72513e49e0f98639cd Mon Sep 17 00:00:00 2001 From: David Whitlock Date: Thu, 1 Jan 2026 09:17:22 -0800 Subject: [PATCH 33/36] Add a main method to PhoneBillDAO that smoked out a couple of problems with the dependencies. --- .../originals-parent/phonebill/pom.xml | 16 ++++++----- .../pdx/cs/joy/phonebill/PhoneBillDAO.java | 27 +++++++++++++++++++ 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/projects-parent/originals-parent/phonebill/pom.xml b/projects-parent/originals-parent/phonebill/pom.xml index 581828304..d2ce44a84 100644 --- a/projects-parent/originals-parent/phonebill/pom.xml +++ b/projects-parent/originals-parent/phonebill/pom.xml @@ -29,6 +29,11 @@ + + com.h2database + h2 + ${h2.version} + org.mockito mockito-core @@ -40,6 +45,11 @@ projects 3.0.4-SNAPSHOT + + io.github.davidwhitlock.joy + examples + 1.4.0-SNAPSHOT + io.github.davidwhitlock.joy projects @@ -47,12 +57,6 @@ tests test - - com.h2database - h2 - ${h2.version} - test - diff --git a/projects-parent/originals-parent/phonebill/src/main/java/edu/pdx/cs/joy/phonebill/PhoneBillDAO.java b/projects-parent/originals-parent/phonebill/src/main/java/edu/pdx/cs/joy/phonebill/PhoneBillDAO.java index b867b24c3..97ff802e2 100644 --- a/projects-parent/originals-parent/phonebill/src/main/java/edu/pdx/cs/joy/phonebill/PhoneBillDAO.java +++ b/projects-parent/originals-parent/phonebill/src/main/java/edu/pdx/cs/joy/phonebill/PhoneBillDAO.java @@ -1,5 +1,8 @@ package edu.pdx.cs.joy.phonebill; +import edu.pdx.cs.joy.jdbc.H2DatabaseHelper; + +import java.io.File; import java.sql.*; /** @@ -77,5 +80,29 @@ public PhoneBill findByCustomer(String customerName) throws SQLException { return null; } + + public static void main(String[] args) throws SQLException { + if (args.length < 2) { + System.err.println("Usage: java PhoneBillDAO "); + return; + } + + String dbFile = args[0]; + String customerName = args[1]; + try (Connection connection = H2DatabaseHelper.createFileBasedConnection(new File(dbFile))) { + PhoneBillDAO dao = new PhoneBillDAO(connection); + PhoneBillDAO.createTable(connection); + + PhoneBill bill = new PhoneBill(customerName); + dao.save(bill); + + PhoneBill retrievedBill = dao.findByCustomer(customerName); + if (retrievedBill != null) { + System.out.println("Retrieved PhoneBill for customer: " + retrievedBill.getCustomer()); + } else { + System.out.println("No PhoneBill found for customer: " + customerName); + } + } + } } From 98f8d7515ae8ee756830ddb0a1865ee46bf097a0 Mon Sep 17 00:00:00 2001 From: David Whitlock Date: Thu, 1 Jan 2026 09:27:45 -0800 Subject: [PATCH 34/36] Increment minor version of phonebill artifacts due to inclusion of JDBC assignment. --- projects-parent/archetypes-parent/phonebill-archetype/pom.xml | 2 +- projects-parent/originals-parent/phonebill/pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/projects-parent/archetypes-parent/phonebill-archetype/pom.xml b/projects-parent/archetypes-parent/phonebill-archetype/pom.xml index bdd0b901e..dd9a1a884 100644 --- a/projects-parent/archetypes-parent/phonebill-archetype/pom.xml +++ b/projects-parent/archetypes-parent/phonebill-archetype/pom.xml @@ -7,7 +7,7 @@ 4.0.0 phonebill-archetype - 2.2.5-SNAPSHOT + 2.3.0-SNAPSHOT maven-archetype phonebill-archetype diff --git a/projects-parent/originals-parent/phonebill/pom.xml b/projects-parent/originals-parent/phonebill/pom.xml index d2ce44a84..1e73ae4cc 100644 --- a/projects-parent/originals-parent/phonebill/pom.xml +++ b/projects-parent/originals-parent/phonebill/pom.xml @@ -9,7 +9,7 @@ io.github.davidwhitlock.joy.original phonebill jar - 2.2.5-SNAPSHOT + 2.3.0-SNAPSHOT Phone Bill Project A Phone Bill application for The Joy of Coding at Portland State University 2000 From 8836530815196ee40bc1b1b23880b14263301be5 Mon Sep 17 00:00:00 2001 From: David Whitlock Date: Thu, 1 Jan 2026 09:48:56 -0800 Subject: [PATCH 35/36] Remove XML examples from phone bill project. They'll just confuse people. --- .../src/main/java/PhoneBillXmlHelper.java | 19 -------- .../src/test/java/PhoneBillXmlHelperTest.java | 47 ------------------- .../invalid-__artifactId__.xml | 23 --------- .../valid-__artifactId__.xml | 36 -------------- .../cs/joy/phonebill/PhoneBillXmlHelper.java | 16 ------- .../joy/phonebill/PhoneBillXmlHelperTest.java | 44 ----------------- .../cs/joy/phonebill/invalid-phonebill.xml | 20 -------- .../pdx/cs/joy/phonebill/valid-phonebill.xml | 33 ------------- 8 files changed, 238 deletions(-) delete mode 100644 projects-parent/archetypes-parent/phonebill-archetype/src/main/resources/archetype-resources/src/main/java/PhoneBillXmlHelper.java delete mode 100644 projects-parent/archetypes-parent/phonebill-archetype/src/main/resources/archetype-resources/src/test/java/PhoneBillXmlHelperTest.java delete mode 100644 projects-parent/archetypes-parent/phonebill-archetype/src/main/resources/archetype-resources/src/test/resources/__packageInPathFormat__/invalid-__artifactId__.xml delete mode 100644 projects-parent/archetypes-parent/phonebill-archetype/src/main/resources/archetype-resources/src/test/resources/__packageInPathFormat__/valid-__artifactId__.xml delete mode 100644 projects-parent/originals-parent/phonebill/src/main/java/edu/pdx/cs/joy/phonebill/PhoneBillXmlHelper.java delete mode 100644 projects-parent/originals-parent/phonebill/src/test/java/edu/pdx/cs/joy/phonebill/PhoneBillXmlHelperTest.java delete mode 100644 projects-parent/originals-parent/phonebill/src/test/resources/edu/pdx/cs/joy/phonebill/invalid-phonebill.xml delete mode 100644 projects-parent/originals-parent/phonebill/src/test/resources/edu/pdx/cs/joy/phonebill/valid-phonebill.xml diff --git a/projects-parent/archetypes-parent/phonebill-archetype/src/main/resources/archetype-resources/src/main/java/PhoneBillXmlHelper.java b/projects-parent/archetypes-parent/phonebill-archetype/src/main/resources/archetype-resources/src/main/java/PhoneBillXmlHelper.java deleted file mode 100644 index 78ce6a19a..000000000 --- a/projects-parent/archetypes-parent/phonebill-archetype/src/main/resources/archetype-resources/src/main/java/PhoneBillXmlHelper.java +++ /dev/null @@ -1,19 +0,0 @@ -#set( $symbol_pound = '#' ) -#set( $symbol_dollar = '$' ) -#set( $symbol_escape = '\' ) -package ${package}; - -import edu.pdx.cs.joy.ProjectXmlHelper; - -public class PhoneBillXmlHelper extends ProjectXmlHelper { - - /** - * The Public ID for the Phone Bill DTD - */ - protected static final String PUBLIC_ID = - "-//Joy of Coding at PSU//DTD Phone Bill//EN"; - - protected PhoneBillXmlHelper() { - super(PUBLIC_ID, "phonebill.dtd"); - } -} diff --git a/projects-parent/archetypes-parent/phonebill-archetype/src/main/resources/archetype-resources/src/test/java/PhoneBillXmlHelperTest.java b/projects-parent/archetypes-parent/phonebill-archetype/src/main/resources/archetype-resources/src/test/java/PhoneBillXmlHelperTest.java deleted file mode 100644 index f427e43a9..000000000 --- a/projects-parent/archetypes-parent/phonebill-archetype/src/main/resources/archetype-resources/src/test/java/PhoneBillXmlHelperTest.java +++ /dev/null @@ -1,47 +0,0 @@ -#set( $symbol_pound = '#' ) -#set( $symbol_dollar = '$' ) -#set( $symbol_escape = '\' ) -package ${package}; - -import org.junit.jupiter.api.Test; -import org.xml.sax.SAXException; -import org.xml.sax.SAXParseException; - -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; -import javax.xml.parsers.ParserConfigurationException; -import java.io.IOException; - -import static org.junit.jupiter.api.Assertions.assertThrows; - -class PhoneBillXmlHelperTest { - - @Test - void canParseValidXmlFile() throws ParserConfigurationException, IOException, SAXException { - DocumentBuilder builder = newValidatingDocumentBuilder(new PhoneBillXmlHelper()); - - builder.parse(this.getClass().getResourceAsStream("valid-${artifactId}.xml")); - } - - @Test - void throwsExceptionWhenParsingInvalidXmlFile() throws ParserConfigurationException { - DocumentBuilder builder = newValidatingDocumentBuilder(new PhoneBillXmlHelper()); - - assertThrows(SAXParseException.class, () -> - builder.parse(this.getClass().getResourceAsStream("invalid-${artifactId}.xml")) - ); - } - - private static DocumentBuilder newValidatingDocumentBuilder(PhoneBillXmlHelper helper) throws ParserConfigurationException { - DocumentBuilderFactory factory = - DocumentBuilderFactory.newInstance(); - factory.setValidating(true); - - DocumentBuilder builder = - factory.newDocumentBuilder(); - builder.setErrorHandler(helper); - builder.setEntityResolver(helper); - return builder; - } - -} diff --git a/projects-parent/archetypes-parent/phonebill-archetype/src/main/resources/archetype-resources/src/test/resources/__packageInPathFormat__/invalid-__artifactId__.xml b/projects-parent/archetypes-parent/phonebill-archetype/src/main/resources/archetype-resources/src/test/resources/__packageInPathFormat__/invalid-__artifactId__.xml deleted file mode 100644 index a912ef877..000000000 --- a/projects-parent/archetypes-parent/phonebill-archetype/src/main/resources/archetype-resources/src/test/resources/__packageInPathFormat__/invalid-__artifactId__.xml +++ /dev/null @@ -1,23 +0,0 @@ -#set( $symbol_pound = '#' ) -#set( $symbol_dollar = '$' ) -#set( $symbol_escape = '\' ) - - - - - - Dave - - 503-245-2345 - - - - - - - - - diff --git a/projects-parent/archetypes-parent/phonebill-archetype/src/main/resources/archetype-resources/src/test/resources/__packageInPathFormat__/valid-__artifactId__.xml b/projects-parent/archetypes-parent/phonebill-archetype/src/main/resources/archetype-resources/src/test/resources/__packageInPathFormat__/valid-__artifactId__.xml deleted file mode 100644 index 7fd53d021..000000000 --- a/projects-parent/archetypes-parent/phonebill-archetype/src/main/resources/archetype-resources/src/test/resources/__packageInPathFormat__/valid-__artifactId__.xml +++ /dev/null @@ -1,36 +0,0 @@ -#set( $symbol_pound = '#' ) -#set( $symbol_dollar = '$' ) -#set( $symbol_escape = '\' ) - - - - - - Dave - - 503-245-2345 - 607-777-1369 - - - - - - - - - - 603-868-1932 - 765-497-8254 - - - - - - - - diff --git a/projects-parent/originals-parent/phonebill/src/main/java/edu/pdx/cs/joy/phonebill/PhoneBillXmlHelper.java b/projects-parent/originals-parent/phonebill/src/main/java/edu/pdx/cs/joy/phonebill/PhoneBillXmlHelper.java deleted file mode 100644 index 9c1796ca1..000000000 --- a/projects-parent/originals-parent/phonebill/src/main/java/edu/pdx/cs/joy/phonebill/PhoneBillXmlHelper.java +++ /dev/null @@ -1,16 +0,0 @@ -package edu.pdx.cs.joy.phonebill; - -import edu.pdx.cs.joy.ProjectXmlHelper; - -public class PhoneBillXmlHelper extends ProjectXmlHelper { - - /** - * The Public ID for the Family Tree DTD - */ - protected static final String PUBLIC_ID = - "-//Joy of Coding at PSU//DTD Phone Bill//EN"; - - protected PhoneBillXmlHelper() { - super(PUBLIC_ID, "phonebill.dtd"); - } -} diff --git a/projects-parent/originals-parent/phonebill/src/test/java/edu/pdx/cs/joy/phonebill/PhoneBillXmlHelperTest.java b/projects-parent/originals-parent/phonebill/src/test/java/edu/pdx/cs/joy/phonebill/PhoneBillXmlHelperTest.java deleted file mode 100644 index 49d6fce96..000000000 --- a/projects-parent/originals-parent/phonebill/src/test/java/edu/pdx/cs/joy/phonebill/PhoneBillXmlHelperTest.java +++ /dev/null @@ -1,44 +0,0 @@ -package edu.pdx.cs.joy.phonebill; - -import org.junit.jupiter.api.Test; -import org.xml.sax.SAXException; -import org.xml.sax.SAXParseException; - -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; -import javax.xml.parsers.ParserConfigurationException; -import java.io.IOException; - -import static org.junit.jupiter.api.Assertions.assertThrows; - -class PhoneBillXmlHelperTest { - - @Test - void canParseValidXmlFile() throws ParserConfigurationException, IOException, SAXException { - DocumentBuilder builder = newValidatingDocumentBuilder(new PhoneBillXmlHelper()); - - builder.parse(this.getClass().getResourceAsStream("valid-phonebill.xml")); - } - - @Test - void throwsExceptionWhenParsingInvalidXmlFile() throws ParserConfigurationException { - DocumentBuilder builder = newValidatingDocumentBuilder(new PhoneBillXmlHelper()); - - assertThrows(SAXParseException.class, () -> - builder.parse(this.getClass().getResourceAsStream("invalid-phonebill.xml")) - ); - } - - private static DocumentBuilder newValidatingDocumentBuilder(PhoneBillXmlHelper helper) throws ParserConfigurationException { - DocumentBuilderFactory factory = - DocumentBuilderFactory.newInstance(); - factory.setValidating(true); - - DocumentBuilder builder = - factory.newDocumentBuilder(); - builder.setErrorHandler(helper); - builder.setEntityResolver(helper); - return builder; - } - -} diff --git a/projects-parent/originals-parent/phonebill/src/test/resources/edu/pdx/cs/joy/phonebill/invalid-phonebill.xml b/projects-parent/originals-parent/phonebill/src/test/resources/edu/pdx/cs/joy/phonebill/invalid-phonebill.xml deleted file mode 100644 index c9c187ca6..000000000 --- a/projects-parent/originals-parent/phonebill/src/test/resources/edu/pdx/cs/joy/phonebill/invalid-phonebill.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - Dave - - 503-245-2345 - - - - - - - - - diff --git a/projects-parent/originals-parent/phonebill/src/test/resources/edu/pdx/cs/joy/phonebill/valid-phonebill.xml b/projects-parent/originals-parent/phonebill/src/test/resources/edu/pdx/cs/joy/phonebill/valid-phonebill.xml deleted file mode 100644 index a55047e32..000000000 --- a/projects-parent/originals-parent/phonebill/src/test/resources/edu/pdx/cs/joy/phonebill/valid-phonebill.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - Dave - - 503-245-2345 - 607-777-1369 - - - - - - - - - - 603-868-1932 - 765-497-8254 - - - - - - - - From e2a2334368de82da8e5795d1adba6e371e1304e7 Mon Sep 17 00:00:00 2001 From: David Whitlock Date: Thu, 1 Jan 2026 09:59:39 -0800 Subject: [PATCH 36/36] Add PhoneBillDAO classes to phone bill archetype project. --- .gitignore | 2 + .../META-INF/maven/archetype-metadata.xml | 1 - .../resources/archetype-resources/pom.xml | 18 ++- .../src/main/java/PhoneBillDAO.java | 111 ++++++++++++++++++ .../src/main/java/Project1.java | 2 +- .../src/test/java/PhoneBillDAOTest.java | 68 +++++++++++ 6 files changed, 196 insertions(+), 6 deletions(-) create mode 100644 projects-parent/archetypes-parent/phonebill-archetype/src/main/resources/archetype-resources/src/main/java/PhoneBillDAO.java create mode 100644 projects-parent/archetypes-parent/phonebill-archetype/src/main/resources/archetype-resources/src/test/java/PhoneBillDAOTest.java diff --git a/.gitignore b/.gitignore index 2978bd9b8..758333738 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,5 @@ target */target dependency-reduced-pom.xml +# H2 database files +*.db diff --git a/projects-parent/archetypes-parent/phonebill-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml b/projects-parent/archetypes-parent/phonebill-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml index 1721a943e..2d6e6e6f3 100644 --- a/projects-parent/archetypes-parent/phonebill-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml +++ b/projects-parent/archetypes-parent/phonebill-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml @@ -45,7 +45,6 @@ src/test/resources **/*.txt - **/*.xml diff --git a/projects-parent/archetypes-parent/phonebill-archetype/src/main/resources/archetype-resources/pom.xml b/projects-parent/archetypes-parent/phonebill-archetype/src/main/resources/archetype-resources/pom.xml index f3ef90e28..990a87650 100644 --- a/projects-parent/archetypes-parent/phonebill-archetype/src/main/resources/archetype-resources/pom.xml +++ b/projects-parent/archetypes-parent/phonebill-archetype/src/main/resources/archetype-resources/pom.xml @@ -1,5 +1,5 @@ - + joy io.github.davidwhitlock.joy @@ -13,15 +13,15 @@ Phone Bill Project A Phone Bill application for The Joy of Coding at Portland State University 2000 - http://www.cs.pdx.edu/~whitlock + https://www.cs.pdx.edu/~whitlock YOU Your name here you@youremail.com - http://www.cs.pdx.edu/~YOU + https://www.cs.pdx.edu/~YOU PSU Department of Computer Science - http://www.cs.pdx.edu + https://www.cs.pdx.edu Student @@ -29,6 +29,11 @@ + + com.h2database + h2 + ${h2.version} + org.mockito mockito-core @@ -40,6 +45,11 @@ projects 3.0.4-SNAPSHOT + + io.github.davidwhitlock.joy + examples + 1.4.0-SNAPSHOT + io.github.davidwhitlock.joy projects diff --git a/projects-parent/archetypes-parent/phonebill-archetype/src/main/resources/archetype-resources/src/main/java/PhoneBillDAO.java b/projects-parent/archetypes-parent/phonebill-archetype/src/main/resources/archetype-resources/src/main/java/PhoneBillDAO.java new file mode 100644 index 000000000..6af819766 --- /dev/null +++ b/projects-parent/archetypes-parent/phonebill-archetype/src/main/resources/archetype-resources/src/main/java/PhoneBillDAO.java @@ -0,0 +1,111 @@ +#set( $symbol_pound = '#' ) +#set( $symbol_dollar = '$' ) +#set( $symbol_escape = '\' ) +package ${package}; + +import edu.pdx.cs.joy.jdbc.H2DatabaseHelper; + +import java.io.File; +import java.sql.*; + +/** + * A Data Access Object (DAO) for persisting PhoneBill instances to a database. + * + * This is a simple example to demonstrate basic JDBC operations. + * Students can expand this to include PhoneCall persistence and more + * sophisticated query capabilities. + */ +public class PhoneBillDAO { + + private final Connection connection; + + /** + * Creates a new PhoneBillDAO with the specified database connection. + * + * @param connection the database connection to use + */ + public PhoneBillDAO(Connection connection) { + this.connection = connection; + } + + /** + * Creates the customers table in the database. + * + * @param connection the database connection to use + * @throws SQLException if a database error occurs + */ + public static void createTable(Connection connection) throws SQLException { + String createTableSQL = + "CREATE TABLE customers (" + + " name VARCHAR(255) PRIMARY KEY" + + ")"; + + try (Statement statement = connection.createStatement()) { + statement.execute(createTableSQL); + } + } + + /** + * Saves a PhoneBill to the database. + * + * @param bill the phone bill to save + * @throws SQLException if a database error occurs + */ + public void save(PhoneBill bill) throws SQLException { + String insertSQL = "INSERT INTO customers (name) VALUES (?)"; + + try (PreparedStatement statement = connection.prepareStatement(insertSQL)) { + statement.setString(1, bill.getCustomer()); + statement.executeUpdate(); + } + } + + /** + * Finds a PhoneBill by customer name. + * + * @param customerName the customer name to search for + * @return the PhoneBill for the customer, or null if not found + * @throws SQLException if a database error occurs + */ + public PhoneBill findByCustomer(String customerName) throws SQLException { + String selectSQL = "SELECT name FROM customers WHERE name = ?"; + + try (PreparedStatement statement = connection.prepareStatement(selectSQL)) { + statement.setString(1, customerName); + + try (ResultSet resultSet = statement.executeQuery()) { + if (resultSet.next()) { + String name = resultSet.getString("name"); + return new PhoneBill(name); + } + } + } + + return null; + } + + public static void main(String[] args) throws SQLException { + if (args.length < 2) { + System.err.println("Usage: java PhoneBillDAO "); + return; + } + + String dbFile = args[0]; + String customerName = args[1]; + try (Connection connection = H2DatabaseHelper.createFileBasedConnection(new File(dbFile))) { + PhoneBillDAO dao = new PhoneBillDAO(connection); + PhoneBillDAO.createTable(connection); + + PhoneBill bill = new PhoneBill(customerName); + dao.save(bill); + + PhoneBill retrievedBill = dao.findByCustomer(customerName); + if (retrievedBill != null) { + System.out.println("Retrieved PhoneBill for customer: " + retrievedBill.getCustomer()); + } else { + System.out.println("No PhoneBill found for customer: " + customerName); + } + } + } +} + diff --git a/projects-parent/archetypes-parent/phonebill-archetype/src/main/resources/archetype-resources/src/main/java/Project1.java b/projects-parent/archetypes-parent/phonebill-archetype/src/main/resources/archetype-resources/src/main/java/Project1.java index 13e8c6167..1c5337ddb 100644 --- a/projects-parent/archetypes-parent/phonebill-archetype/src/main/resources/archetype-resources/src/main/java/Project1.java +++ b/projects-parent/archetypes-parent/phonebill-archetype/src/main/resources/archetype-resources/src/main/java/Project1.java @@ -11,7 +11,7 @@ public class Project1 { @VisibleForTesting - static boolean isValidPhoneNumber(String phoneNumber) { + static boolean isValidDateAndTime(String dateAndTime) { return true; } diff --git a/projects-parent/archetypes-parent/phonebill-archetype/src/main/resources/archetype-resources/src/test/java/PhoneBillDAOTest.java b/projects-parent/archetypes-parent/phonebill-archetype/src/main/resources/archetype-resources/src/test/java/PhoneBillDAOTest.java new file mode 100644 index 000000000..a1fdd15c4 --- /dev/null +++ b/projects-parent/archetypes-parent/phonebill-archetype/src/main/resources/archetype-resources/src/test/java/PhoneBillDAOTest.java @@ -0,0 +1,68 @@ +#set( $symbol_pound = '#' ) +#set( $symbol_dollar = '$' ) +#set( $symbol_escape = '\' ) +package ${package}; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.sql.*; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; + +/** + * A simple example unit test that demonstrates persisting a PhoneBill + * to an H2 in-memory database using JDBC. + * + * This is a starting point for students to understand how to use + * Data Access Objects (DAOs) to persist domain objects to a database. + */ +public class PhoneBillDAOTest { + + private Connection connection; + private PhoneBillDAO dao; + + @BeforeEach + public void setUp() throws SQLException { + // Create an in-memory H2 database + connection = DriverManager.getConnection("jdbc:h2:mem:${artifactId}_test"); + + // Create the phone_bills table + PhoneBillDAO.createTable(connection); + + // Create the DAO + dao = new PhoneBillDAO(connection); + } + + @AfterEach + public void tearDown() throws SQLException { + if (connection != null && !connection.isClosed()) { + connection.close(); + } + } + + @Test + public void canPersistAndFetchPhoneBillByCustomerName() throws SQLException { + String customerName = "Jane Doe"; + PhoneBill bill = new PhoneBill(customerName); + + // Persist the phone bill using the DAO + dao.save(bill); + + // Fetch the phone bill by customer name + PhoneBill fetchedBill = dao.findByCustomer(customerName); + + // Validate that the fetched bill matches the original + assertThat(fetchedBill, is(notNullValue())); + assertThat(fetchedBill.getCustomer(), is(equalTo(customerName))); + } + + @Test + public void returnsNullWhenPhoneBillNotFound() throws SQLException { + PhoneBill fetchedBill = dao.findByCustomer("Non-existent Customer"); + assertThat(fetchedBill, is(nullValue())); + } +} +