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/examples/pom.xml b/examples/pom.xml
index f4252b19d..711e9cd06 100644
--- a/examples/pom.xml
+++ b/examples/pom.xml
@@ -8,14 +8,9 @@
4.0.0
examples
examples
- 1.3.5-SNAPSHOT
+ 1.4.0-SNAPSHOT
https://www.cs.pdx.edu/~whitlock
-
- io.github.davidwhitlock.joy
- family
- 1.1.6-SNAPSHOT
-
io.github.davidwhitlock.joy
projects
@@ -42,6 +37,11 @@
4.0.5
runtime
+
+ com.h2database
+ h2
+ ${h2.version}
+
io.github.davidwhitlock.joy
projects
@@ -50,4 +50,18 @@
test
+
+
+
+ org.codehaus.mojo
+ build-helper-maven-plugin
+ ${build-helper-maven-plugin.version}
+
+
+ org.apache.maven.plugins
+ maven-failsafe-plugin
+ ${surefire.version}
+
+
+
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
new file mode 100644
index 000000000..24290e3b2
--- /dev/null
+++ b/examples/src/it/java/edu/pdx/cs/joy/jdbc/DepartmentDAOIT.java
@@ -0,0 +1,101 @@
+package edu.pdx.cs.joy.jdbc;
+
+import org.junit.jupiter.api.*;
+
+import java.io.File;
+import java.sql.Connection;
+import java.sql.SQLException;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.*;
+
+@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
+public class DepartmentDAOIT {
+
+ private static final String TEST_DEPARTMENT_NAME = "Computer Science";
+ private static int generatedDepartmentId;
+
+ private static String dbFilePath;
+ private Connection connection;
+ 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 + "DepartmentDAOIT.db";
+
+ // Connect to the file-based H2 database
+ Connection connection = H2DatabaseHelper.createFileBasedConnection(new File(dbFilePath));
+
+ // Create the departments table
+ DepartmentDAOImpl.createTable(connection);
+
+ connection.close();
+ }
+
+ @BeforeEach
+ public void setUp() throws SQLException {
+ // Connect to the existing database file
+ connection = H2DatabaseHelper.createFileBasedConnection(new File(dbFilePath));
+ departmentDAO = new DepartmentDAOImpl(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 = H2DatabaseHelper.createFileBasedConnection(new File(dbFilePath));
+ DepartmentDAOImpl.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 testPersistDepartment() throws SQLException {
+ // Create and persist a department (ID will be auto-generated)
+ Department department = new Department(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(generatedDepartmentId);
+ assertThat(fetchedDepartment, is(notNullValue()));
+ assertThat(fetchedDepartment.getId(), is(equalTo(generatedDepartmentId)));
+ assertThat(fetchedDepartment.getName(), is(equalTo(TEST_DEPARTMENT_NAME)));
+ }
+
+ @Test
+ @Order(2)
+ public void testFindPersistedDepartment() throws SQLException {
+ // Search for the department that was persisted in the previous test
+ // 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(generatedDepartmentId)));
+ assertThat(fetchedDepartment.getName(), is(equalTo(TEST_DEPARTMENT_NAME)));
+ }
+}
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/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/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..6d2fdadcb
--- /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
+ 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 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/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..6b041ad01
--- /dev/null
+++ b/examples/src/main/java/edu/pdx/cs/joy/jdbc/AcademicTermDAO.java
@@ -0,0 +1,63 @@
+package edu.pdx.cs.joy.jdbc;
+
+import java.sql.SQLException;
+import java.util.List;
+
+/**
+ * Data Access Object interface for managing AcademicTerm entities in the database.
+ */
+public interface AcademicTermDAO {
+
+ /**
+ * 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
+ */
+ void save(AcademicTerm term) throws SQLException;
+
+ /**
+ * 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
+ */
+ AcademicTerm findById(int id) throws SQLException;
+
+ /**
+ * 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
+ */
+ AcademicTerm findByName(String name) throws SQLException;
+
+ /**
+ * Finds all academic terms in the database.
+ *
+ * @return a list of all academic terms
+ * @throws SQLException if a database error occurs
+ */
+ List findAll() throws SQLException;
+
+ /**
+ * 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
+ */
+ void update(AcademicTerm term) throws SQLException;
+
+ /**
+ * 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
+ */
+ 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/Course.java b/examples/src/main/java/edu/pdx/cs/joy/jdbc/Course.java
new file mode 100644
index 000000000..fc673450c
--- /dev/null
+++ b/examples/src/main/java/edu/pdx/cs/joy/jdbc/Course.java
@@ -0,0 +1,136 @@
+package edu.pdx.cs.joy.jdbc;
+
+/**
+ * Represents a course in a college course catalog.
+ * 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;
+
+ /**
+ * 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, int credits) {
+ this.title = title;
+ this.departmentId = departmentId;
+ this.credits = credits;
+ }
+
+ /**
+ * Creates a new Course with no initial values.
+ * Useful for frameworks that use reflection.
+ */
+ 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.
+ *
+ * @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;
+ }
+
+ /**
+ * 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{" +
+ "id=" + id +
+ ", title='" + title + '\'' +
+ ", departmentId=" + departmentId +
+ ", credits=" + credits +
+ '}';
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ 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;
+ }
+
+ @Override
+ public int hashCode() {
+ 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
new file mode 100644
index 000000000..32d05bf60
--- /dev/null
+++ b/examples/src/main/java/edu/pdx/cs/joy/jdbc/CourseDAO.java
@@ -0,0 +1,47 @@
+package edu.pdx.cs.joy.jdbc;
+
+import java.sql.SQLException;
+import java.util.List;
+
+/**
+ * Data Access Object interface for managing Course entities in the database.
+ */
+public interface CourseDAO {
+
+ /**
+ * 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
+ */
+ void save(Course course) throws SQLException;
+
+ /**
+ * 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
+ */
+ Course findByTitle(String title) throws SQLException;
+
+ /**
+ * 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
+ */
+ List findByDepartmentId(int departmentId) throws SQLException;
+
+ /**
+ * 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
+ */
+ 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/Department.java b/examples/src/main/java/edu/pdx/cs/joy/jdbc/Department.java
new file mode 100644
index 000000000..4ccdefd59
--- /dev/null
+++ b/examples/src/main/java/edu/pdx/cs/joy/jdbc/Department.java
@@ -0,0 +1,101 @@
+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 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.
+ */
+ 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..bcdc18f9e
--- /dev/null
+++ b/examples/src/main/java/edu/pdx/cs/joy/jdbc/DepartmentDAO.java
@@ -0,0 +1,62 @@
+package edu.pdx.cs.joy.jdbc;
+
+import java.sql.SQLException;
+import java.util.List;
+
+/**
+ * Data Access Object interface for managing Department entities in the database.
+ */
+public interface DepartmentDAO {
+
+ /**
+ * 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
+ */
+ void save(Department department) throws SQLException;
+
+ /**
+ * 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
+ */
+ Department findById(int id) throws SQLException;
+
+ /**
+ * 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
+ */
+ Department findByName(String name) throws SQLException;
+
+ /**
+ * Finds all departments in the database.
+ *
+ * @return a list of all departments
+ * @throws SQLException if a database error occurs
+ */
+ List findAll() throws SQLException;
+
+ /**
+ * Updates an existing department in the database.
+ *
+ * @param department the department to update
+ * @throws SQLException if a database error occurs
+ */
+ void update(Department department) throws SQLException;
+
+ /**
+ * Deletes a department from the database by ID.
+ *
+ * @param id the ID of the department 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/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/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();
+ }
+}
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..e09501877
--- /dev/null
+++ b/examples/src/main/java/edu/pdx/cs/joy/jdbc/H2DatabaseHelper.java
@@ -0,0 +1,38 @@
+package edu.pdx.cs.joy.jdbc;
+
+import java.io.File;
+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 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(File databaseFilesDirectory) throws SQLException {
+ return DriverManager.getConnection("jdbc:h2:" + databaseFilesDirectory.getAbsolutePath());
+ }
+}
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..ad66e748f
--- /dev/null
+++ b/examples/src/main/java/edu/pdx/cs/joy/jdbc/ManageDepartments.java
@@ -0,0 +1,157 @@
+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 CRUD operations on Department objects
+ * using the DepartmentDAO class with an H2 database.
+ */
+public class ManageDepartments {
+
+ /**
+ * 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 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 < 2) {
+ printUsage();
+ return;
+ }
+
+ String dbFilePath = args[0];
+ String command = args[1].toLowerCase();
+
+ File dbFile = new File(dbFilePath);
+
+ try (Connection connection = H2DatabaseHelper.createFileBasedConnection(dbFile)) {
+ // Create the departments table if it doesn't exist
+ DepartmentDAOImpl.createTable(connection);
+
+ // Create a new DepartmentDAO
+ DepartmentDAO departmentDAO = new DepartmentDAOImpl(connection);
+
+ 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();
+ }
+ }
+ }
+
+ 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");
+ }
+
+ 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;
+ }
+
+ String departmentName = args[2];
+ Department department = new Department(departmentName);
+ departmentDAO.save(department);
+
+ 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);
+ }
+ }
+}
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..9d55b38be
--- /dev/null
+++ b/examples/src/main/java/edu/pdx/cs/joy/jdbc/PrintH2DatabaseSchema.java
@@ -0,0 +1,217 @@
+package edu.pdx.cs.joy.jdbc;
+
+import java.io.File;
+import java.sql.*;
+import java.util.Set;
+
+/**
+ * A command-line program that uses the JDBC DatabaseMetaData API to print
+ * information about the tables in an H2 database file.
+ */
+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.
+ *
+ * @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");
+
+ if (tableNameIsNotInteresting(tableName)) {
+ continue; // Skip system or uninteresting tables
+ }
+
+ 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.");
+ }
+ }
+ }
+
+ private static boolean tableNameIsNotInteresting(String tableName) {
+ return uninterestingTablePrefixes.stream().anyMatch(tableName::startsWith);
+ }
+
+ /**
+ * 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);
+ }
+ }
+}
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");
+ }
+}
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..a5a4b1956
--- /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
+ AcademicTermDAOImpl.dropTable(connection);
+ AcademicTermDAOImpl.createTable(connection);
+
+ // Initialize the DAO with the connection
+ termDAO = new AcademicTermDAOImpl(connection);
+ }
+
+ @AfterEach
+ public void tearDown() throws SQLException {
+ if (connection != null && !connection.isClosed()) {
+ AcademicTermDAOImpl.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))));
+ }
+}
+
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..23c4a75a6
--- /dev/null
+++ b/examples/src/test/java/edu/pdx/cs/joy/jdbc/CourseDAOTest.java
@@ -0,0 +1,202 @@
+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.*;
+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 tables if they exist from a previous test, then create them
+ // Note: Must drop courses first due to foreign key constraint
+ CourseDAOImpl.dropTable(connection);
+ DepartmentDAOImpl.dropTable(connection);
+
+ // Create departments table first, then courses (due to foreign key)
+ DepartmentDAOImpl.createTable(connection);
+ CourseDAOImpl.createTable(connection);
+
+ // Initialize the DAOs with the connection
+ courseDAO = new CourseDAOImpl(connection);
+ departmentDAO = new DepartmentDAOImpl(connection);
+ }
+
+ @AfterEach
+ 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
+ CourseDAOImpl.dropTable(connection);
+ DepartmentDAOImpl.dropTable(connection);
+ connection.close();
+ }
+ }
+
+ @Test
+ public void testPersistAndFetchCourse() throws SQLException {
+ // Create and persist a department first (required for foreign key)
+ Department department = new Department("Computer Science");
+ departmentDAO.save(department);
+
+ // Get the auto-generated department ID
+ int csDepartmentId = department.getId();
+
+ // Create a course
+ String javaCourseName = "Introduction to Java";
+ int credits = 4;
+ Course course = new Course(javaCourseName, csDepartmentId, credits);
+
+ // 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)));
+ }
+
+ @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 and persist departments first (required for foreign key)
+ Department csDepartment = new Department("Computer Science");
+ Department mathDepartment = new Department("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";
+ String calculusName = "Calculus";
+
+ 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);
+ 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))));
+ }
+
+ @Test
+ public void testForeignKeyConstraintPreventsInvalidDepartmentId() {
+ // Try to create a course with a non-existent department ID
+ 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, () -> {
+ courseDAO.save(course);
+ });
+ 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)));
+ }
+
+ @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()));
+ }
+
+}
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..abd3b6ad9
--- /dev/null
+++ b/examples/src/test/java/edu/pdx/cs/joy/jdbc/DepartmentDAOTest.java
@@ -0,0 +1,140 @@
+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
+ DepartmentDAOImpl.dropTable(connection);
+ DepartmentDAOImpl.createTable(connection);
+
+ // Initialize the DAO with the connection
+ departmentDAO = new DepartmentDAOImpl(connection);
+ }
+
+ @AfterEach
+ public void tearDown() throws SQLException {
+ if (connection != null && !connection.isClosed()) {
+ // Drop the table and close the connection
+ DepartmentDAOImpl.dropTable(connection);
+ connection.close();
+ }
+ }
+
+ @Test
+ public void testPersistAndFetchDepartmentById() throws SQLException {
+ // Create a department (ID will be auto-generated)
+ Department department = new Department("Computer Science");
+
+ // Persist the department
+ departmentDAO.save(department);
+
+ // 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(department.getId())));
+ assertThat(fetchedDepartment.getName(), is(equalTo("Computer Science")));
+ }
+
+ @Test
+ public void testFindDepartmentByName() throws SQLException {
+ // Create and persist a department (ID will be auto-generated)
+ Department department = new Department("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(department.getId())));
+ 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 (IDs will be auto-generated)
+ Department dept1 = new Department("Computer Science");
+ Department dept2 = new Department("Mathematics");
+ Department dept3 = new Department("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(dept1.getId()))));
+ assertThat(allDepartments, hasItem(hasProperty("id", is(dept2.getId()))));
+ assertThat(allDepartments, hasItem(hasProperty("id", is(dept3.getId()))));
+ }
+
+ @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 (ID will be auto-generated)
+ Department original = new Department("Engineering");
+ departmentDAO.save(original);
+
+ // 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)));
+ }
+}
diff --git a/family/pom.xml b/family/pom.xml
index 6d99c0f68..863fb3823 100644
--- a/family/pom.xml
+++ b/family/pom.xml
@@ -18,6 +18,16 @@
projects
3.0.4-SNAPSHOT
+
+ io.github.davidwhitlock.joy
+ examples
+ 1.4.0-SNAPSHOT
+
+
+ com.h2database
+ h2
+ ${h2.version}
+
diff --git a/family/src/main/java/edu/pdx/cs/joy/family/FamilyTreeDAO.java b/family/src/main/java/edu/pdx/cs/joy/family/FamilyTreeDAO.java
new file mode 100644
index 000000000..0d55f9b82
--- /dev/null
+++ b/family/src/main/java/edu/pdx/cs/joy/family/FamilyTreeDAO.java
@@ -0,0 +1,49 @@
+package edu.pdx.cs.joy.family;
+
+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/family/src/main/java/edu/pdx/cs/joy/family/FamilyTreeDAOImpl.java b/family/src/main/java/edu/pdx/cs/joy/family/FamilyTreeDAOImpl.java
new file mode 100644
index 000000000..35de309ee
--- /dev/null
+++ b/family/src/main/java/edu/pdx/cs/joy/family/FamilyTreeDAOImpl.java
@@ -0,0 +1,189 @@
+package edu.pdx.cs.joy.family;
+
+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/family/src/main/java/edu/pdx/cs/joy/family/MarriageDAO.java b/family/src/main/java/edu/pdx/cs/joy/family/MarriageDAO.java
new file mode 100644
index 000000000..4e6da9816
--- /dev/null
+++ b/family/src/main/java/edu/pdx/cs/joy/family/MarriageDAO.java
@@ -0,0 +1,66 @@
+package edu.pdx.cs.joy.family;
+
+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/family/src/main/java/edu/pdx/cs/joy/family/MarriageDAOImpl.java b/family/src/main/java/edu/pdx/cs/joy/family/MarriageDAOImpl.java
new file mode 100644
index 000000000..cd6c9b3f4
--- /dev/null
+++ b/family/src/main/java/edu/pdx/cs/joy/family/MarriageDAOImpl.java
@@ -0,0 +1,190 @@
+package edu.pdx.cs.joy.family;
+
+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/family/src/main/java/edu/pdx/cs/joy/family/PersonDAO.java b/family/src/main/java/edu/pdx/cs/joy/family/PersonDAO.java
new file mode 100644
index 000000000..53fb8e536
--- /dev/null
+++ b/family/src/main/java/edu/pdx/cs/joy/family/PersonDAO.java
@@ -0,0 +1,73 @@
+package edu.pdx.cs.joy.family;
+
+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/family/src/main/java/edu/pdx/cs/joy/family/PersonDAOImpl.java b/family/src/main/java/edu/pdx/cs/joy/family/PersonDAOImpl.java
new file mode 100644
index 000000000..58e3dcb77
--- /dev/null
+++ b/family/src/main/java/edu/pdx/cs/joy/family/PersonDAOImpl.java
@@ -0,0 +1,290 @@
+package edu.pdx.cs.joy.family;
+
+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/family/src/test/java/edu/pdx/cs/joy/family/FamilyTreeDAOTest.java b/family/src/test/java/edu/pdx/cs/joy/family/FamilyTreeDAOTest.java
new file mode 100644
index 000000000..94218e6ff
--- /dev/null
+++ b/family/src/test/java/edu/pdx/cs/joy/family/FamilyTreeDAOTest.java
@@ -0,0 +1,229 @@
+package edu.pdx.cs.joy.family;
+
+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;
+
+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();
+ }
+}
+
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
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
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-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/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/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/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()));
+ }
+}
+
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/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/phonebill/pom.xml b/projects-parent/originals-parent/phonebill/pom.xml
index ee5222080..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
@@ -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/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..97ff802e2
--- /dev/null
+++ b/projects-parent/originals-parent/phonebill/src/main/java/edu/pdx/cs/joy/phonebill/PhoneBillDAO.java
@@ -0,0 +1,108 @@
+package edu.pdx.cs.joy.phonebill;
+
+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/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/PhoneBillDAOTest.java b/projects-parent/originals-parent/phonebill/src/test/java/edu/pdx/cs/joy/phonebill/PhoneBillDAOTest.java
new file mode 100644
index 000000000..eff5e73e8
--- /dev/null
+++ b/projects-parent/originals-parent/phonebill/src/test/java/edu/pdx/cs/joy/phonebill/PhoneBillDAOTest.java
@@ -0,0 +1,65 @@
+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;
+ private PhoneBillDAO dao;
+
+ @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
+ 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()));
+ }
+}
+
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
-
-
-
-
-
-
-
-
-
-
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 23b2ca8f0..23425e900 100644
--- a/web/pom.xml
+++ b/web/pom.xml
@@ -117,7 +117,12 @@
io.github.davidwhitlock.joy
examples
- 1.3.5-SNAPSHOT
+ 1.4.0-SNAPSHOT
+
+
+ io.github.davidwhitlock.joy
+ family
+ 1.1.6-SNAPSHOT
jakarta.xml.bind