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