From 941f55759367942feafb1def73dddb1223d9f26d Mon Sep 17 00:00:00 2001 From: David Whitlock Date: Sun, 20 Jul 2025 06:09:16 -0700 Subject: [PATCH 01/77] For issues #478, break ground on a tool that will identify project submissions that have no yet been graded. --- grader/pom.xml | 2 +- .../joy/grader/FindUngradedSubmissions.java | 49 +++++++++++++++++++ .../grader/FindUngradedSubmissionsTest.java | 45 +++++++++++++++++ 3 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 grader/src/main/java/edu/pdx/cs/joy/grader/FindUngradedSubmissions.java create mode 100644 grader/src/test/java/edu/pdx/cs/joy/grader/FindUngradedSubmissionsTest.java diff --git a/grader/pom.xml b/grader/pom.xml index d82dc0da5..77a5fd72b 100644 --- a/grader/pom.xml +++ b/grader/pom.xml @@ -7,7 +7,7 @@ 4.0.0 grader grader - ${grader.version} + 1.5.0-SNAPSHOT jar https://www.cs.pdx.edu/~whitlock diff --git a/grader/src/main/java/edu/pdx/cs/joy/grader/FindUngradedSubmissions.java b/grader/src/main/java/edu/pdx/cs/joy/grader/FindUngradedSubmissions.java new file mode 100644 index 000000000..fd410873f --- /dev/null +++ b/grader/src/main/java/edu/pdx/cs/joy/grader/FindUngradedSubmissions.java @@ -0,0 +1,49 @@ +package edu.pdx.cs.joy.grader; + +import com.google.common.annotations.VisibleForTesting; + +import java.nio.file.Files; +import java.nio.file.Path; + +public class FindUngradedSubmissions { + private final SubmissionDetailsProvider submissionDetailsProvider; + private final TestOutputProvider testOutputProvider; + + @VisibleForTesting + FindUngradedSubmissions(SubmissionDetailsProvider submissionDetailsProvider, TestOutputProvider testOutputProvider) { + this.submissionDetailsProvider = submissionDetailsProvider; + this.testOutputProvider = testOutputProvider; + } + + + public boolean isGraded(Path submission) { + SubmissionDetails details = this.submissionDetailsProvider.getSubmissionDetails(submission); + Path testOutput = this.testOutputProvider.getTestOutput(details.getStudentId()); + if (!Files.exists(testOutput)) { + return false; + } + throw new UnsupportedOperationException("Not implemented yet: isGraded(Path submission)"); + } + + @VisibleForTesting + static class SubmissionDetails { + + private final String studentId; + + public SubmissionDetails(String studentId) { + this.studentId = studentId; + } + + public String getStudentId() { + return this.studentId; + } + } + + interface SubmissionDetailsProvider { + SubmissionDetails getSubmissionDetails(Path submission); + } + + interface TestOutputProvider { + Path getTestOutput(String studentId); + } +} diff --git a/grader/src/test/java/edu/pdx/cs/joy/grader/FindUngradedSubmissionsTest.java b/grader/src/test/java/edu/pdx/cs/joy/grader/FindUngradedSubmissionsTest.java new file mode 100644 index 000000000..8f0d4a896 --- /dev/null +++ b/grader/src/test/java/edu/pdx/cs/joy/grader/FindUngradedSubmissionsTest.java @@ -0,0 +1,45 @@ +package edu.pdx.cs.joy.grader; + +import org.junit.jupiter.api.Test; + +import java.nio.file.FileSystem; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.spi.FileSystemProvider; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class FindUngradedSubmissionsTest { + + @Test + void submissionWithNoTestOutputIsGraded() { + FindUngradedSubmissions.SubmissionDetailsProvider submissionDetailsProvider = mock(FindUngradedSubmissions.SubmissionDetailsProvider.class); + + String studentId = "student123"; + FindUngradedSubmissions.SubmissionDetails submissionDetails = new FindUngradedSubmissions.SubmissionDetails(studentId); + Path submission = mock(Path.class); + when(submissionDetailsProvider.getSubmissionDetails(submission)).thenReturn(submissionDetails); + + Path testOutput = getPathToNonExistingFile(); + + FindUngradedSubmissions.TestOutputProvider testOutputProvider = mock(FindUngradedSubmissions.TestOutputProvider.class); + when(testOutputProvider.getTestOutput(studentId)).thenReturn(testOutput); + + FindUngradedSubmissions finder = new FindUngradedSubmissions(submissionDetailsProvider, testOutputProvider); + assertThat(finder.isGraded(submission), equalTo(false)); + } + + private static Path getPathToNonExistingFile() { + Path testOutput = mock(Path.class); + FileSystemProvider provider = mock(FileSystemProvider.class); + when(provider.exists(testOutput)).thenReturn(false); + FileSystem fileSystem = mock(FileSystem.class); + when(fileSystem.provider()).thenReturn(provider); + when(testOutput.getFileSystem()).thenReturn(fileSystem); + assertThat(Files.exists(testOutput), equalTo(false)); + return testOutput; + } +} From fa539c7e807607174d6e86b2168fc7aa7c4e229a Mon Sep 17 00:00:00 2001 From: David Whitlock Date: Wed, 30 Jul 2025 19:25:50 -0700 Subject: [PATCH 02/77] A submission made in after the previous grade needs to be graded. --- .../joy/grader/FindUngradedSubmissions.java | 49 +++++++++++++--- .../grader/FindUngradedSubmissionsTest.java | 58 ++++++++++++++----- 2 files changed, 86 insertions(+), 21 deletions(-) diff --git a/grader/src/main/java/edu/pdx/cs/joy/grader/FindUngradedSubmissions.java b/grader/src/main/java/edu/pdx/cs/joy/grader/FindUngradedSubmissions.java index fd410873f..8ab0f751a 100644 --- a/grader/src/main/java/edu/pdx/cs/joy/grader/FindUngradedSubmissions.java +++ b/grader/src/main/java/edu/pdx/cs/joy/grader/FindUngradedSubmissions.java @@ -4,46 +4,79 @@ import java.nio.file.Files; import java.nio.file.Path; +import java.time.ZonedDateTime; public class FindUngradedSubmissions { private final SubmissionDetailsProvider submissionDetailsProvider; - private final TestOutputProvider testOutputProvider; + private final TestOutputPathProvider testOutputProvider; + private final TestOutputDetailsProvider testOutputDetailsProvider; @VisibleForTesting - FindUngradedSubmissions(SubmissionDetailsProvider submissionDetailsProvider, TestOutputProvider testOutputProvider) { + FindUngradedSubmissions(SubmissionDetailsProvider submissionDetailsProvider, TestOutputPathProvider testOutputProvider, TestOutputDetailsProvider testOutputDetailsProvider) { this.submissionDetailsProvider = submissionDetailsProvider; this.testOutputProvider = testOutputProvider; + this.testOutputDetailsProvider = testOutputDetailsProvider; } - public boolean isGraded(Path submission) { - SubmissionDetails details = this.submissionDetailsProvider.getSubmissionDetails(submission); - Path testOutput = this.testOutputProvider.getTestOutput(details.getStudentId()); + public boolean isGraded(Path submissionPath) { + SubmissionDetails submission = this.submissionDetailsProvider.getSubmissionDetails(submissionPath); + Path testOutput = this.testOutputProvider.getTestOutput(submission.getStudentId()); if (!Files.exists(testOutput)) { return false; } - throw new UnsupportedOperationException("Not implemented yet: isGraded(Path submission)"); + + TestOutputDetails testOutputDetails = this.testOutputDetailsProvider.getTestOutputDetails(testOutput); + if (submission.getSubmissionTime().isAfter(testOutputDetails.getGradedTime())) { + return false; + } + + throw new UnsupportedOperationException("Grading logic not implemented yet"); } @VisibleForTesting static class SubmissionDetails { private final String studentId; + private final ZonedDateTime submissionTime; - public SubmissionDetails(String studentId) { + public SubmissionDetails(String studentId, ZonedDateTime submissionTime) { this.studentId = studentId; + this.submissionTime = submissionTime; } public String getStudentId() { return this.studentId; } + + public ZonedDateTime getSubmissionTime() { + return this.submissionTime; + } } interface SubmissionDetailsProvider { SubmissionDetails getSubmissionDetails(Path submission); } - interface TestOutputProvider { + interface TestOutputPathProvider { Path getTestOutput(String studentId); } + + interface TestOutputDetailsProvider { + TestOutputDetails getTestOutputDetails(Path testOutput); + } + + @VisibleForTesting + static + class TestOutputDetails { + private final ZonedDateTime gradedTime; + + public TestOutputDetails(ZonedDateTime gradedTime) { + this.gradedTime = gradedTime; + } + + public ZonedDateTime getGradedTime() { + return this.gradedTime; + } + } } diff --git a/grader/src/test/java/edu/pdx/cs/joy/grader/FindUngradedSubmissionsTest.java b/grader/src/test/java/edu/pdx/cs/joy/grader/FindUngradedSubmissionsTest.java index 8f0d4a896..a50fd33c6 100644 --- a/grader/src/test/java/edu/pdx/cs/joy/grader/FindUngradedSubmissionsTest.java +++ b/grader/src/test/java/edu/pdx/cs/joy/grader/FindUngradedSubmissionsTest.java @@ -6,6 +6,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.spi.FileSystemProvider; +import java.time.ZonedDateTime; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; @@ -14,32 +15,63 @@ public class FindUngradedSubmissionsTest { + private static Path getPathToNonExistingFile() { + return getMockPath(false); + } + + private Path getPathToExistingFile() { + return getMockPath(true); + } + + private static Path getMockPath(boolean exists) { + Path testOutput = mock(Path.class); + FileSystemProvider provider = mock(FileSystemProvider.class); + when(provider.exists(testOutput)).thenReturn(exists); + FileSystem fileSystem = mock(FileSystem.class); + when(fileSystem.provider()).thenReturn(provider); + when(testOutput.getFileSystem()).thenReturn(fileSystem); + assertThat(Files.exists(testOutput), equalTo(exists)); + return testOutput; + } + @Test - void submissionWithNoTestOutputIsGraded() { + void submissionWithNoTestOutputIsNotGraded() { FindUngradedSubmissions.SubmissionDetailsProvider submissionDetailsProvider = mock(FindUngradedSubmissions.SubmissionDetailsProvider.class); String studentId = "student123"; - FindUngradedSubmissions.SubmissionDetails submissionDetails = new FindUngradedSubmissions.SubmissionDetails(studentId); + FindUngradedSubmissions.SubmissionDetails submissionDetails = new FindUngradedSubmissions.SubmissionDetails(studentId, ZonedDateTime.now()); Path submission = mock(Path.class); when(submissionDetailsProvider.getSubmissionDetails(submission)).thenReturn(submissionDetails); Path testOutput = getPathToNonExistingFile(); - FindUngradedSubmissions.TestOutputProvider testOutputProvider = mock(FindUngradedSubmissions.TestOutputProvider.class); + FindUngradedSubmissions.TestOutputPathProvider testOutputProvider = mock(FindUngradedSubmissions.TestOutputPathProvider.class); when(testOutputProvider.getTestOutput(studentId)).thenReturn(testOutput); - FindUngradedSubmissions finder = new FindUngradedSubmissions(submissionDetailsProvider, testOutputProvider); + FindUngradedSubmissions finder = new FindUngradedSubmissions(submissionDetailsProvider, testOutputProvider, mock(FindUngradedSubmissions.TestOutputDetailsProvider.class)); assertThat(finder.isGraded(submission), equalTo(false)); } - private static Path getPathToNonExistingFile() { - Path testOutput = mock(Path.class); - FileSystemProvider provider = mock(FileSystemProvider.class); - when(provider.exists(testOutput)).thenReturn(false); - FileSystem fileSystem = mock(FileSystem.class); - when(fileSystem.provider()).thenReturn(provider); - when(testOutput.getFileSystem()).thenReturn(fileSystem); - assertThat(Files.exists(testOutput), equalTo(false)); - return testOutput; + @Test + void submissionWithTestOutputOlderThanSubmissionIsNotGraded() { + FindUngradedSubmissions.SubmissionDetailsProvider submissionDetailsProvider = mock(FindUngradedSubmissions.SubmissionDetailsProvider.class); + + String studentId = "student123"; + ZonedDateTime submissionTime = ZonedDateTime.now(); + FindUngradedSubmissions.SubmissionDetails submissionDetails = new FindUngradedSubmissions.SubmissionDetails(studentId, submissionTime); + Path submission = mock(Path.class); + when(submissionDetailsProvider.getSubmissionDetails(submission)).thenReturn(submissionDetails); + + Path testOutput = getPathToExistingFile(); + + FindUngradedSubmissions.TestOutputPathProvider testOutputProvider = mock(FindUngradedSubmissions.TestOutputPathProvider.class); + when(testOutputProvider.getTestOutput(studentId)).thenReturn(testOutput); + + FindUngradedSubmissions.TestOutputDetailsProvider testOutputDetailsProvider = mock(FindUngradedSubmissions.TestOutputDetailsProvider.class); + ZonedDateTime gradedTime = submissionTime.minusDays(1); // Simulate test output older than submission + when(testOutputDetailsProvider.getTestOutputDetails(testOutput)).thenReturn(new FindUngradedSubmissions.TestOutputDetails(gradedTime)); + + FindUngradedSubmissions finder = new FindUngradedSubmissions(submissionDetailsProvider, testOutputProvider, testOutputDetailsProvider); + assertThat(finder.isGraded(submission), equalTo(false)); } } From 4a02446f6b0183fa47dc830f5ae79a5ec86a4527 Mon Sep 17 00:00:00 2001 From: David Whitlock Date: Wed, 30 Jul 2025 19:33:14 -0700 Subject: [PATCH 03/77] Test output with a grade needs to be graded. --- .../joy/grader/FindUngradedSubmissions.java | 21 +++++++++++----- .../grader/FindUngradedSubmissionsTest.java | 25 ++++++++++++++++++- 2 files changed, 39 insertions(+), 7 deletions(-) diff --git a/grader/src/main/java/edu/pdx/cs/joy/grader/FindUngradedSubmissions.java b/grader/src/main/java/edu/pdx/cs/joy/grader/FindUngradedSubmissions.java index 8ab0f751a..ccb4c1cf4 100644 --- a/grader/src/main/java/edu/pdx/cs/joy/grader/FindUngradedSubmissions.java +++ b/grader/src/main/java/edu/pdx/cs/joy/grader/FindUngradedSubmissions.java @@ -27,7 +27,10 @@ public boolean isGraded(Path submissionPath) { } TestOutputDetails testOutputDetails = this.testOutputDetailsProvider.getTestOutputDetails(testOutput); - if (submission.getSubmissionTime().isAfter(testOutputDetails.getGradedTime())) { + if (!testOutputDetails.hasGrade()) { + return false; + + } else if (submission.getSubmissionTime().isAfter(testOutputDetails.getTestedTime())) { return false; } @@ -69,14 +72,20 @@ interface TestOutputDetailsProvider { @VisibleForTesting static class TestOutputDetails { - private final ZonedDateTime gradedTime; + private final ZonedDateTime testedTime; + private final boolean hasGrade; + + public TestOutputDetails(ZonedDateTime testedTime, boolean hasGrade) { + this.testedTime = testedTime; + this.hasGrade = hasGrade; + } - public TestOutputDetails(ZonedDateTime gradedTime) { - this.gradedTime = gradedTime; + public ZonedDateTime getTestedTime() { + return this.testedTime; } - public ZonedDateTime getGradedTime() { - return this.gradedTime; + public boolean hasGrade() { + return this.hasGrade; } } } diff --git a/grader/src/test/java/edu/pdx/cs/joy/grader/FindUngradedSubmissionsTest.java b/grader/src/test/java/edu/pdx/cs/joy/grader/FindUngradedSubmissionsTest.java index a50fd33c6..35ba9dbe7 100644 --- a/grader/src/test/java/edu/pdx/cs/joy/grader/FindUngradedSubmissionsTest.java +++ b/grader/src/test/java/edu/pdx/cs/joy/grader/FindUngradedSubmissionsTest.java @@ -69,7 +69,30 @@ void submissionWithTestOutputOlderThanSubmissionIsNotGraded() { FindUngradedSubmissions.TestOutputDetailsProvider testOutputDetailsProvider = mock(FindUngradedSubmissions.TestOutputDetailsProvider.class); ZonedDateTime gradedTime = submissionTime.minusDays(1); // Simulate test output older than submission - when(testOutputDetailsProvider.getTestOutputDetails(testOutput)).thenReturn(new FindUngradedSubmissions.TestOutputDetails(gradedTime)); + when(testOutputDetailsProvider.getTestOutputDetails(testOutput)).thenReturn(new FindUngradedSubmissions.TestOutputDetails(gradedTime, true)); + + FindUngradedSubmissions finder = new FindUngradedSubmissions(submissionDetailsProvider, testOutputProvider, testOutputDetailsProvider); + assertThat(finder.isGraded(submission), equalTo(false)); + } + + @Test + void submissionWithNoGradeNeedsToBeGraded() { + FindUngradedSubmissions.SubmissionDetailsProvider submissionDetailsProvider = mock(FindUngradedSubmissions.SubmissionDetailsProvider.class); + + String studentId = "student123"; + ZonedDateTime submissionTime = ZonedDateTime.now(); + FindUngradedSubmissions.SubmissionDetails submissionDetails = new FindUngradedSubmissions.SubmissionDetails(studentId, submissionTime); + Path submission = mock(Path.class); + when(submissionDetailsProvider.getSubmissionDetails(submission)).thenReturn(submissionDetails); + + Path testOutput = getPathToExistingFile(); + + FindUngradedSubmissions.TestOutputPathProvider testOutputProvider = mock(FindUngradedSubmissions.TestOutputPathProvider.class); + when(testOutputProvider.getTestOutput(studentId)).thenReturn(testOutput); + + FindUngradedSubmissions.TestOutputDetailsProvider testOutputDetailsProvider = mock(FindUngradedSubmissions.TestOutputDetailsProvider.class); + ZonedDateTime gradedTime = submissionTime.plusDays(1); // Simulate test output newer than submission + when(testOutputDetailsProvider.getTestOutputDetails(testOutput)).thenReturn(new FindUngradedSubmissions.TestOutputDetails(gradedTime, false)); FindUngradedSubmissions finder = new FindUngradedSubmissions(submissionDetailsProvider, testOutputProvider, testOutputDetailsProvider); assertThat(finder.isGraded(submission), equalTo(false)); From c4a45d25c2075d158d266de53770834e41b5b137 Mon Sep 17 00:00:00 2001 From: David Whitlock Date: Wed, 30 Jul 2025 19:37:52 -0700 Subject: [PATCH 04/77] Use records instead of classes for JavaBeans. --- .../joy/grader/FindUngradedSubmissions.java | 39 ++----------------- 1 file changed, 4 insertions(+), 35 deletions(-) diff --git a/grader/src/main/java/edu/pdx/cs/joy/grader/FindUngradedSubmissions.java b/grader/src/main/java/edu/pdx/cs/joy/grader/FindUngradedSubmissions.java index ccb4c1cf4..581c1e3d0 100644 --- a/grader/src/main/java/edu/pdx/cs/joy/grader/FindUngradedSubmissions.java +++ b/grader/src/main/java/edu/pdx/cs/joy/grader/FindUngradedSubmissions.java @@ -21,7 +21,7 @@ public class FindUngradedSubmissions { public boolean isGraded(Path submissionPath) { SubmissionDetails submission = this.submissionDetailsProvider.getSubmissionDetails(submissionPath); - Path testOutput = this.testOutputProvider.getTestOutput(submission.getStudentId()); + Path testOutput = this.testOutputProvider.getTestOutput(submission.studentId()); if (!Files.exists(testOutput)) { return false; } @@ -30,7 +30,7 @@ public boolean isGraded(Path submissionPath) { if (!testOutputDetails.hasGrade()) { return false; - } else if (submission.getSubmissionTime().isAfter(testOutputDetails.getTestedTime())) { + } else if (submission.submissionTime().isAfter(testOutputDetails.testedTime())) { return false; } @@ -38,23 +38,8 @@ public boolean isGraded(Path submissionPath) { } @VisibleForTesting - static class SubmissionDetails { + record SubmissionDetails(String studentId, ZonedDateTime submissionTime) { - private final String studentId; - private final ZonedDateTime submissionTime; - - public SubmissionDetails(String studentId, ZonedDateTime submissionTime) { - this.studentId = studentId; - this.submissionTime = submissionTime; - } - - public String getStudentId() { - return this.studentId; - } - - public ZonedDateTime getSubmissionTime() { - return this.submissionTime; - } } interface SubmissionDetailsProvider { @@ -70,22 +55,6 @@ interface TestOutputDetailsProvider { } @VisibleForTesting - static - class TestOutputDetails { - private final ZonedDateTime testedTime; - private final boolean hasGrade; - - public TestOutputDetails(ZonedDateTime testedTime, boolean hasGrade) { - this.testedTime = testedTime; - this.hasGrade = hasGrade; - } - - public ZonedDateTime getTestedTime() { - return this.testedTime; - } - - public boolean hasGrade() { - return this.hasGrade; - } + record TestOutputDetails(ZonedDateTime testedTime, boolean hasGrade) { } } From 6e9bd41c795cdd4c1e0b9b96e1d6a575bb25a534 Mon Sep 17 00:00:00 2001 From: David Whitlock Date: Wed, 30 Jul 2025 19:56:32 -0700 Subject: [PATCH 05/77] A submission that was graded after it was submitted graded. --- .../joy/grader/FindUngradedSubmissions.java | 12 +++++----- .../grader/FindUngradedSubmissionsTest.java | 23 +++++++++++++++++++ 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/grader/src/main/java/edu/pdx/cs/joy/grader/FindUngradedSubmissions.java b/grader/src/main/java/edu/pdx/cs/joy/grader/FindUngradedSubmissions.java index 581c1e3d0..10b2faa18 100644 --- a/grader/src/main/java/edu/pdx/cs/joy/grader/FindUngradedSubmissions.java +++ b/grader/src/main/java/edu/pdx/cs/joy/grader/FindUngradedSubmissions.java @@ -21,20 +21,20 @@ public class FindUngradedSubmissions { public boolean isGraded(Path submissionPath) { SubmissionDetails submission = this.submissionDetailsProvider.getSubmissionDetails(submissionPath); - Path testOutput = this.testOutputProvider.getTestOutput(submission.studentId()); - if (!Files.exists(testOutput)) { + Path testOutputPath = this.testOutputProvider.getTestOutput(submission.studentId()); + if (!Files.exists(testOutputPath)) { return false; } - TestOutputDetails testOutputDetails = this.testOutputDetailsProvider.getTestOutputDetails(testOutput); - if (!testOutputDetails.hasGrade()) { + TestOutputDetails testOutput = this.testOutputDetailsProvider.getTestOutputDetails(testOutputPath); + if (!testOutput.hasGrade()) { return false; - } else if (submission.submissionTime().isAfter(testOutputDetails.testedTime())) { + } else if (submission.submissionTime().isAfter(testOutput.testedTime())) { return false; } - throw new UnsupportedOperationException("Grading logic not implemented yet"); + return true; } @VisibleForTesting diff --git a/grader/src/test/java/edu/pdx/cs/joy/grader/FindUngradedSubmissionsTest.java b/grader/src/test/java/edu/pdx/cs/joy/grader/FindUngradedSubmissionsTest.java index 35ba9dbe7..e10b28935 100644 --- a/grader/src/test/java/edu/pdx/cs/joy/grader/FindUngradedSubmissionsTest.java +++ b/grader/src/test/java/edu/pdx/cs/joy/grader/FindUngradedSubmissionsTest.java @@ -97,4 +97,27 @@ void submissionWithNoGradeNeedsToBeGraded() { FindUngradedSubmissions finder = new FindUngradedSubmissions(submissionDetailsProvider, testOutputProvider, testOutputDetailsProvider); assertThat(finder.isGraded(submission), equalTo(false)); } + + @Test + void submissionWithGradeIsGraded() { + FindUngradedSubmissions.SubmissionDetailsProvider submissionDetailsProvider = mock(FindUngradedSubmissions.SubmissionDetailsProvider.class); + + String studentId = "student123"; + ZonedDateTime submissionTime = ZonedDateTime.now(); + FindUngradedSubmissions.SubmissionDetails submissionDetails = new FindUngradedSubmissions.SubmissionDetails(studentId, submissionTime); + Path submission = mock(Path.class); + when(submissionDetailsProvider.getSubmissionDetails(submission)).thenReturn(submissionDetails); + + Path testOutput = getPathToExistingFile(); + + FindUngradedSubmissions.TestOutputPathProvider testOutputProvider = mock(FindUngradedSubmissions.TestOutputPathProvider.class); + when(testOutputProvider.getTestOutput(studentId)).thenReturn(testOutput); + + FindUngradedSubmissions.TestOutputDetailsProvider testOutputDetailsProvider = mock(FindUngradedSubmissions.TestOutputDetailsProvider.class); + ZonedDateTime gradedTime = submissionTime.plusDays(1); + when(testOutputDetailsProvider.getTestOutputDetails(testOutput)).thenReturn(new FindUngradedSubmissions.TestOutputDetails(gradedTime, true)); + + FindUngradedSubmissions finder = new FindUngradedSubmissions(submissionDetailsProvider, testOutputProvider, testOutputDetailsProvider); + assertThat(finder.isGraded(submission), equalTo(true)); + } } From 8891f0cf47826a6a54c5449dec02f54651b5207c Mon Sep 17 00:00:00 2001 From: David Whitlock Date: Wed, 30 Jul 2025 21:48:51 -0700 Subject: [PATCH 06/77] Provide more nuance about what about what to do with a submission (test it? grade it?). --- .../joy/grader/FindUngradedSubmissions.java | 36 ++++++++++++++----- .../grader/FindUngradedSubmissionsTest.java | 20 +++++++---- 2 files changed, 41 insertions(+), 15 deletions(-) diff --git a/grader/src/main/java/edu/pdx/cs/joy/grader/FindUngradedSubmissions.java b/grader/src/main/java/edu/pdx/cs/joy/grader/FindUngradedSubmissions.java index 10b2faa18..d210884dc 100644 --- a/grader/src/main/java/edu/pdx/cs/joy/grader/FindUngradedSubmissions.java +++ b/grader/src/main/java/edu/pdx/cs/joy/grader/FindUngradedSubmissions.java @@ -19,22 +19,35 @@ public class FindUngradedSubmissions { } - public boolean isGraded(Path submissionPath) { + @VisibleForTesting + SubmissionAnalysis analyzeSubmission(Path submissionPath) { SubmissionDetails submission = this.submissionDetailsProvider.getSubmissionDetails(submissionPath); Path testOutputPath = this.testOutputProvider.getTestOutput(submission.studentId()); + boolean needsToBeTested; + boolean needsToBeGraded; + if (!Files.exists(testOutputPath)) { - return false; - } + needsToBeTested = true; + needsToBeGraded = true; + + } else { + + TestOutputDetails testOutput = this.testOutputDetailsProvider.getTestOutputDetails(testOutputPath); + if (submission.submissionTime().isAfter(testOutput.testedTime())) { + needsToBeTested = true; + needsToBeGraded = true; - TestOutputDetails testOutput = this.testOutputDetailsProvider.getTestOutputDetails(testOutputPath); - if (!testOutput.hasGrade()) { - return false; + } else if (!testOutput.hasGrade()) { + needsToBeTested = false; + needsToBeGraded = true; - } else if (submission.submissionTime().isAfter(testOutput.testedTime())) { - return false; + } else { + needsToBeTested = false; + needsToBeGraded = false; + } } - return true; + return new SubmissionAnalysis(needsToBeTested, needsToBeGraded); } @VisibleForTesting @@ -57,4 +70,9 @@ interface TestOutputDetailsProvider { @VisibleForTesting record TestOutputDetails(ZonedDateTime testedTime, boolean hasGrade) { } + + @VisibleForTesting + record SubmissionAnalysis (boolean needsToBeTested, boolean needsToBeGraded) { + + } } diff --git a/grader/src/test/java/edu/pdx/cs/joy/grader/FindUngradedSubmissionsTest.java b/grader/src/test/java/edu/pdx/cs/joy/grader/FindUngradedSubmissionsTest.java index e10b28935..197532267 100644 --- a/grader/src/test/java/edu/pdx/cs/joy/grader/FindUngradedSubmissionsTest.java +++ b/grader/src/test/java/edu/pdx/cs/joy/grader/FindUngradedSubmissionsTest.java @@ -35,7 +35,7 @@ private static Path getMockPath(boolean exists) { } @Test - void submissionWithNoTestOutputIsNotGraded() { + void submissionWithNoTestOutputNeedsToBeTested() { FindUngradedSubmissions.SubmissionDetailsProvider submissionDetailsProvider = mock(FindUngradedSubmissions.SubmissionDetailsProvider.class); String studentId = "student123"; @@ -49,11 +49,13 @@ void submissionWithNoTestOutputIsNotGraded() { when(testOutputProvider.getTestOutput(studentId)).thenReturn(testOutput); FindUngradedSubmissions finder = new FindUngradedSubmissions(submissionDetailsProvider, testOutputProvider, mock(FindUngradedSubmissions.TestOutputDetailsProvider.class)); - assertThat(finder.isGraded(submission), equalTo(false)); + FindUngradedSubmissions.SubmissionAnalysis analysis = finder.analyzeSubmission(submission); + assertThat(analysis.needsToBeTested(), equalTo(true)); + assertThat(analysis.needsToBeGraded(), equalTo(true)); } @Test - void submissionWithTestOutputOlderThanSubmissionIsNotGraded() { + void submissionWithTestOutputOlderThanSubmissionNeedsToBeTested() { FindUngradedSubmissions.SubmissionDetailsProvider submissionDetailsProvider = mock(FindUngradedSubmissions.SubmissionDetailsProvider.class); String studentId = "student123"; @@ -72,7 +74,9 @@ void submissionWithTestOutputOlderThanSubmissionIsNotGraded() { when(testOutputDetailsProvider.getTestOutputDetails(testOutput)).thenReturn(new FindUngradedSubmissions.TestOutputDetails(gradedTime, true)); FindUngradedSubmissions finder = new FindUngradedSubmissions(submissionDetailsProvider, testOutputProvider, testOutputDetailsProvider); - assertThat(finder.isGraded(submission), equalTo(false)); + FindUngradedSubmissions.SubmissionAnalysis analysis = finder.analyzeSubmission(submission); + assertThat(analysis.needsToBeTested(), equalTo(true)); + assertThat(analysis.needsToBeGraded(), equalTo(true)); } @Test @@ -95,7 +99,9 @@ void submissionWithNoGradeNeedsToBeGraded() { when(testOutputDetailsProvider.getTestOutputDetails(testOutput)).thenReturn(new FindUngradedSubmissions.TestOutputDetails(gradedTime, false)); FindUngradedSubmissions finder = new FindUngradedSubmissions(submissionDetailsProvider, testOutputProvider, testOutputDetailsProvider); - assertThat(finder.isGraded(submission), equalTo(false)); + FindUngradedSubmissions.SubmissionAnalysis analysis = finder.analyzeSubmission(submission); + assertThat(analysis.needsToBeTested(), equalTo(false)); + assertThat(analysis.needsToBeGraded(), equalTo(true)); } @Test @@ -118,6 +124,8 @@ void submissionWithGradeIsGraded() { when(testOutputDetailsProvider.getTestOutputDetails(testOutput)).thenReturn(new FindUngradedSubmissions.TestOutputDetails(gradedTime, true)); FindUngradedSubmissions finder = new FindUngradedSubmissions(submissionDetailsProvider, testOutputProvider, testOutputDetailsProvider); - assertThat(finder.isGraded(submission), equalTo(true)); + FindUngradedSubmissions.SubmissionAnalysis analysis = finder.analyzeSubmission(submission); + assertThat(analysis.needsToBeTested(), equalTo(false)); + assertThat(analysis.needsToBeGraded(), equalTo(false)); } } From e2b7f3a4f91279049f76fa90fda4d0017a905d64 Mon Sep 17 00:00:00 2001 From: David Whitlock Date: Wed, 20 Aug 2025 08:31:10 -0700 Subject: [PATCH 07/77] Do a better job of finding zip file. Still very much a work in progress. --- .../joy/grader/FindUngradedSubmissions.java | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/grader/src/main/java/edu/pdx/cs/joy/grader/FindUngradedSubmissions.java b/grader/src/main/java/edu/pdx/cs/joy/grader/FindUngradedSubmissions.java index d210884dc..7b4422d0b 100644 --- a/grader/src/main/java/edu/pdx/cs/joy/grader/FindUngradedSubmissions.java +++ b/grader/src/main/java/edu/pdx/cs/joy/grader/FindUngradedSubmissions.java @@ -2,9 +2,11 @@ import com.google.common.annotations.VisibleForTesting; +import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.time.ZonedDateTime; +import java.util.stream.Stream; public class FindUngradedSubmissions { private final SubmissionDetailsProvider submissionDetailsProvider; @@ -55,6 +57,34 @@ record SubmissionDetails(String studentId, ZonedDateTime submissionTime) { } + public static void main(String[] args) { + Stream submissions = findSubmissionsIn(args); + submissions.forEach(System.out::println); + } + + private static Stream findSubmissionsIn(String... fileNames) { + return Stream.of(fileNames) + .map(Path::of) + .filter(Files::exists) + .flatMap(path -> { + if (Files.isDirectory(path)) { + try (Stream walk = Files.walk(path)) { + return walk.filter(FindUngradedSubmissions::isZipFile); + + } catch (IOException e) { + throw new RuntimeException(e); + } + } else if (isZipFile(path)) { + return Stream.of(path); + } + return Stream.empty(); + }); + } + + private static boolean isZipFile(Path p) { + return Files.isRegularFile(p) && p.getFileName().toString().endsWith(".zip"); + } + interface SubmissionDetailsProvider { SubmissionDetails getSubmissionDetails(Path submission); } From 010fa6f439182530e1be3e402ac55f4b003691a6 Mon Sep 17 00:00:00 2001 From: David Whitlock Date: Thu, 21 Aug 2025 06:02:16 -0700 Subject: [PATCH 08/77] Create the snapshot versions for Winter 2026. --- examples/pom.xml | 10 +++++----- family/pom.xml | 6 +++--- grader/pom.xml | 4 ++-- pom.xml | 4 ++-- .../archetypes-parent/airline-archetype/pom.xml | 4 ++-- .../src/main/resources/archetype-resources/pom.xml | 6 +++--- .../archetypes-parent/airline-web-archetype/pom.xml | 4 ++-- .../src/main/resources/archetype-resources/pom.xml | 8 ++++---- .../archetypes-parent/apptbook-archetype/pom.xml | 4 ++-- .../src/main/resources/archetype-resources/pom.xml | 6 +++--- .../archetypes-parent/apptbook-web-archetype/pom.xml | 4 ++-- .../src/main/resources/archetype-resources/pom.xml | 8 ++++---- .../archetypes-parent/java-koans-archetype/pom.xml | 4 ++-- .../src/main/resources/archetype-resources/pom.xml | 2 +- .../archetypes-parent/kata-archetype/pom.xml | 4 ++-- .../src/main/resources/archetype-resources/pom.xml | 4 ++-- .../archetypes-parent/phonebill-archetype/pom.xml | 4 ++-- .../src/main/resources/archetype-resources/pom.xml | 6 +++--- .../archetypes-parent/phonebill-web-archetype/pom.xml | 4 ++-- .../src/main/resources/archetype-resources/pom.xml | 8 ++++---- projects-parent/archetypes-parent/pom.xml | 4 ++-- .../archetypes-parent/student-archetype/pom.xml | 7 ++++--- .../src/main/resources/archetype-resources/pom.xml | 6 +++--- projects-parent/originals-parent/airline-web/pom.xml | 10 +++++----- projects-parent/originals-parent/airline/pom.xml | 8 ++++---- projects-parent/originals-parent/apptbook-web/pom.xml | 10 +++++----- projects-parent/originals-parent/apptbook/pom.xml | 8 ++++---- projects-parent/originals-parent/kata/pom.xml | 6 +++--- projects-parent/originals-parent/phonebill-web/pom.xml | 10 +++++----- projects-parent/originals-parent/phonebill/pom.xml | 8 ++++---- projects-parent/originals-parent/pom.xml | 4 ++-- projects-parent/originals-parent/student/pom.xml | 8 ++++---- projects-parent/pom.xml | 4 ++-- projects-parent/projects/pom.xml | 4 ++-- web/pom.xml | 6 +++--- 35 files changed, 104 insertions(+), 103 deletions(-) diff --git a/examples/pom.xml b/examples/pom.xml index 72da988e6..f4252b19d 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -3,23 +3,23 @@ joy io.github.davidwhitlock.joy - 1.2.3 + 1.2.4-SNAPSHOT 4.0.0 examples examples - 1.3.4 + 1.3.5-SNAPSHOT https://www.cs.pdx.edu/~whitlock io.github.davidwhitlock.joy family - 1.1.5 + 1.1.6-SNAPSHOT io.github.davidwhitlock.joy projects - 3.0.3 + 3.0.4-SNAPSHOT com.sun.mail @@ -45,7 +45,7 @@ io.github.davidwhitlock.joy projects - 3.0.3 + 3.0.4-SNAPSHOT tests test diff --git a/family/pom.xml b/family/pom.xml index 9590f9ba5..6d99c0f68 100644 --- a/family/pom.xml +++ b/family/pom.xml @@ -3,12 +3,12 @@ joy io.github.davidwhitlock.joy - 1.2.3 + 1.2.4-SNAPSHOT 4.0.0 family jar - 1.1.5 + 1.1.6-SNAPSHOT Family Tree Application An Family Tree application for The Joy of Coding https://www.cs.pdx.edu/~whitlock @@ -16,7 +16,7 @@ io.github.davidwhitlock.joy projects - 3.0.3 + 3.0.4-SNAPSHOT diff --git a/grader/pom.xml b/grader/pom.xml index d82dc0da5..d4eb30fcf 100644 --- a/grader/pom.xml +++ b/grader/pom.xml @@ -2,7 +2,7 @@ joy io.github.davidwhitlock.joy - 1.2.3 + 1.2.4-SNAPSHOT 4.0.0 grader @@ -19,7 +19,7 @@ io.github.davidwhitlock.joy projects - 3.0.3 + 3.0.4-SNAPSHOT com.opencsv diff --git a/pom.xml b/pom.xml index 861397222..a05ff0953 100644 --- a/pom.xml +++ b/pom.xml @@ -2,7 +2,7 @@ 4.0.0 io.github.davidwhitlock.joy - 1.2.3 + 1.2.4-SNAPSHOT joy pom Java Example Code @@ -93,7 +93,7 @@ 0.75 0 - 1.4.0 + 1.4.1-SNAPSHOT diff --git a/projects-parent/archetypes-parent/airline-archetype/pom.xml b/projects-parent/archetypes-parent/airline-archetype/pom.xml index 81bf446b1..31360948a 100644 --- a/projects-parent/archetypes-parent/airline-archetype/pom.xml +++ b/projects-parent/archetypes-parent/airline-archetype/pom.xml @@ -5,10 +5,10 @@ archetypes-parent io.github.davidwhitlock.joy - 2.2.3 + 2.2.5-SNAPSHOT airline-archetype - 2.2.3 + 2.2.5-SNAPSHOT maven-archetype airline-archetype diff --git a/projects-parent/archetypes-parent/airline-archetype/src/main/resources/archetype-resources/pom.xml b/projects-parent/archetypes-parent/airline-archetype/src/main/resources/archetype-resources/pom.xml index cba2f3f53..c0ce20347 100644 --- a/projects-parent/archetypes-parent/airline-archetype/src/main/resources/archetype-resources/pom.xml +++ b/projects-parent/archetypes-parent/airline-archetype/src/main/resources/archetype-resources/pom.xml @@ -3,7 +3,7 @@ joy io.github.davidwhitlock.joy - 1.2.3 + 1.2.4-SNAPSHOT 4.0.0 ${groupId} @@ -32,12 +32,12 @@ io.github.davidwhitlock.joy projects - 3.0.3 + 3.0.4-SNAPSHOT io.github.davidwhitlock.joy projects - 3.0.3 + 3.0.4-SNAPSHOT tests test diff --git a/projects-parent/archetypes-parent/airline-web-archetype/pom.xml b/projects-parent/archetypes-parent/airline-web-archetype/pom.xml index 741cb8255..543369dd3 100644 --- a/projects-parent/archetypes-parent/airline-web-archetype/pom.xml +++ b/projects-parent/archetypes-parent/airline-web-archetype/pom.xml @@ -4,10 +4,10 @@ archetypes-parent io.github.davidwhitlock.joy - 2.2.3 + 2.2.5-SNAPSHOT airline-web-archetype - 3.0.3 + 3.0.4-SNAPSHOT maven-archetype airline-web-archetype 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 a3879aff0..29708764b 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 @@ -3,7 +3,7 @@ joy io.github.davidwhitlock.joy - 1.2.3 + 1.2.4-SNAPSHOT 4.0.0 ${groupId} @@ -21,17 +21,17 @@ io.github.davidwhitlock.joy projects - 3.0.3 + 3.0.4-SNAPSHOT io.github.davidwhitlock.joy examples - 1.3.4 + 1.3.5-SNAPSHOT io.github.davidwhitlock.joy projects - 3.0.3 + 3.0.4-SNAPSHOT tests test diff --git a/projects-parent/archetypes-parent/apptbook-archetype/pom.xml b/projects-parent/archetypes-parent/apptbook-archetype/pom.xml index d6d704d0e..4ba91da33 100644 --- a/projects-parent/archetypes-parent/apptbook-archetype/pom.xml +++ b/projects-parent/archetypes-parent/apptbook-archetype/pom.xml @@ -4,10 +4,10 @@ archetypes-parent io.github.davidwhitlock.joy - 2.2.3 + 2.2.5-SNAPSHOT apptbook-archetype - 2.2.3 + 2.2.5-SNAPSHOT maven-archetype apptbook-archetype diff --git a/projects-parent/archetypes-parent/apptbook-archetype/src/main/resources/archetype-resources/pom.xml b/projects-parent/archetypes-parent/apptbook-archetype/src/main/resources/archetype-resources/pom.xml index dd88be58a..f780ced61 100644 --- a/projects-parent/archetypes-parent/apptbook-archetype/src/main/resources/archetype-resources/pom.xml +++ b/projects-parent/archetypes-parent/apptbook-archetype/src/main/resources/archetype-resources/pom.xml @@ -3,7 +3,7 @@ joy io.github.davidwhitlock.joy - 1.2.3 + 1.2.4-SNAPSHOT 4.0.0 ${groupId} @@ -32,12 +32,12 @@ io.github.davidwhitlock.joy projects - 3.0.3 + 3.0.4-SNAPSHOT io.github.davidwhitlock.joy projects - 3.0.3 + 3.0.4-SNAPSHOT tests test diff --git a/projects-parent/archetypes-parent/apptbook-web-archetype/pom.xml b/projects-parent/archetypes-parent/apptbook-web-archetype/pom.xml index 173b1db34..4ac60ee78 100644 --- a/projects-parent/archetypes-parent/apptbook-web-archetype/pom.xml +++ b/projects-parent/archetypes-parent/apptbook-web-archetype/pom.xml @@ -4,10 +4,10 @@ archetypes-parent io.github.davidwhitlock.joy - 2.2.3 + 2.2.5-SNAPSHOT apptbook-web-archetype - 3.0.3 + 3.0.4-SNAPSHOT maven-archetype apptbook-web-archetype 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 d905bfea0..28ec5c60a 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 @@ -3,7 +3,7 @@ joy io.github.davidwhitlock.joy - 1.2.3 + 1.2.4-SNAPSHOT 4.0.0 ${groupId} @@ -21,17 +21,17 @@ io.github.davidwhitlock.joy projects - 3.0.3 + 3.0.4-SNAPSHOT io.github.davidwhitlock.joy examples - 1.3.4 + 1.3.5-SNAPSHOT io.github.davidwhitlock.joy projects - 3.0.3 + 3.0.4-SNAPSHOT tests test diff --git a/projects-parent/archetypes-parent/java-koans-archetype/pom.xml b/projects-parent/archetypes-parent/java-koans-archetype/pom.xml index ee01c8e81..4448be4b7 100644 --- a/projects-parent/archetypes-parent/java-koans-archetype/pom.xml +++ b/projects-parent/archetypes-parent/java-koans-archetype/pom.xml @@ -3,13 +3,13 @@ archetypes-parent io.github.davidwhitlock.joy - 2.2.3 + 2.2.5-SNAPSHOT 4.0.0 java-koans-archetype - 2.2.4 + 2.2.5-SNAPSHOT maven-archetype java-koans-archetype diff --git a/projects-parent/archetypes-parent/java-koans-archetype/src/main/resources/archetype-resources/pom.xml b/projects-parent/archetypes-parent/java-koans-archetype/src/main/resources/archetype-resources/pom.xml index 99b2de9be..fc26fdaaf 100644 --- a/projects-parent/archetypes-parent/java-koans-archetype/src/main/resources/archetype-resources/pom.xml +++ b/projects-parent/archetypes-parent/java-koans-archetype/src/main/resources/archetype-resources/pom.xml @@ -4,7 +4,7 @@ joy io.github.davidwhitlock.joy - 1.2.3 + 1.2.4-SNAPSHOT ${artifactId} ${groupId} diff --git a/projects-parent/archetypes-parent/kata-archetype/pom.xml b/projects-parent/archetypes-parent/kata-archetype/pom.xml index 41742c38d..02f3789cc 100644 --- a/projects-parent/archetypes-parent/kata-archetype/pom.xml +++ b/projects-parent/archetypes-parent/kata-archetype/pom.xml @@ -5,10 +5,10 @@ archetypes-parent io.github.davidwhitlock.joy - 2.2.3 + 2.2.5-SNAPSHOT kata-archetype - 2.2.3 + 2.2.5-SNAPSHOT maven-archetype kata-archetype diff --git a/projects-parent/archetypes-parent/kata-archetype/src/main/resources/archetype-resources/pom.xml b/projects-parent/archetypes-parent/kata-archetype/src/main/resources/archetype-resources/pom.xml index a802f8f47..64d5aa800 100644 --- a/projects-parent/archetypes-parent/kata-archetype/src/main/resources/archetype-resources/pom.xml +++ b/projects-parent/archetypes-parent/kata-archetype/src/main/resources/archetype-resources/pom.xml @@ -4,7 +4,7 @@ joy io.github.davidwhitlock.joy - 1.2.3 + 1.2.4-SNAPSHOT ${groupId} ${artifactId} @@ -88,7 +88,7 @@ io.github.davidwhitlock.joy projects - 3.0.3 + 3.0.4-SNAPSHOT tests test diff --git a/projects-parent/archetypes-parent/phonebill-archetype/pom.xml b/projects-parent/archetypes-parent/phonebill-archetype/pom.xml index 9f26b1aac..bdd0b901e 100644 --- a/projects-parent/archetypes-parent/phonebill-archetype/pom.xml +++ b/projects-parent/archetypes-parent/phonebill-archetype/pom.xml @@ -3,11 +3,11 @@ archetypes-parent io.github.davidwhitlock.joy - 2.2.3 + 2.2.5-SNAPSHOT 4.0.0 phonebill-archetype - 2.2.3 + 2.2.5-SNAPSHOT maven-archetype phonebill-archetype 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 6318b3057..f3ef90e28 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 @@ -3,7 +3,7 @@ joy io.github.davidwhitlock.joy - 1.2.3 + 1.2.4-SNAPSHOT 4.0.0 ${groupId} @@ -38,12 +38,12 @@ io.github.davidwhitlock.joy projects - 3.0.3 + 3.0.4-SNAPSHOT io.github.davidwhitlock.joy projects - 3.0.3 + 3.0.4-SNAPSHOT tests test diff --git a/projects-parent/archetypes-parent/phonebill-web-archetype/pom.xml b/projects-parent/archetypes-parent/phonebill-web-archetype/pom.xml index dd08ad252..1e476e38d 100644 --- a/projects-parent/archetypes-parent/phonebill-web-archetype/pom.xml +++ b/projects-parent/archetypes-parent/phonebill-web-archetype/pom.xml @@ -4,10 +4,10 @@ archetypes-parent io.github.davidwhitlock.joy - 2.2.3 + 2.2.5-SNAPSHOT phonebill-web-archetype - 3.0.3 + 3.0.4-SNAPSHOT maven-archetype phonebill-web-archetype 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 fbe79922a..aa7661620 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 @@ -3,7 +3,7 @@ joy io.github.davidwhitlock.joy - 1.2.3 + 1.2.4-SNAPSHOT 4.0.0 ${groupId} @@ -22,17 +22,17 @@ io.github.davidwhitlock.joy projects - 3.0.3 + 3.0.4-SNAPSHOT io.github.davidwhitlock.joy examples - 1.3.4 + 1.3.5-SNAPSHOT io.github.davidwhitlock.joy projects - 3.0.3 + 3.0.4-SNAPSHOT tests test diff --git a/projects-parent/archetypes-parent/pom.xml b/projects-parent/archetypes-parent/pom.xml index 8b245722d..960bfd4cf 100644 --- a/projects-parent/archetypes-parent/pom.xml +++ b/projects-parent/archetypes-parent/pom.xml @@ -7,11 +7,11 @@ io.github.davidwhitlock.joy projects-parent - 2.2.3 + 2.2.5-SNAPSHOT archetypes-parent - 2.2.3 + 2.2.5-SNAPSHOT pom diff --git a/projects-parent/archetypes-parent/student-archetype/pom.xml b/projects-parent/archetypes-parent/student-archetype/pom.xml index 31f7da9e9..ae5577a71 100644 --- a/projects-parent/archetypes-parent/student-archetype/pom.xml +++ b/projects-parent/archetypes-parent/student-archetype/pom.xml @@ -5,11 +5,11 @@ archetypes-parent io.github.davidwhitlock.joy - 2.2.3 + 2.2.5-SNAPSHOT student-archetype - 2.3.4 + 2.3.5-SNAPSHOT maven-archetype Archetype for Student project @@ -65,7 +65,7 @@ scm:git:git@github.com:JoyOfCodingPDX/JoyOfCoding.git/projects-parent/originals-parent/student https://github.com/JoyOfCodingPDX/JoyOfCoding/tree/main/projects-parent/originals-parent/student - + 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 06ed4d6c0..fe8811753 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 @@ -4,7 +4,7 @@ joy io.github.davidwhitlock.joy - 1.2.3 + 1.2.4-SNAPSHOT ${groupId} ${artifactId} @@ -102,12 +102,12 @@ io.github.davidwhitlock.joy examples - 1.3.4 + 1.3.5-SNAPSHOT io.github.davidwhitlock.joy projects - 3.0.3 + 3.0.4-SNAPSHOT tests test diff --git a/projects-parent/originals-parent/airline-web/pom.xml b/projects-parent/originals-parent/airline-web/pom.xml index eba30bacc..cf0db4c5b 100644 --- a/projects-parent/originals-parent/airline-web/pom.xml +++ b/projects-parent/originals-parent/airline-web/pom.xml @@ -3,12 +3,12 @@ originals-parent io.github.davidwhitlock.joy - 2.2.3 + 2.2.5-SNAPSHOT 4.0.0 io.github.davidwhitlock.joy.original airline-web - 3.0.3 + 3.0.4-SNAPSHOT 8080 @@ -21,17 +21,17 @@ io.github.davidwhitlock.joy projects - 3.0.3 + 3.0.4-SNAPSHOT io.github.davidwhitlock.joy examples - 1.3.4 + 1.3.5-SNAPSHOT io.github.davidwhitlock.joy projects - 3.0.3 + 3.0.4-SNAPSHOT tests test diff --git a/projects-parent/originals-parent/airline/pom.xml b/projects-parent/originals-parent/airline/pom.xml index 19fe284d6..932ae81b8 100644 --- a/projects-parent/originals-parent/airline/pom.xml +++ b/projects-parent/originals-parent/airline/pom.xml @@ -3,13 +3,13 @@ originals-parent io.github.davidwhitlock.joy - 2.2.3 + 2.2.5-SNAPSHOT 4.0.0 io.github.davidwhitlock.joy.original airline jar - 2.2.3 + 2.2.5-SNAPSHOT Airline Project An Airline application for The Joy of Coding at Portland State University 2000 @@ -32,12 +32,12 @@ io.github.davidwhitlock.joy projects - 3.0.3 + 3.0.4-SNAPSHOT io.github.davidwhitlock.joy projects - 3.0.3 + 3.0.4-SNAPSHOT tests test diff --git a/projects-parent/originals-parent/apptbook-web/pom.xml b/projects-parent/originals-parent/apptbook-web/pom.xml index bb0227ab6..88620f899 100644 --- a/projects-parent/originals-parent/apptbook-web/pom.xml +++ b/projects-parent/originals-parent/apptbook-web/pom.xml @@ -3,12 +3,12 @@ originals-parent io.github.davidwhitlock.joy - 2.2.3 + 2.2.5-SNAPSHOT 4.0.0 io.github.davidwhitlock.joy.original apptbook-web - 3.0.3 + 3.0.4-SNAPSHOT 8080 @@ -21,17 +21,17 @@ io.github.davidwhitlock.joy projects - 3.0.3 + 3.0.4-SNAPSHOT io.github.davidwhitlock.joy examples - 1.3.4 + 1.3.5-SNAPSHOT io.github.davidwhitlock.joy projects - 3.0.3 + 3.0.4-SNAPSHOT tests test diff --git a/projects-parent/originals-parent/apptbook/pom.xml b/projects-parent/originals-parent/apptbook/pom.xml index 07a54544b..da9f706e5 100644 --- a/projects-parent/originals-parent/apptbook/pom.xml +++ b/projects-parent/originals-parent/apptbook/pom.xml @@ -3,13 +3,13 @@ originals-parent io.github.davidwhitlock.joy - 2.2.3 + 2.2.5-SNAPSHOT 4.0.0 io.github.davidwhitlock.joy.original apptbook jar - 2.2.3 + 2.2.5-SNAPSHOT Appointment Book Project An Appointment Book application for The Joy of Coding at Portland State University 2000 @@ -32,12 +32,12 @@ io.github.davidwhitlock.joy projects - 3.0.3 + 3.0.4-SNAPSHOT io.github.davidwhitlock.joy projects - 3.0.3 + 3.0.4-SNAPSHOT tests test diff --git a/projects-parent/originals-parent/kata/pom.xml b/projects-parent/originals-parent/kata/pom.xml index 8a2ffae8e..f70fc76f7 100644 --- a/projects-parent/originals-parent/kata/pom.xml +++ b/projects-parent/originals-parent/kata/pom.xml @@ -4,11 +4,11 @@ originals-parent io.github.davidwhitlock.joy - 2.2.3 + 2.2.5-SNAPSHOT io.github.davidwhitlock.joy.original kata - 2.2.3 + 2.2.5-SNAPSHOT jar Kata Project @@ -88,7 +88,7 @@ io.github.davidwhitlock.joy projects - 3.0.3 + 3.0.4-SNAPSHOT tests test diff --git a/projects-parent/originals-parent/phonebill-web/pom.xml b/projects-parent/originals-parent/phonebill-web/pom.xml index e3ca9ac94..990426c30 100644 --- a/projects-parent/originals-parent/phonebill-web/pom.xml +++ b/projects-parent/originals-parent/phonebill-web/pom.xml @@ -3,12 +3,12 @@ originals-parent io.github.davidwhitlock.joy - 2.2.3 + 2.2.5-SNAPSHOT 4.0.0 io.github.davidwhitlock.joy.original phonebill-web - 3.0.3 + 3.0.4-SNAPSHOT 8080 @@ -22,17 +22,17 @@ io.github.davidwhitlock.joy projects - 3.0.3 + 3.0.4-SNAPSHOT io.github.davidwhitlock.joy examples - 1.3.4 + 1.3.5-SNAPSHOT io.github.davidwhitlock.joy projects - 3.0.3 + 3.0.4-SNAPSHOT tests test diff --git a/projects-parent/originals-parent/phonebill/pom.xml b/projects-parent/originals-parent/phonebill/pom.xml index 6098d5380..ee5222080 100644 --- a/projects-parent/originals-parent/phonebill/pom.xml +++ b/projects-parent/originals-parent/phonebill/pom.xml @@ -3,13 +3,13 @@ originals-parent io.github.davidwhitlock.joy - 2.2.3 + 2.2.5-SNAPSHOT 4.0.0 io.github.davidwhitlock.joy.original phonebill jar - 2.2.3 + 2.2.5-SNAPSHOT Phone Bill Project A Phone Bill application for The Joy of Coding at Portland State University 2000 @@ -38,12 +38,12 @@ io.github.davidwhitlock.joy projects - 3.0.3 + 3.0.4-SNAPSHOT io.github.davidwhitlock.joy projects - 3.0.3 + 3.0.4-SNAPSHOT tests test diff --git a/projects-parent/originals-parent/pom.xml b/projects-parent/originals-parent/pom.xml index ac25fc936..124d14da6 100644 --- a/projects-parent/originals-parent/pom.xml +++ b/projects-parent/originals-parent/pom.xml @@ -5,11 +5,11 @@ io.github.davidwhitlock.joy projects-parent - 2.2.3 + 2.2.5-SNAPSHOT originals-parent - 2.2.3 + 2.2.5-SNAPSHOT pom diff --git a/projects-parent/originals-parent/student/pom.xml b/projects-parent/originals-parent/student/pom.xml index bcc443733..b1a54d6d7 100644 --- a/projects-parent/originals-parent/student/pom.xml +++ b/projects-parent/originals-parent/student/pom.xml @@ -4,11 +4,11 @@ originals-parent io.github.davidwhitlock.joy - 2.2.3 + 2.2.5-SNAPSHOT io.github.davidwhitlock.joy.original student - 2.3.4 + 2.3.5-SNAPSHOT jar Student Project @@ -88,12 +88,12 @@ io.github.davidwhitlock.joy examples - 1.3.4 + 1.3.5-SNAPSHOT io.github.davidwhitlock.joy projects - 3.0.3 + 3.0.4-SNAPSHOT tests test diff --git a/projects-parent/pom.xml b/projects-parent/pom.xml index 7bb54268f..4a6ff553a 100644 --- a/projects-parent/pom.xml +++ b/projects-parent/pom.xml @@ -7,11 +7,11 @@ io.github.davidwhitlock.joy joy - 1.2.3 + 1.2.4-SNAPSHOT projects-parent - 2.2.3 + 2.2.5-SNAPSHOT pom diff --git a/projects-parent/projects/pom.xml b/projects-parent/projects/pom.xml index 9d80e62c0..6c237fce1 100644 --- a/projects-parent/projects/pom.xml +++ b/projects-parent/projects/pom.xml @@ -2,13 +2,13 @@ projects-parent io.github.davidwhitlock.joy - 2.2.3 + 2.2.5-SNAPSHOT 4.0.0 projects Project APIs Classes needed for the Projects in The Joy of Coding - 3.0.3 + 3.0.4-SNAPSHOT http://www.cs.pdx.edu/~whitlock diff --git a/web/pom.xml b/web/pom.xml index aa380907c..23b2ca8f0 100644 --- a/web/pom.xml +++ b/web/pom.xml @@ -3,13 +3,13 @@ joy io.github.davidwhitlock.joy - 1.2.3 + 1.2.4-SNAPSHOT 4.0.0 web war Web Application examples - 2.0.3 + 2.0.4-SNAPSHOT http://www.cs.pdx.edu/~whitlock 6.2.11.Final @@ -117,7 +117,7 @@ io.github.davidwhitlock.joy examples - 1.3.4 + 1.3.5-SNAPSHOT jakarta.xml.bind From 2305f0506378a100558dc7f88de44ede1ff449c3 Mon Sep 17 00:00:00 2001 From: David Whitlock Date: Thu, 21 Aug 2025 06:08:23 -0700 Subject: [PATCH 09/77] Integrate Winter 2026 artifact versions. --- grader/pom.xml | 2 +- pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/grader/pom.xml b/grader/pom.xml index 72d818cb0..d4eb30fcf 100644 --- a/grader/pom.xml +++ b/grader/pom.xml @@ -7,7 +7,7 @@ 4.0.0 grader grader - 1.5.0-SNAPSHOT + ${grader.version} jar https://www.cs.pdx.edu/~whitlock diff --git a/pom.xml b/pom.xml index a05ff0953..d1cc91911 100644 --- a/pom.xml +++ b/pom.xml @@ -93,7 +93,7 @@ 0.75 0 - 1.4.1-SNAPSHOT + 1.5.0-SNAPSHOT From ff1c55a9469cee082a532a56e421dc381cfeaca6 Mon Sep 17 00:00:00 2001 From: David Whitlock Date: Thu, 21 Aug 2025 06:30:09 -0700 Subject: [PATCH 10/77] Fix the URLs for the Central Portal Snapshots used by archetype projects. --- projects-parent/archetypes-parent/airline-archetype/pom.xml | 4 ++-- .../src/main/resources/archetype-resources/pom.xml | 4 ++-- .../archetypes-parent/java-koans-archetype/pom.xml | 4 ++-- .../src/main/resources/archetype-resources/pom.xml | 4 ++-- projects-parent/archetypes-parent/student-archetype/pom.xml | 6 ++---- .../src/main/resources/archetype-resources/pom.xml | 4 ++-- 6 files changed, 12 insertions(+), 14 deletions(-) diff --git a/projects-parent/archetypes-parent/airline-archetype/pom.xml b/projects-parent/archetypes-parent/airline-archetype/pom.xml index 31360948a..6815f961b 100644 --- a/projects-parent/archetypes-parent/airline-archetype/pom.xml +++ b/projects-parent/archetypes-parent/airline-archetype/pom.xml @@ -35,8 +35,8 @@ Central Portal Snapshots - central-portals - https://central.sonatype.com/repository/mavens/ + central-portal-snapshots + https://central.sonatype.com/repository/maven-snapshots/ false diff --git a/projects-parent/archetypes-parent/airline-archetype/src/main/resources/archetype-resources/pom.xml b/projects-parent/archetypes-parent/airline-archetype/src/main/resources/archetype-resources/pom.xml index c0ce20347..8ea6109f6 100644 --- a/projects-parent/archetypes-parent/airline-archetype/src/main/resources/archetype-resources/pom.xml +++ b/projects-parent/archetypes-parent/airline-archetype/src/main/resources/archetype-resources/pom.xml @@ -97,8 +97,8 @@ Central Portal Snapshots - central-portals - https://central.sonatype.com/repository/mavens/ + central-portal-snapshots + https://central.sonatype.com/repository/maven-snapshots/ false diff --git a/projects-parent/archetypes-parent/java-koans-archetype/pom.xml b/projects-parent/archetypes-parent/java-koans-archetype/pom.xml index 4448be4b7..79795ecaf 100644 --- a/projects-parent/archetypes-parent/java-koans-archetype/pom.xml +++ b/projects-parent/archetypes-parent/java-koans-archetype/pom.xml @@ -69,8 +69,8 @@ Central Portal Snapshots - central-portals - https://central.sonatype.com/repository/mavens/ + central-portal-snapshots + https://central.sonatype.com/repository/maven-snapshots/ false diff --git a/projects-parent/archetypes-parent/java-koans-archetype/src/main/resources/archetype-resources/pom.xml b/projects-parent/archetypes-parent/java-koans-archetype/src/main/resources/archetype-resources/pom.xml index fc26fdaaf..cb9d03212 100644 --- a/projects-parent/archetypes-parent/java-koans-archetype/src/main/resources/archetype-resources/pom.xml +++ b/projects-parent/archetypes-parent/java-koans-archetype/src/main/resources/archetype-resources/pom.xml @@ -112,8 +112,8 @@ Central Portal Snapshots - central-portals - https://central.sonatype.com/repository/mavens/ + central-portal-snapshots + https://central.sonatype.com/repository/maven-snapshots/ false diff --git a/projects-parent/archetypes-parent/student-archetype/pom.xml b/projects-parent/archetypes-parent/student-archetype/pom.xml index ae5577a71..e90ef36a3 100644 --- a/projects-parent/archetypes-parent/student-archetype/pom.xml +++ b/projects-parent/archetypes-parent/student-archetype/pom.xml @@ -65,12 +65,11 @@ scm:git:git@github.com:JoyOfCodingPDX/JoyOfCoding.git/projects-parent/originals-parent/student https://github.com/JoyOfCodingPDX/JoyOfCoding/tree/main/projects-parent/originals-parent/student - 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 fe8811753..d03bc1d7b 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 @@ -22,8 +22,8 @@ Central Portal Snapshots - central-portals - https://central.sonatype.com/repository/mavens/ + central-portal-snapshots + https://central.sonatype.com/repository/maven-snapshots/ false From 7afde59ec7b8042f2e3c5aa87cb17843cee13f8c Mon Sep 17 00:00:00 2001 From: David Whitlock Date: Thu, 21 Aug 2025 07:42:44 -0700 Subject: [PATCH 11/77] Finally figured out how to get Files.walk() method to do what I want. --- .../joy/grader/FindUngradedSubmissions.java | 38 +++++++++++-------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/grader/src/main/java/edu/pdx/cs/joy/grader/FindUngradedSubmissions.java b/grader/src/main/java/edu/pdx/cs/joy/grader/FindUngradedSubmissions.java index 7b4422d0b..01a648d78 100644 --- a/grader/src/main/java/edu/pdx/cs/joy/grader/FindUngradedSubmissions.java +++ b/grader/src/main/java/edu/pdx/cs/joy/grader/FindUngradedSubmissions.java @@ -3,9 +3,14 @@ import com.google.common.annotations.VisibleForTesting; import java.io.IOException; +import java.nio.file.FileVisitOption; import java.nio.file.Files; import java.nio.file.Path; import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; import java.util.stream.Stream; public class FindUngradedSubmissions { @@ -64,21 +69,24 @@ public static void main(String[] args) { private static Stream findSubmissionsIn(String... fileNames) { return Stream.of(fileNames) - .map(Path::of) - .filter(Files::exists) - .flatMap(path -> { - if (Files.isDirectory(path)) { - try (Stream walk = Files.walk(path)) { - return walk.filter(FindUngradedSubmissions::isZipFile); - - } catch (IOException e) { - throw new RuntimeException(e); - } - } else if (isZipFile(path)) { - return Stream.of(path); - } - return Stream.empty(); - }); + .map(Path::of) + .filter(Files::exists) + .flatMap(path -> findSubmissionsIn(path).stream()); + } + + private static Collection findSubmissionsIn(Path path) { + if (Files.isDirectory(path)) { + try (Stream walk = Files.walk(path, FileVisitOption.FOLLOW_LINKS)) { + return walk.filter(FindUngradedSubmissions::isZipFile).toList(); + + } catch (IOException e) { + throw new RuntimeException("Error while walking through directory: " + path, e); + } + } else if (isZipFile(path)) { + return List.of(path); + } else { + return Collections.emptyList(); + } } private static boolean isZipFile(Path p) { From 9deed2ca9f547a13db76b7fcd54ebaa10273d526 Mon Sep 17 00:00:00 2001 From: David Whitlock Date: Thu, 21 Aug 2025 07:46:53 -0700 Subject: [PATCH 12/77] Use the stream API even though we can't use a try-with-resources. --- .../joy/grader/FindUngradedSubmissions.java | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/grader/src/main/java/edu/pdx/cs/joy/grader/FindUngradedSubmissions.java b/grader/src/main/java/edu/pdx/cs/joy/grader/FindUngradedSubmissions.java index 01a648d78..3aa2e9dc4 100644 --- a/grader/src/main/java/edu/pdx/cs/joy/grader/FindUngradedSubmissions.java +++ b/grader/src/main/java/edu/pdx/cs/joy/grader/FindUngradedSubmissions.java @@ -7,10 +7,6 @@ import java.nio.file.Files; import java.nio.file.Path; import java.time.ZonedDateTime; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.List; import java.util.stream.Stream; public class FindUngradedSubmissions { @@ -71,21 +67,24 @@ private static Stream findSubmissionsIn(String... fileNames) { return Stream.of(fileNames) .map(Path::of) .filter(Files::exists) - .flatMap(path -> findSubmissionsIn(path).stream()); + .flatMap(FindUngradedSubmissions::findSubmissionsIn); } - private static Collection findSubmissionsIn(Path path) { + private static Stream findSubmissionsIn(Path path) { if (Files.isDirectory(path)) { - try (Stream walk = Files.walk(path, FileVisitOption.FOLLOW_LINKS)) { - return walk.filter(FindUngradedSubmissions::isZipFile).toList(); + try { + // If we put the walk into a try-with-resources, the consumer of the stream will encounter and + // exception, because the stream will be closed immediately. + Stream walk = Files.walk(path, FileVisitOption.FOLLOW_LINKS); + return walk.filter(FindUngradedSubmissions::isZipFile); } catch (IOException e) { throw new RuntimeException("Error while walking through directory: " + path, e); } } else if (isZipFile(path)) { - return List.of(path); + return Stream.of(path); } else { - return Collections.emptyList(); + return Stream.empty(); } } From 406088d731c7464dc5455b05a3899117ac98af7f Mon Sep 17 00:00:00 2001 From: David Whitlock Date: Thu, 21 Aug 2025 08:30:44 -0700 Subject: [PATCH 13/77] Realized that we need the path to the directory containing the submission in order to find the test output file. --- .../joy/grader/FindUngradedSubmissions.java | 73 +++++++++++++++++-- .../grader/ProjectSubmissionsProcessor.java | 22 ++++-- .../StudentEmailAttachmentProcessor.java | 2 +- .../grader/FindUngradedSubmissionsTest.java | 42 +++++++---- 4 files changed, 109 insertions(+), 30 deletions(-) diff --git a/grader/src/main/java/edu/pdx/cs/joy/grader/FindUngradedSubmissions.java b/grader/src/main/java/edu/pdx/cs/joy/grader/FindUngradedSubmissions.java index 3aa2e9dc4..0ae3daa66 100644 --- a/grader/src/main/java/edu/pdx/cs/joy/grader/FindUngradedSubmissions.java +++ b/grader/src/main/java/edu/pdx/cs/joy/grader/FindUngradedSubmissions.java @@ -3,10 +3,15 @@ import com.google.common.annotations.VisibleForTesting; import java.io.IOException; +import java.io.InputStream; import java.nio.file.FileVisitOption; import java.nio.file.Files; import java.nio.file.Path; -import java.time.ZonedDateTime; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.jar.Attributes; +import java.util.jar.Manifest; import java.util.stream.Stream; public class FindUngradedSubmissions { @@ -21,11 +26,16 @@ public class FindUngradedSubmissions { this.testOutputDetailsProvider = testOutputDetailsProvider; } + public FindUngradedSubmissions() { + this(new SubmissionDetailsProviderFromZipFile(), new TestOutputProviderInParentDirectory(), new TestOutputDetailsProviderFromTestOutputFile()); + } + @VisibleForTesting SubmissionAnalysis analyzeSubmission(Path submissionPath) { SubmissionDetails submission = this.submissionDetailsProvider.getSubmissionDetails(submissionPath); - Path testOutputPath = this.testOutputProvider.getTestOutput(submission.studentId()); + Path submissionDirectory = submissionPath.getParent(); + Path testOutputPath = this.testOutputProvider.getTestOutput(submissionDirectory, submission.studentId()); boolean needsToBeTested; boolean needsToBeGraded; @@ -54,13 +64,30 @@ SubmissionAnalysis analyzeSubmission(Path submissionPath) { } @VisibleForTesting - record SubmissionDetails(String studentId, ZonedDateTime submissionTime) { + record SubmissionDetails(String studentId, LocalDateTime submissionTime) { } public static void main(String[] args) { Stream submissions = findSubmissionsIn(args); - submissions.forEach(System.out::println); + FindUngradedSubmissions finder = new FindUngradedSubmissions(); + Stream analyses = submissions.map(finder::analyzeSubmission); + List needsToBeTested = new ArrayList<>(); + List needsToBeGraded = new ArrayList<>(); + analyses.forEach(analysis -> { + if (analysis.needsToBeTested()) { + needsToBeTested.add(analysis); + + } else if (analysis.needsToBeGraded()) { + needsToBeGraded.add(analysis); + } + }); + + System.out.println(needsToBeTested.size() + " submissions need to be tested: "); + needsToBeTested.forEach(System.out::println); + + System.out.println(needsToBeGraded.size() + " submissions need to be graded: "); + needsToBeGraded.forEach(System.out::println); } private static Stream findSubmissionsIn(String... fileNames) { @@ -97,7 +124,7 @@ interface SubmissionDetailsProvider { } interface TestOutputPathProvider { - Path getTestOutput(String studentId); + Path getTestOutput(Path submissionDirectory, String studentId); } interface TestOutputDetailsProvider { @@ -105,11 +132,45 @@ interface TestOutputDetailsProvider { } @VisibleForTesting - record TestOutputDetails(ZonedDateTime testedTime, boolean hasGrade) { + record TestOutputDetails(LocalDateTime testedTime, boolean hasGrade) { } @VisibleForTesting record SubmissionAnalysis (boolean needsToBeTested, boolean needsToBeGraded) { } + + private static class SubmissionDetailsProviderFromZipFile implements SubmissionDetailsProvider { + @Override + public SubmissionDetails getSubmissionDetails(Path submission) { + try (InputStream zipFile = Files.newInputStream(submission)) { + Manifest manifest = ProjectSubmissionsProcessor.getManifestFromZipFile(zipFile); + return getSubmissionDetails(manifest); + + } catch (IOException | StudentEmailAttachmentProcessor.SubmissionException e) { + throw new RuntimeException(e); + } + } + + private SubmissionDetails getSubmissionDetails(Manifest manifest) throws StudentEmailAttachmentProcessor.SubmissionException { + Attributes attrs = manifest.getMainAttributes(); + String studentId = ProjectSubmissionsProcessor.getStudentIdFromManifestAttributes(attrs); + LocalDateTime submissionTime = ProjectSubmissionsProcessor.getSubmissionTime(attrs); + return new SubmissionDetails(studentId, submissionTime); + } + } + + private static class TestOutputProviderInParentDirectory implements TestOutputPathProvider { + @Override + public Path getTestOutput(Path submissionDirectory, String studentId) { + throw new UnsupportedOperationException("This method is not implemented yet"); + } + } + + private static class TestOutputDetailsProviderFromTestOutputFile implements TestOutputDetailsProvider { + @Override + public TestOutputDetails getTestOutputDetails(Path testOutput) { + throw new UnsupportedOperationException("This method is not implemented yet"); + } + } } diff --git a/grader/src/main/java/edu/pdx/cs/joy/grader/ProjectSubmissionsProcessor.java b/grader/src/main/java/edu/pdx/cs/joy/grader/ProjectSubmissionsProcessor.java index 2d01d9c21..13f408d4a 100644 --- a/grader/src/main/java/edu/pdx/cs/joy/grader/ProjectSubmissionsProcessor.java +++ b/grader/src/main/java/edu/pdx/cs/joy/grader/ProjectSubmissionsProcessor.java @@ -130,7 +130,7 @@ void noteSubmissionInGradeBook(Manifest manifest) throws SubmissionException { } } - private LocalDateTime getSubmissionTime(Attributes attrs) throws SubmissionException { + public static LocalDateTime getSubmissionTime(Attributes attrs) throws SubmissionException { String string = getSubmissionTimeString(attrs); return Submit.ManifestAttributes.parseSubmissionTime(string); } @@ -150,7 +150,7 @@ private String getSubmissionNote(Attributes attrs) throws SubmissionException { "With comment: " + submissionComment + "\n"; } - private String getSubmissionTimeString(Attributes attrs) throws SubmissionException { + private static String getSubmissionTimeString(Attributes attrs) throws SubmissionException { return getManifestAttributeValue(attrs, Submit.ManifestAttributes.SUBMISSION_TIME, "Submission time missing from manifest"); } @@ -169,7 +169,7 @@ private String getProjectNameFromManifest(Attributes attrs) throws SubmissionExc } private Student getStudentFromGradeBook(Attributes attrs) throws SubmissionException { - String studentId = getManifestAttributeValue(attrs, Submit.ManifestAttributes.USER_ID, "Student Id missing from manifest"); + String studentId = getStudentIdFromManifestAttributes(attrs); String studentName = getManifestAttributeValue(attrs, Submit.ManifestAttributes.USER_NAME, "Student Name missing from manifest"); String studentEmail = getManifestAttributeValue(attrs, Submit.ManifestAttributes.USER_EMAIL, "Student Email missing from manifest"); @@ -186,6 +186,10 @@ private Student getStudentFromGradeBook(Attributes attrs) throws SubmissionExcep }); } + public static String getStudentIdFromManifestAttributes(Attributes attrs) throws SubmissionException { + return getManifestAttributeValue(attrs, Submit.ManifestAttributes.USER_ID, "Student Id missing from manifest"); + } + private boolean hasEmail(Student student, String studentEmail) { return studentEmail.equals(student.getEmail()); } @@ -203,7 +207,7 @@ private boolean hasStudentId(Student student, String studentId) { } - private String getManifestAttributeValue(Attributes attrs, Attributes.Name attribute, String message) throws SubmissionException { + private static String getManifestAttributeValue(Attributes attrs, Attributes.Name attribute, String message) throws SubmissionException { String value = attrs.getValue(attribute); if (value == null) { throwSubmissionException(message); @@ -211,13 +215,17 @@ private String getManifestAttributeValue(Attributes attrs, Attributes.Name attri return value; } - private void throwSubmissionException(String message) throws SubmissionException { + private static void throwSubmissionException(String message) throws SubmissionException { throw new SubmissionException(message); - } private Manifest getManifestFromByteArray(byte[] file) throws IOException { - ZipInputStream in = new ZipInputStream(new ByteArrayInputStream(file)); + InputStream zipFile = new ByteArrayInputStream(file); + return getManifestFromZipFile(zipFile); + } + + public static Manifest getManifestFromZipFile(InputStream zipFile) throws IOException { + ZipInputStream in = new ZipInputStream(zipFile); for (ZipEntry entry = in.getNextEntry(); entry != null ; entry = in.getNextEntry()) { if (entry.getName().equals(JarFile.MANIFEST_NAME)) { Manifest manifest = new Manifest(); diff --git a/grader/src/main/java/edu/pdx/cs/joy/grader/StudentEmailAttachmentProcessor.java b/grader/src/main/java/edu/pdx/cs/joy/grader/StudentEmailAttachmentProcessor.java index 6e3796ba3..5a5ca6640 100644 --- a/grader/src/main/java/edu/pdx/cs/joy/grader/StudentEmailAttachmentProcessor.java +++ b/grader/src/main/java/edu/pdx/cs/joy/grader/StudentEmailAttachmentProcessor.java @@ -35,7 +35,7 @@ protected void debug(String message) { this.logger.debug(message); } - protected class SubmissionException extends Exception { + protected static class SubmissionException extends Exception { public SubmissionException(String message) { super(message); } diff --git a/grader/src/test/java/edu/pdx/cs/joy/grader/FindUngradedSubmissionsTest.java b/grader/src/test/java/edu/pdx/cs/joy/grader/FindUngradedSubmissionsTest.java index 197532267..00b0520a8 100644 --- a/grader/src/test/java/edu/pdx/cs/joy/grader/FindUngradedSubmissionsTest.java +++ b/grader/src/test/java/edu/pdx/cs/joy/grader/FindUngradedSubmissionsTest.java @@ -6,10 +6,12 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.spi.FileSystemProvider; -import java.time.ZonedDateTime; +import java.time.LocalDateTime; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -25,11 +27,19 @@ private Path getPathToExistingFile() { private static Path getMockPath(boolean exists) { Path testOutput = mock(Path.class); + FileSystemProvider provider = mock(FileSystemProvider.class); when(provider.exists(testOutput)).thenReturn(exists); + FileSystem fileSystem = mock(FileSystem.class); when(fileSystem.provider()).thenReturn(provider); when(testOutput.getFileSystem()).thenReturn(fileSystem); + + Path parent = mock(Path.class); + when(parent.getFileSystem()).thenReturn(fileSystem); + when(testOutput.getParent()).thenReturn(parent); + when(provider.exists(parent)).thenReturn(true); + assertThat(Files.exists(testOutput), equalTo(exists)); return testOutput; } @@ -39,14 +49,14 @@ void submissionWithNoTestOutputNeedsToBeTested() { FindUngradedSubmissions.SubmissionDetailsProvider submissionDetailsProvider = mock(FindUngradedSubmissions.SubmissionDetailsProvider.class); String studentId = "student123"; - FindUngradedSubmissions.SubmissionDetails submissionDetails = new FindUngradedSubmissions.SubmissionDetails(studentId, ZonedDateTime.now()); - Path submission = mock(Path.class); + FindUngradedSubmissions.SubmissionDetails submissionDetails = new FindUngradedSubmissions.SubmissionDetails(studentId, LocalDateTime.now()); + Path submission = getPathToExistingFile(); when(submissionDetailsProvider.getSubmissionDetails(submission)).thenReturn(submissionDetails); Path testOutput = getPathToNonExistingFile(); FindUngradedSubmissions.TestOutputPathProvider testOutputProvider = mock(FindUngradedSubmissions.TestOutputPathProvider.class); - when(testOutputProvider.getTestOutput(studentId)).thenReturn(testOutput); + when(testOutputProvider.getTestOutput(any(Path.class), eq(studentId))).thenReturn(testOutput); FindUngradedSubmissions finder = new FindUngradedSubmissions(submissionDetailsProvider, testOutputProvider, mock(FindUngradedSubmissions.TestOutputDetailsProvider.class)); FindUngradedSubmissions.SubmissionAnalysis analysis = finder.analyzeSubmission(submission); @@ -59,18 +69,18 @@ void submissionWithTestOutputOlderThanSubmissionNeedsToBeTested() { FindUngradedSubmissions.SubmissionDetailsProvider submissionDetailsProvider = mock(FindUngradedSubmissions.SubmissionDetailsProvider.class); String studentId = "student123"; - ZonedDateTime submissionTime = ZonedDateTime.now(); + LocalDateTime submissionTime = LocalDateTime.now(); FindUngradedSubmissions.SubmissionDetails submissionDetails = new FindUngradedSubmissions.SubmissionDetails(studentId, submissionTime); - Path submission = mock(Path.class); + Path submission = getPathToExistingFile(); when(submissionDetailsProvider.getSubmissionDetails(submission)).thenReturn(submissionDetails); Path testOutput = getPathToExistingFile(); FindUngradedSubmissions.TestOutputPathProvider testOutputProvider = mock(FindUngradedSubmissions.TestOutputPathProvider.class); - when(testOutputProvider.getTestOutput(studentId)).thenReturn(testOutput); + when(testOutputProvider.getTestOutput(any(Path.class), eq(studentId))).thenReturn(testOutput); FindUngradedSubmissions.TestOutputDetailsProvider testOutputDetailsProvider = mock(FindUngradedSubmissions.TestOutputDetailsProvider.class); - ZonedDateTime gradedTime = submissionTime.minusDays(1); // Simulate test output older than submission + LocalDateTime gradedTime = submissionTime.minusDays(1); // Simulate test output older than submission when(testOutputDetailsProvider.getTestOutputDetails(testOutput)).thenReturn(new FindUngradedSubmissions.TestOutputDetails(gradedTime, true)); FindUngradedSubmissions finder = new FindUngradedSubmissions(submissionDetailsProvider, testOutputProvider, testOutputDetailsProvider); @@ -84,18 +94,18 @@ void submissionWithNoGradeNeedsToBeGraded() { FindUngradedSubmissions.SubmissionDetailsProvider submissionDetailsProvider = mock(FindUngradedSubmissions.SubmissionDetailsProvider.class); String studentId = "student123"; - ZonedDateTime submissionTime = ZonedDateTime.now(); + LocalDateTime submissionTime = LocalDateTime.now(); FindUngradedSubmissions.SubmissionDetails submissionDetails = new FindUngradedSubmissions.SubmissionDetails(studentId, submissionTime); - Path submission = mock(Path.class); + Path submission = getPathToExistingFile(); when(submissionDetailsProvider.getSubmissionDetails(submission)).thenReturn(submissionDetails); Path testOutput = getPathToExistingFile(); FindUngradedSubmissions.TestOutputPathProvider testOutputProvider = mock(FindUngradedSubmissions.TestOutputPathProvider.class); - when(testOutputProvider.getTestOutput(studentId)).thenReturn(testOutput); + when(testOutputProvider.getTestOutput(any(Path.class), eq(studentId))).thenReturn(testOutput); FindUngradedSubmissions.TestOutputDetailsProvider testOutputDetailsProvider = mock(FindUngradedSubmissions.TestOutputDetailsProvider.class); - ZonedDateTime gradedTime = submissionTime.plusDays(1); // Simulate test output newer than submission + LocalDateTime gradedTime = submissionTime.plusDays(1); // Simulate test output newer than submission when(testOutputDetailsProvider.getTestOutputDetails(testOutput)).thenReturn(new FindUngradedSubmissions.TestOutputDetails(gradedTime, false)); FindUngradedSubmissions finder = new FindUngradedSubmissions(submissionDetailsProvider, testOutputProvider, testOutputDetailsProvider); @@ -109,18 +119,18 @@ void submissionWithGradeIsGraded() { FindUngradedSubmissions.SubmissionDetailsProvider submissionDetailsProvider = mock(FindUngradedSubmissions.SubmissionDetailsProvider.class); String studentId = "student123"; - ZonedDateTime submissionTime = ZonedDateTime.now(); + LocalDateTime submissionTime = LocalDateTime.now(); FindUngradedSubmissions.SubmissionDetails submissionDetails = new FindUngradedSubmissions.SubmissionDetails(studentId, submissionTime); - Path submission = mock(Path.class); + Path submission = getPathToExistingFile(); when(submissionDetailsProvider.getSubmissionDetails(submission)).thenReturn(submissionDetails); Path testOutput = getPathToExistingFile(); FindUngradedSubmissions.TestOutputPathProvider testOutputProvider = mock(FindUngradedSubmissions.TestOutputPathProvider.class); - when(testOutputProvider.getTestOutput(studentId)).thenReturn(testOutput); + when(testOutputProvider.getTestOutput(any(Path.class), eq(studentId))).thenReturn(testOutput); FindUngradedSubmissions.TestOutputDetailsProvider testOutputDetailsProvider = mock(FindUngradedSubmissions.TestOutputDetailsProvider.class); - ZonedDateTime gradedTime = submissionTime.plusDays(1); + LocalDateTime gradedTime = submissionTime.plusDays(1); when(testOutputDetailsProvider.getTestOutputDetails(testOutput)).thenReturn(new FindUngradedSubmissions.TestOutputDetails(gradedTime, true)); FindUngradedSubmissions finder = new FindUngradedSubmissions(submissionDetailsProvider, testOutputProvider, testOutputDetailsProvider); From 82e65fb09ec6a26c03e3b9aa811c060829b626f2 Mon Sep 17 00:00:00 2001 From: David Whitlock Date: Thu, 21 Aug 2025 10:46:47 -0700 Subject: [PATCH 14/77] Implement a lot of logic. --- .../joy/grader/FindUngradedSubmissions.java | 97 +++++++++++++++++-- .../grader/FindUngradedSubmissionsTest.java | 54 ++++++++++- 2 files changed, 143 insertions(+), 8 deletions(-) diff --git a/grader/src/main/java/edu/pdx/cs/joy/grader/FindUngradedSubmissions.java b/grader/src/main/java/edu/pdx/cs/joy/grader/FindUngradedSubmissions.java index 0ae3daa66..e88ed08ad 100644 --- a/grader/src/main/java/edu/pdx/cs/joy/grader/FindUngradedSubmissions.java +++ b/grader/src/main/java/edu/pdx/cs/joy/grader/FindUngradedSubmissions.java @@ -8,10 +8,16 @@ import java.nio.file.Files; import java.nio.file.Path; import java.time.LocalDateTime; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; import java.util.ArrayList; import java.util.List; +import java.util.function.Consumer; import java.util.jar.Attributes; import java.util.jar.Manifest; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import java.util.stream.Stream; public class FindUngradedSubmissions { @@ -30,7 +36,6 @@ public FindUngradedSubmissions() { this(new SubmissionDetailsProviderFromZipFile(), new TestOutputProviderInParentDirectory(), new TestOutputDetailsProviderFromTestOutputFile()); } - @VisibleForTesting SubmissionAnalysis analyzeSubmission(Path submissionPath) { SubmissionDetails submission = this.submissionDetailsProvider.getSubmissionDetails(submissionPath); @@ -46,7 +51,7 @@ SubmissionAnalysis analyzeSubmission(Path submissionPath) { } else { TestOutputDetails testOutput = this.testOutputDetailsProvider.getTestOutputDetails(testOutputPath); - if (submission.submissionTime().isAfter(testOutput.testedTime())) { + if (submission.submissionTime().isAfter(testOutput.testedSubmissionTime())) { needsToBeTested = true; needsToBeGraded = true; @@ -132,7 +137,7 @@ interface TestOutputDetailsProvider { } @VisibleForTesting - record TestOutputDetails(LocalDateTime testedTime, boolean hasGrade) { + record TestOutputDetails(LocalDateTime testedSubmissionTime, boolean hasGrade) { } @VisibleForTesting @@ -163,14 +168,94 @@ private SubmissionDetails getSubmissionDetails(Manifest manifest) throws Student private static class TestOutputProviderInParentDirectory implements TestOutputPathProvider { @Override public Path getTestOutput(Path submissionDirectory, String studentId) { - throw new UnsupportedOperationException("This method is not implemented yet"); + return submissionDirectory.resolve(studentId + ".out"); } } - private static class TestOutputDetailsProviderFromTestOutputFile implements TestOutputDetailsProvider { + @VisibleForTesting + static class TestOutputDetailsProviderFromTestOutputFile implements TestOutputDetailsProvider { + private static final Pattern SUBMISSION_TIME_PATTERN = Pattern.compile(".*Submitted on (.+)"); + + public static LocalDateTime parseSubmissionTime(String line) { + if (line.contains("Submitted on")) { + Matcher matcher = TestOutputDetailsProviderFromTestOutputFile.SUBMISSION_TIME_PATTERN.matcher(line); + if (matcher.matches()) { + String timeString = matcher.group(1).trim(); + return parseTime(timeString); + } else { + throw new IllegalArgumentException("Could not parse submission time from line: " + line); + } + } + + return null; + } + + private static LocalDateTime parseTime(String timeString) { + try { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("E MMM d hh:mm:ss a z yyyy"); + return ZonedDateTime.parse(timeString, formatter).toLocalDateTime(); + + } catch (DateTimeParseException ex) { + return LocalDateTime.parse(timeString); + } + } + + public static Double parseGrade(String line) { + if (line.contains("out of")) { + String[] parts = line.split("out of"); + if (parts.length == 2) { + try { + return Double.parseDouble(parts[0].trim()); + } catch (NumberFormatException e) { + return Double.NaN; + } + } + } + return null; + } + @Override public TestOutputDetails getTestOutputDetails(Path testOutput) { - throw new UnsupportedOperationException("This method is not implemented yet"); + try { + return parseTestOutputDetails(Files.lines(testOutput)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + static TestOutputDetails parseTestOutputDetails(Stream lines) { + TestOutputDetailsCreator creator = new TestOutputDetailsCreator(); + lines.forEach(creator); + return creator.createTestOutputDetails(); + } + + private static class TestOutputDetailsCreator implements Consumer { + private LocalDateTime testedSubmissionTime; + private Boolean hasGrade; + + @Override + public void accept(String line) { + LocalDateTime submissionTime = parseSubmissionTime(line); + if (submissionTime != null) { + this.testedSubmissionTime = submissionTime; + } + + Double grade = parseGrade(line); + if (grade != null) { + this.hasGrade = !grade.isNaN(); + } + } + + public TestOutputDetails createTestOutputDetails() { + if (this.testedSubmissionTime == null) { + throw new IllegalStateException("Tested submission time was not set"); + + } else if( this.hasGrade == null) { + throw new IllegalStateException("Has grade was not set"); + } + + return new TestOutputDetails(this.testedSubmissionTime, hasGrade); + } } } } diff --git a/grader/src/test/java/edu/pdx/cs/joy/grader/FindUngradedSubmissionsTest.java b/grader/src/test/java/edu/pdx/cs/joy/grader/FindUngradedSubmissionsTest.java index 00b0520a8..0150dc055 100644 --- a/grader/src/test/java/edu/pdx/cs/joy/grader/FindUngradedSubmissionsTest.java +++ b/grader/src/test/java/edu/pdx/cs/joy/grader/FindUngradedSubmissionsTest.java @@ -1,5 +1,6 @@ package edu.pdx.cs.joy.grader; +import edu.pdx.cs.joy.grader.FindUngradedSubmissions.TestOutputDetailsProviderFromTestOutputFile; import org.junit.jupiter.api.Test; import java.nio.file.FileSystem; @@ -7,6 +8,7 @@ import java.nio.file.Path; import java.nio.file.spi.FileSystemProvider; import java.time.LocalDateTime; +import java.util.stream.Stream; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; @@ -80,8 +82,8 @@ void submissionWithTestOutputOlderThanSubmissionNeedsToBeTested() { when(testOutputProvider.getTestOutput(any(Path.class), eq(studentId))).thenReturn(testOutput); FindUngradedSubmissions.TestOutputDetailsProvider testOutputDetailsProvider = mock(FindUngradedSubmissions.TestOutputDetailsProvider.class); - LocalDateTime gradedTime = submissionTime.minusDays(1); // Simulate test output older than submission - when(testOutputDetailsProvider.getTestOutputDetails(testOutput)).thenReturn(new FindUngradedSubmissions.TestOutputDetails(gradedTime, true)); + LocalDateTime testedSubmissionTime = submissionTime.minusDays(1); // Simulate test output older than submission + when(testOutputDetailsProvider.getTestOutputDetails(testOutput)).thenReturn(new FindUngradedSubmissions.TestOutputDetails(testedSubmissionTime, true)); FindUngradedSubmissions finder = new FindUngradedSubmissions(submissionDetailsProvider, testOutputProvider, testOutputDetailsProvider); FindUngradedSubmissions.SubmissionAnalysis analysis = finder.analyzeSubmission(submission); @@ -138,4 +140,52 @@ void submissionWithGradeIsGraded() { assertThat(analysis.needsToBeTested(), equalTo(false)); assertThat(analysis.needsToBeGraded(), equalTo(false)); } + + @Test + void parseSubmissionTimeFromAndroidProjectTestOutputLine() { + LocalDateTime submissionTime = TestOutputDetailsProviderFromTestOutputFile.parseSubmissionTime(" Submitted on 2025-08-18T11:34:19.017953486"); + LocalDateTime expectedTime = LocalDateTime.of(2025, 8, 18, 11, 34, 19, 17953486); + assertThat(submissionTime, equalTo(expectedTime)); + } + + @Test + void parseSubmissionTimeFromTestOutputLine() { + LocalDateTime submissionTime = TestOutputDetailsProviderFromTestOutputFile.parseSubmissionTime(" Submitted on Wed Aug 6 01:13:59 PM PDT 2025"); + LocalDateTime expectedTime = LocalDateTime.of(2025, 8, 6, 13, 13, 59); + assertThat(submissionTime, equalTo(expectedTime)); + } + + @Test + void lineWithGradeHasGrade() { + String line = "12.5 out of 13.0"; + Double grade = TestOutputDetailsProviderFromTestOutputFile.parseGrade(line); + assertThat(grade, equalTo(12.5)); + } + + @Test + void lineWithNoGradeHasNoGrade() { + String line = "No grade"; + Double grade = TestOutputDetailsProviderFromTestOutputFile.parseGrade(line); + assertThat(grade, equalTo(null)); + } + + @Test + void lineWithMissingGradeHasNaNGrade() { + String line = " out of 13.0"; + Double nan = TestOutputDetailsProviderFromTestOutputFile.parseGrade(line); + assertThat(nan, equalTo(Double.NaN)); + } + + @Test + void parseTestOutputDetails() { + Stream lines = Stream.of( + " Submitted on Wed Aug 6 01:13:59 PM PDT 2025", + "", + "12.5 out of 13.0" + ); + FindUngradedSubmissions.TestOutputDetails details = TestOutputDetailsProviderFromTestOutputFile.parseTestOutputDetails(lines); + LocalDateTime submissionTime = LocalDateTime.of(2025, 8, 6, 13, 13, 59); + assertThat(details.testedSubmissionTime(), equalTo(submissionTime)); + assertThat(details.hasGrade(), equalTo(true)); + } } From e3fd3cb4e7ab47491a769dcc7cd3989a53df345f Mon Sep 17 00:00:00 2001 From: David Whitlock Date: Thu, 21 Aug 2025 11:02:07 -0700 Subject: [PATCH 15/77] Add submission path to analysis. --- .../cs/joy/grader/FindUngradedSubmissions.java | 16 ++++++++++------ .../joy/grader/FindUngradedSubmissionsTest.java | 1 + 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/grader/src/main/java/edu/pdx/cs/joy/grader/FindUngradedSubmissions.java b/grader/src/main/java/edu/pdx/cs/joy/grader/FindUngradedSubmissions.java index e88ed08ad..e21b3f4a6 100644 --- a/grader/src/main/java/edu/pdx/cs/joy/grader/FindUngradedSubmissions.java +++ b/grader/src/main/java/edu/pdx/cs/joy/grader/FindUngradedSubmissions.java @@ -65,7 +65,7 @@ SubmissionAnalysis analyzeSubmission(Path submissionPath) { } } - return new SubmissionAnalysis(needsToBeTested, needsToBeGraded); + return new SubmissionAnalysis(submissionPath, needsToBeTested, needsToBeGraded); } @VisibleForTesting @@ -88,11 +88,15 @@ public static void main(String[] args) { } }); - System.out.println(needsToBeTested.size() + " submissions need to be tested: "); - needsToBeTested.forEach(System.out::println); + printOutAnalyses(needsToBeTested, "tested"); + printOutAnalyses(needsToBeGraded, "graded"); + } - System.out.println(needsToBeGraded.size() + " submissions need to be graded: "); - needsToBeGraded.forEach(System.out::println); + private static void printOutAnalyses(List analyses, String action) { + int size = analyses.size(); + String plural = (size != 1 ? "s" : ""); + System.out.println(size + " submission" + plural + " need to be " + action + ": "); + analyses.forEach(analysis -> System.out.println(" " + analysis.submission)); } private static Stream findSubmissionsIn(String... fileNames) { @@ -141,7 +145,7 @@ record TestOutputDetails(LocalDateTime testedSubmissionTime, boolean hasGrade) { } @VisibleForTesting - record SubmissionAnalysis (boolean needsToBeTested, boolean needsToBeGraded) { + record SubmissionAnalysis (Path submission, boolean needsToBeTested, boolean needsToBeGraded) { } diff --git a/grader/src/test/java/edu/pdx/cs/joy/grader/FindUngradedSubmissionsTest.java b/grader/src/test/java/edu/pdx/cs/joy/grader/FindUngradedSubmissionsTest.java index 0150dc055..975b1e1dc 100644 --- a/grader/src/test/java/edu/pdx/cs/joy/grader/FindUngradedSubmissionsTest.java +++ b/grader/src/test/java/edu/pdx/cs/joy/grader/FindUngradedSubmissionsTest.java @@ -64,6 +64,7 @@ void submissionWithNoTestOutputNeedsToBeTested() { FindUngradedSubmissions.SubmissionAnalysis analysis = finder.analyzeSubmission(submission); assertThat(analysis.needsToBeTested(), equalTo(true)); assertThat(analysis.needsToBeGraded(), equalTo(true)); + assertThat(analysis.submission(), equalTo(submission)); } @Test From 7a22458b9567b825f71653f0eae6a71d1bead673 Mon Sep 17 00:00:00 2001 From: David Whitlock Date: Thu, 21 Aug 2025 11:19:30 -0700 Subject: [PATCH 16/77] Add FindUngradedSubmissions to GraderTools. --- .../edu/pdx/cs/joy/grader/FindUngradedSubmissions.java | 9 +++++++-- .../src/main/java/edu/pdx/cs/joy/grader/GraderTools.java | 6 +++++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/grader/src/main/java/edu/pdx/cs/joy/grader/FindUngradedSubmissions.java b/grader/src/main/java/edu/pdx/cs/joy/grader/FindUngradedSubmissions.java index e21b3f4a6..029686e04 100644 --- a/grader/src/main/java/edu/pdx/cs/joy/grader/FindUngradedSubmissions.java +++ b/grader/src/main/java/edu/pdx/cs/joy/grader/FindUngradedSubmissions.java @@ -74,6 +74,11 @@ record SubmissionDetails(String studentId, LocalDateTime submissionTime) { } public static void main(String[] args) { + if (args.length == 0) { + System.err.println("Usage: java FindUngradedSubmissions submissionZipOrDirectory+"); + System.exit(1); + } + Stream submissions = findSubmissionsIn(args); FindUngradedSubmissions finder = new FindUngradedSubmissions(); Stream analyses = submissions.map(finder::analyzeSubmission); @@ -94,8 +99,8 @@ public static void main(String[] args) { private static void printOutAnalyses(List analyses, String action) { int size = analyses.size(); - String plural = (size != 1 ? "s" : ""); - System.out.println(size + " submission" + plural + " need to be " + action + ": "); + String description = (size == 1 ? " submission needs" : " submissions need"); + System.out.println(size + description + " to be " + action + ": "); analyses.forEach(analysis -> System.out.println(" " + analysis.submission)); } diff --git a/grader/src/main/java/edu/pdx/cs/joy/grader/GraderTools.java b/grader/src/main/java/edu/pdx/cs/joy/grader/GraderTools.java index bdc595cfa..e0407f496 100644 --- a/grader/src/main/java/edu/pdx/cs/joy/grader/GraderTools.java +++ b/grader/src/main/java/edu/pdx/cs/joy/grader/GraderTools.java @@ -77,9 +77,12 @@ private static Class getToolClass(String tool) { case "projectTimeEstimates": return ProjectTimeEstimatesSummary.class; - case "generateStudentInitialsFile": + case "generateStudentInitialsFile": return GenerateStudentInitialsFile.class; + case "findUngradedSubmissions": + return FindUngradedSubmissions.class; + default: usage("Unknown tool: " + tool); return null; @@ -113,6 +116,7 @@ private static void usage(String message) { err.println(" fixAndroidZips Fix zip files for the Android project to work with grading script"); err.println(" projectTimeEstimates Generate markdown that summarizes the estimated project hours"); err.println(" generateStudentInitialsFile Generate a list of student initials from a grade book"); + err.println(" findUngradedSubmissions List submissions that need to be tested or graded"); err.println(" toolArg A command line argument to send to the tool"); err.println(); From 59042a55faf8179a493a991219623c6ff93b3316 Mon Sep 17 00:00:00 2001 From: David Whitlock Date: Thu, 21 Aug 2025 11:56:51 -0700 Subject: [PATCH 17/77] Fix some things found while working with real project submissions. --- .../joy/grader/FindUngradedSubmissions.java | 32 +++++++++++++++---- .../grader/FindUngradedSubmissionsTest.java | 31 ++++++++++++++++++ 2 files changed, 57 insertions(+), 6 deletions(-) diff --git a/grader/src/main/java/edu/pdx/cs/joy/grader/FindUngradedSubmissions.java b/grader/src/main/java/edu/pdx/cs/joy/grader/FindUngradedSubmissions.java index 029686e04..6735f9980 100644 --- a/grader/src/main/java/edu/pdx/cs/joy/grader/FindUngradedSubmissions.java +++ b/grader/src/main/java/edu/pdx/cs/joy/grader/FindUngradedSubmissions.java @@ -43,29 +43,40 @@ SubmissionAnalysis analyzeSubmission(Path submissionPath) { Path testOutputPath = this.testOutputProvider.getTestOutput(submissionDirectory, submission.studentId()); boolean needsToBeTested; boolean needsToBeGraded; + String reason; if (!Files.exists(testOutputPath)) { needsToBeTested = true; needsToBeGraded = true; + reason = "Test output file does not exist: " + testOutputPath; } else { TestOutputDetails testOutput = this.testOutputDetailsProvider.getTestOutputDetails(testOutputPath); - if (submission.submissionTime().isAfter(testOutput.testedSubmissionTime())) { + if (submittedAfterTesting(submission, testOutput)) { needsToBeTested = true; needsToBeGraded = true; + reason = "Submission on " + submission.submissionTime() + " is after testing on " + testOutput.testedSubmissionTime(); } else if (!testOutput.hasGrade()) { needsToBeTested = false; needsToBeGraded = true; + reason = "Test output file does not have a grade: " + testOutputPath; } else { needsToBeTested = false; needsToBeGraded = false; + reason = "Test output file was graded after submission: " + testOutputPath; } } - return new SubmissionAnalysis(submissionPath, needsToBeTested, needsToBeGraded); + return new SubmissionAnalysis(submissionPath, needsToBeTested, needsToBeGraded, reason); + } + + private static boolean submittedAfterTesting(SubmissionDetails submission, TestOutputDetails testOutput) { + LocalDateTime submissionTime = submission.submissionTime(); + LocalDateTime testedSubmissionTime = testOutput.testedSubmissionTime(); + return submissionTime.isAfter(testedSubmissionTime.plusMinutes(1L)); } @VisibleForTesting @@ -101,7 +112,7 @@ private static void printOutAnalyses(List analyses, String a int size = analyses.size(); String description = (size == 1 ? " submission needs" : " submissions need"); System.out.println(size + description + " to be " + action + ": "); - analyses.forEach(analysis -> System.out.println(" " + analysis.submission)); + analyses.forEach(analysis -> System.out.println(" " + analysis.submission + " " + analysis.reason)); } private static Stream findSubmissionsIn(String... fileNames) { @@ -150,7 +161,7 @@ record TestOutputDetails(LocalDateTime testedSubmissionTime, boolean hasGrade) { } @VisibleForTesting - record SubmissionAnalysis (Path submission, boolean needsToBeTested, boolean needsToBeGraded) { + record SubmissionAnalysis (Path submission, boolean needsToBeTested, boolean needsToBeGraded, String reason) { } @@ -201,8 +212,17 @@ public static LocalDateTime parseSubmissionTime(String line) { private static LocalDateTime parseTime(String timeString) { try { - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("E MMM d hh:mm:ss a z yyyy"); - return ZonedDateTime.parse(timeString, formatter).toLocalDateTime(); + ZonedDateTime zoned; + try { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("E MMM d hh:mm:ss a z yyyy"); + zoned = ZonedDateTime.parse(timeString, formatter); + + } catch (DateTimeParseException ex) { + // Single-digit day format + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("E MMM d hh:mm:ss a z yyyy"); + zoned = ZonedDateTime.parse(timeString, formatter); + } + return zoned.toLocalDateTime(); } catch (DateTimeParseException ex) { return LocalDateTime.parse(timeString); diff --git a/grader/src/test/java/edu/pdx/cs/joy/grader/FindUngradedSubmissionsTest.java b/grader/src/test/java/edu/pdx/cs/joy/grader/FindUngradedSubmissionsTest.java index 975b1e1dc..5b47b57ed 100644 --- a/grader/src/test/java/edu/pdx/cs/joy/grader/FindUngradedSubmissionsTest.java +++ b/grader/src/test/java/edu/pdx/cs/joy/grader/FindUngradedSubmissionsTest.java @@ -92,6 +92,30 @@ void submissionWithTestOutputOlderThanSubmissionNeedsToBeTested() { assertThat(analysis.needsToBeGraded(), equalTo(true)); } + @Test + void submissionWithTestOutputLessThanAMinuteOlderThanSubmissionDoesNotNeedToBeTested() { + FindUngradedSubmissions.SubmissionDetailsProvider submissionDetailsProvider = mock(FindUngradedSubmissions.SubmissionDetailsProvider.class); + + String studentId = "student123"; + LocalDateTime submissionTime = LocalDateTime.now(); + FindUngradedSubmissions.SubmissionDetails submissionDetails = new FindUngradedSubmissions.SubmissionDetails(studentId, submissionTime); + Path submission = getPathToExistingFile(); + when(submissionDetailsProvider.getSubmissionDetails(submission)).thenReturn(submissionDetails); + + Path testOutput = getPathToExistingFile(); + + FindUngradedSubmissions.TestOutputPathProvider testOutputProvider = mock(FindUngradedSubmissions.TestOutputPathProvider.class); + when(testOutputProvider.getTestOutput(any(Path.class), eq(studentId))).thenReturn(testOutput); + + FindUngradedSubmissions.TestOutputDetailsProvider testOutputDetailsProvider = mock(FindUngradedSubmissions.TestOutputDetailsProvider.class); + LocalDateTime testedSubmissionTime = submissionTime.minusSeconds(10); // Simulate test output older than submission + when(testOutputDetailsProvider.getTestOutputDetails(testOutput)).thenReturn(new FindUngradedSubmissions.TestOutputDetails(testedSubmissionTime, true)); + + FindUngradedSubmissions finder = new FindUngradedSubmissions(submissionDetailsProvider, testOutputProvider, testOutputDetailsProvider); + FindUngradedSubmissions.SubmissionAnalysis analysis = finder.analyzeSubmission(submission); + assertThat(analysis.needsToBeTested(), equalTo(false)); + } + @Test void submissionWithNoGradeNeedsToBeGraded() { FindUngradedSubmissions.SubmissionDetailsProvider submissionDetailsProvider = mock(FindUngradedSubmissions.SubmissionDetailsProvider.class); @@ -156,6 +180,13 @@ void parseSubmissionTimeFromTestOutputLine() { assertThat(submissionTime, equalTo(expectedTime)); } + @Test + void parseSubmissionTimeFromTestOutputLineWithTwoDigitDay() { + LocalDateTime submissionTime = TestOutputDetailsProviderFromTestOutputFile.parseSubmissionTime(" Submitted on Wed Jul 23 12:59:13 PM PDT 2025"); + LocalDateTime expectedTime = LocalDateTime.of(2025, 7, 23, 12, 59, 13); + assertThat(submissionTime, equalTo(expectedTime)); + } + @Test void lineWithGradeHasGrade() { String line = "12.5 out of 13.0"; From 443a3c6425956f1c1e26ac1fe96f2887f6ed6094 Mon Sep 17 00:00:00 2001 From: David Whitlock Date: Thu, 21 Aug 2025 12:20:26 -0700 Subject: [PATCH 18/77] If the " out of " grade line appears after line 7 of the test output, then consider it graded because the outputhas a note to the student in it. It doesn't need to be graded. --- .../joy/grader/FindUngradedSubmissions.java | 6 +++++- .../grader/FindUngradedSubmissionsTest.java | 20 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/grader/src/main/java/edu/pdx/cs/joy/grader/FindUngradedSubmissions.java b/grader/src/main/java/edu/pdx/cs/joy/grader/FindUngradedSubmissions.java index 6735f9980..760b4d403 100644 --- a/grader/src/main/java/edu/pdx/cs/joy/grader/FindUngradedSubmissions.java +++ b/grader/src/main/java/edu/pdx/cs/joy/grader/FindUngradedSubmissions.java @@ -261,9 +261,12 @@ static TestOutputDetails parseTestOutputDetails(Stream lines) { private static class TestOutputDetailsCreator implements Consumer { private LocalDateTime testedSubmissionTime; private Boolean hasGrade; + private int lineCount; @Override public void accept(String line) { + this.lineCount++; + LocalDateTime submissionTime = parseSubmissionTime(line); if (submissionTime != null) { this.testedSubmissionTime = submissionTime; @@ -271,7 +274,8 @@ public void accept(String line) { Double grade = parseGrade(line); if (grade != null) { - this.hasGrade = !grade.isNaN(); + boolean testOutputHasNotesForStudent = lineCount > 7; + this.hasGrade = testOutputHasNotesForStudent || !grade.isNaN(); } } diff --git a/grader/src/test/java/edu/pdx/cs/joy/grader/FindUngradedSubmissionsTest.java b/grader/src/test/java/edu/pdx/cs/joy/grader/FindUngradedSubmissionsTest.java index 5b47b57ed..283dba292 100644 --- a/grader/src/test/java/edu/pdx/cs/joy/grader/FindUngradedSubmissionsTest.java +++ b/grader/src/test/java/edu/pdx/cs/joy/grader/FindUngradedSubmissionsTest.java @@ -220,4 +220,24 @@ void parseTestOutputDetails() { assertThat(details.testedSubmissionTime(), equalTo(submissionTime)); assertThat(details.hasGrade(), equalTo(true)); } + + @Test + void testOutputDetailsWithMessageToStudentDoesNotNeedToBeGraded() { + Stream lines = Stream.of( + "Hi, Student. There were some problems with your submission", + "", + "I ran it through the testing script and there are a couple of things", + "I'd like you to fix before I ask the Graders to score it", + "", + " The Joy of Coding Project 3: edu.pdx.cs.joy.student.Project3", + " Submitted by Student Name", + " Submitted on Wed Jul 30 05:10:26 PM PDT 2025", + " Graded on Wed Jul 30 05:41:04 PM PDT 2025", + "", + " out of 7.0" + ); + FindUngradedSubmissions.TestOutputDetails details = TestOutputDetailsProviderFromTestOutputFile.parseTestOutputDetails(lines); + assertThat(details.hasGrade(), equalTo(true)); + + } } From b2896c6b06b027f4b1abb4098940607a076b764e Mon Sep 17 00:00:00 2001 From: David Whitlock Date: Thu, 21 Aug 2025 12:27:17 -0700 Subject: [PATCH 19/77] Optionally print out the reason that the submission needs testing/grading. --- .../joy/grader/FindUngradedSubmissions.java | 38 +++++++++++++++---- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/grader/src/main/java/edu/pdx/cs/joy/grader/FindUngradedSubmissions.java b/grader/src/main/java/edu/pdx/cs/joy/grader/FindUngradedSubmissions.java index 760b4d403..9c4648a8f 100644 --- a/grader/src/main/java/edu/pdx/cs/joy/grader/FindUngradedSubmissions.java +++ b/grader/src/main/java/edu/pdx/cs/joy/grader/FindUngradedSubmissions.java @@ -86,11 +86,27 @@ record SubmissionDetails(String studentId, LocalDateTime submissionTime) { public static void main(String[] args) { if (args.length == 0) { - System.err.println("Usage: java FindUngradedSubmissions submissionZipOrDirectory+"); + System.err.println("Usage: java FindUngradedSubmissions -includeReason submissionZipOrDirectory+"); System.exit(1); } - Stream submissions = findSubmissionsIn(args); + boolean includeReason = false; + List fileNames = new ArrayList<>(); + + for (String arg : args) { + if (arg.equals("-includeReason")) { + includeReason = true; + + } else if (arg.startsWith("-")) { + System.err.println("Unknown option: " + arg); + System.exit(1); + + } else { + fileNames.add(arg); + } + } + + Stream submissions = findSubmissionsIn(fileNames); FindUngradedSubmissions finder = new FindUngradedSubmissions(); Stream analyses = submissions.map(finder::analyzeSubmission); List needsToBeTested = new ArrayList<>(); @@ -104,19 +120,25 @@ public static void main(String[] args) { } }); - printOutAnalyses(needsToBeTested, "tested"); - printOutAnalyses(needsToBeGraded, "graded"); + printOutAnalyses(needsToBeTested, "tested", includeReason); + printOutAnalyses(needsToBeGraded, "graded", includeReason); } - private static void printOutAnalyses(List analyses, String action) { + private static void printOutAnalyses(List analyses, String action, boolean includeReason) { int size = analyses.size(); String description = (size == 1 ? " submission needs" : " submissions need"); System.out.println(size + description + " to be " + action + ": "); - analyses.forEach(analysis -> System.out.println(" " + analysis.submission + " " + analysis.reason)); + analyses.forEach(analysis -> { + System.out.print(" " + analysis.submission); + if (includeReason) { + System.out.print(" " + analysis.reason); + } + System.out.println(); + }); } - private static Stream findSubmissionsIn(String... fileNames) { - return Stream.of(fileNames) + private static Stream findSubmissionsIn(List fileNames) { + return fileNames.stream() .map(Path::of) .filter(Files::exists) .flatMap(FindUngradedSubmissions::findSubmissionsIn); From 1de2ceea644e57b8d8fd021461fe271f29acf234 Mon Sep 17 00:00:00 2001 From: David Whitlock Date: Thu, 21 Aug 2025 12:46:31 -0700 Subject: [PATCH 20/77] When generating letter grades, add a salutation to the grade report to save me some time. --- .../edu/pdx/cs/joy/grader/SummaryReport.java | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/grader/src/main/java/edu/pdx/cs/joy/grader/SummaryReport.java b/grader/src/main/java/edu/pdx/cs/joy/grader/SummaryReport.java index 2ae79cd94..7a8d064cf 100644 --- a/grader/src/main/java/edu/pdx/cs/joy/grader/SummaryReport.java +++ b/grader/src/main/java/edu/pdx/cs/joy/grader/SummaryReport.java @@ -37,6 +37,12 @@ static void dumpReportTo(GradeBook book, Student student, double best = 0.0; double total = 0.0; + if (assignLetterGrades) { + String studentName = student.getNickName() != null ? student.getNickName() : student.getFirstName(); + pw.println("Hi, " + studentName + ". Here are your final grades for \"The Joy of Coding\"."); + pw.println(); + } + pw.println("Grade summary for: " + student.getFullName()); SimpleDateFormat df = new SimpleDateFormat("EEEE MMMM d, yyyy 'at' h:mm a"); @@ -171,15 +177,19 @@ static boolean dueDateHasPassed(Assignment assignment) { } static boolean noStudentHasGradeFor(Assignment assignment, GradeBook book) { - boolean noAssignmentIsGraded = book.studentsStream() - .map(student -> getGrade(assignment, student)) - .noneMatch(grade -> grade != null && !grade.isNotGraded()); + return noSubmissionIsGraded(assignment, book) || allSubmissionsHaveGradeOfZero(assignment, book); + } - boolean allAssignmentHaveGradeOfZero = book.studentsStream() + private static boolean allSubmissionsHaveGradeOfZero(Assignment assignment, GradeBook book) { + return book.studentsStream() .map(student -> getGrade(assignment, student)) .allMatch(grade -> grade != null && grade.getScore() == 0.0); + } - return noAssignmentIsGraded || allAssignmentHaveGradeOfZero; + private static boolean noSubmissionIsGraded(Assignment assignment, GradeBook book) { + return book.studentsStream() + .map(student -> getGrade(assignment, student)) + .noneMatch(grade -> grade != null && !grade.isNotGraded()); } private static Grade getGrade(Assignment assignment, Student student) { From b028eac9071c1d187c34994ea0e49d727df284b5 Mon Sep 17 00:00:00 2001 From: David Whitlock Date: Thu, 21 Aug 2025 13:09:35 -0700 Subject: [PATCH 21/77] Write the grade percentage totals for each student to a file (in addition to standard output) so that I can quick reference them again. --- .../edu/pdx/cs/joy/grader/SummaryReport.java | 37 +++++++++++++++---- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/grader/src/main/java/edu/pdx/cs/joy/grader/SummaryReport.java b/grader/src/main/java/edu/pdx/cs/joy/grader/SummaryReport.java index 2ae79cd94..b97f2e747 100644 --- a/grader/src/main/java/edu/pdx/cs/joy/grader/SummaryReport.java +++ b/grader/src/main/java/edu/pdx/cs/joy/grader/SummaryReport.java @@ -223,7 +223,7 @@ private static void usage(String s) { * Main program that creates summary reports for every student in a * grade book located in a given XML file. */ - public static void main(String[] args) { + public static void main(String[] args) throws IOException { boolean assignLetterGrades = false; String xmlFileName = null; String outputDirName = null; @@ -283,23 +283,46 @@ public static void main(String[] args) { // Sort students by totals and print out results: Set students1 = allTotals.keySet(); - printOutStudentTotals(students1, out); + try (PrintWriter allStudentTotalsFile = new PrintWriter(new FileWriter("all-student-totals.txt"), true)) { + printOutStudentTotals(students1, new Writer() { + @Override + public void write(char[] cbuf, int off, int len) { + allStudentTotalsFile.write(cbuf, off, len); + out.write(cbuf, off, len); + } + + @Override + public void flush() { + allStudentTotalsFile.flush(); + out.flush(); + } + + @Override + public void close() { + allStudentTotalsFile.close(); + out.close(); + } + }); + + allStudentTotalsFile.flush(); + } saveGradeBookIfDirty(xmlFileName, book); } @VisibleForTesting - static void printOutStudentTotals(Set allStudents, PrintWriter out) { + static void printOutStudentTotals(Set allStudents, Writer writer) { + PrintWriter pw = new PrintWriter(writer, true); SortedSet sorted = getStudentSortedByTotalPoints(allStudents); - out.println("Undergraduates:"); + pw.println("Undergraduates:"); Stream undergrads = sorted.stream().filter(student -> student.getEnrolledSection() == Student.Section.UNDERGRADUATE); - printOutStudentTotals(out, undergrads); + printOutStudentTotals(pw, undergrads); - out.println("Graduate Students:"); + pw.println("Graduate Students:"); Stream grads = sorted.stream().filter(student -> student.getEnrolledSection() == Student.Section.GRADUATE); - printOutStudentTotals(out, grads); + printOutStudentTotals(pw, grads); } From 0f40a20fd3cb5e84e185b3a07c700edded5525ad Mon Sep 17 00:00:00 2001 From: David Whitlock Date: Thu, 21 Aug 2025 13:19:11 -0700 Subject: [PATCH 22/77] If the me.xml file already exists, have the Survey program issue a error and request that the student delete the me.xml file before running the Survey program again. --- .../main/java/edu/pdx/cs/joy/grader/Survey.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/grader/src/main/java/edu/pdx/cs/joy/grader/Survey.java b/grader/src/main/java/edu/pdx/cs/joy/grader/Survey.java index c8630cbe6..f292ff2c1 100644 --- a/grader/src/main/java/edu/pdx/cs/joy/grader/Survey.java +++ b/grader/src/main/java/edu/pdx/cs/joy/grader/Survey.java @@ -106,6 +106,8 @@ public static void main(String[] args) { void takeSurvey(String... args) { parseCommandLine(args); + exitIfStudentXmlFileAlreadyExists(); + printIntroduction(); Student student = gatherStudentInformation(); @@ -121,6 +123,19 @@ void takeSurvey(String... args) { } + private void exitIfStudentXmlFileAlreadyExists() { + File studentXmlFile = new File(this.xmlFileDir, STUDENT_XML_FILE_NAME); + if (studentXmlFile.exists()) { + String message = "\nIt looks like you've already run the Survey program.\n" + + "\nThe student XML file \"" + STUDENT_XML_FILE_NAME + + "\" already exists in the directory \"" + this.xmlFileDir + "\".\n" + + "\nYou don't need to run the Survey program again.\n" + + "\nIf you want to run it again, please delete the file \"" + + STUDENT_XML_FILE_NAME + "\" and try again."; + printErrorMessageAndExit(message); + } + } + private void addNotesToStudent(Student student, String learn, String comments) { if (isNotEmpty(learn)) { student.addNote(student.getFullName() + " would like to learn " + learn); From 1d4bffb2c2c34164fb897d9c63b065094bc8c6e3 Mon Sep 17 00:00:00 2001 From: David Whitlock Date: Fri, 22 Aug 2025 08:36:27 -0700 Subject: [PATCH 23/77] Fix some typos in the Student class and clarify the error message printed by the main method. --- .../src/it/java/StudentIT.java | 2 +- .../src/main/java/Student.java | 54 +++++++++---------- .../resources/projects/basic/verify.groovy | 2 +- .../resources/projects/javadoc/verify.groovy | 2 +- .../edu/pdx/cs/joy/student/StudentIT.java | 2 +- .../java/edu/pdx/cs/joy/student/Student.java | 6 +-- 6 files changed, 34 insertions(+), 34 deletions(-) diff --git a/projects-parent/archetypes-parent/student-archetype/src/main/resources/archetype-resources/src/it/java/StudentIT.java b/projects-parent/archetypes-parent/student-archetype/src/main/resources/archetype-resources/src/it/java/StudentIT.java index 90e1cabe3..d14fe00e5 100644 --- a/projects-parent/archetypes-parent/student-archetype/src/main/resources/archetype-resources/src/it/java/StudentIT.java +++ b/projects-parent/archetypes-parent/student-archetype/src/main/resources/archetype-resources/src/it/java/StudentIT.java @@ -19,7 +19,7 @@ class StudentIT extends InvokeMainTestCase { @Test void invokingMainWithNoArgumentsPrintsMissingArgumentsToStandardError() { InvokeMainTestCase.MainMethodResult result = invokeMain(Student.class); - assertThat(result.getTextWrittenToStandardError(), containsString("Missing command line arguments")); + assertThat(result.getTextWrittenToStandardError(), containsString("Missing required student information")); } } diff --git a/projects-parent/archetypes-parent/student-archetype/src/main/resources/archetype-resources/src/main/java/Student.java b/projects-parent/archetypes-parent/student-archetype/src/main/resources/archetype-resources/src/main/java/Student.java index 3fbde3517..48329141b 100644 --- a/projects-parent/archetypes-parent/student-archetype/src/main/resources/archetype-resources/src/main/java/Student.java +++ b/projects-parent/archetypes-parent/student-archetype/src/main/resources/archetype-resources/src/main/java/Student.java @@ -6,41 +6,41 @@ import edu.pdx.cs.joy.lang.Human; import java.util.ArrayList; - -/** - * This class is represents a Student. - */ -public class Student extends Human { - - /** - * Creates a new Student - * - * @param name - * The ${artifactId}'s name - * @param classes - * The names of the classes the ${artifactId} is taking. A ${artifactId} - * may take zero or more classes. - * @param gpa - * The ${artifactId}'s grade point average - * @param gender - * The ${artifactId}'s gender ("male", "female", or "other", case insensitive) - */ + +/** + * This class represents a Student. + */ +public class Student extends Human { + + /** + * Creates a new Student + * + * @param name + * The student's name + * @param classes + * The names of the classes the student is taking. A student + * may take zero or more classes. + * @param gpa + * The student's grade point average + * @param gender + * The student's gender ("male", "female", or "other", case-insensitive) + */ public Student(String name, ArrayList classes, double gpa, String gender) { super(name); } - /** + /** * All students say "This class is too much work" */ @Override - public String says() { + public String says() { throw new UnsupportedOperationException("Not implemented yet"); } - - /** - * Returns a String that describes this - * Student. - */ + + /** + * Returns a String that describes this + * Student. + */ public String toString() { throw new UnsupportedOperationException("Not implemented yet"); } @@ -51,6 +51,6 @@ public String toString() { * standard out by invoking its toString method. */ public static void main(String[] args) { - System.err.println("Missing command line arguments"); + System.err.println("Missing required student information"); } } \ No newline at end of file diff --git a/projects-parent/archetypes-parent/student-archetype/src/test/resources/projects/basic/verify.groovy b/projects-parent/archetypes-parent/student-archetype/src/test/resources/projects/basic/verify.groovy index 55b51cc33..0f4afce94 100644 --- a/projects-parent/archetypes-parent/student-archetype/src/test/resources/projects/basic/verify.groovy +++ b/projects-parent/archetypes-parent/student-archetype/src/test/resources/projects/basic/verify.groovy @@ -13,6 +13,6 @@ String jarCommand = "java -jar ${projectDir}/target/basic-0.1-SNAPSHOT.jar" def execution = jarCommand.execute() execution.waitFor() String stderr = execution.err.text -if (!stderr.contains("Missing command line arguments")) { +if (!stderr.contains("Missing required student information")) { throw new IllegalStateException("Running jar returned \"" + stderr + "\""); } \ No newline at end of file diff --git a/projects-parent/archetypes-parent/student-archetype/src/test/resources/projects/javadoc/verify.groovy b/projects-parent/archetypes-parent/student-archetype/src/test/resources/projects/javadoc/verify.groovy index a60308b03..8ed6be97a 100644 --- a/projects-parent/archetypes-parent/student-archetype/src/test/resources/projects/javadoc/verify.groovy +++ b/projects-parent/archetypes-parent/student-archetype/src/test/resources/projects/javadoc/verify.groovy @@ -5,7 +5,7 @@ if (!buildLog.isFile()) { String logText = buildLog.text -def expectedJavaDoc = "This class is represents a Student." +def expectedJavaDoc = "This class represents a Student." if (!logText.contains(expectedJavaDoc)) { throw new IllegalStateException("Didn't find expected JavaDoc: " + expectedJavaDoc) } diff --git a/projects-parent/originals-parent/student/src/it/java/edu/pdx/cs/joy/student/StudentIT.java b/projects-parent/originals-parent/student/src/it/java/edu/pdx/cs/joy/student/StudentIT.java index 91c393e12..6fa8f9b32 100644 --- a/projects-parent/originals-parent/student/src/it/java/edu/pdx/cs/joy/student/StudentIT.java +++ b/projects-parent/originals-parent/student/src/it/java/edu/pdx/cs/joy/student/StudentIT.java @@ -16,7 +16,7 @@ class StudentIT extends InvokeMainTestCase { @Test void invokingMainWithNoArgumentsPrintsMissingArgumentsToStandardError() { InvokeMainTestCase.MainMethodResult result = invokeMain(Student.class); - assertThat(result.getTextWrittenToStandardError(), containsString("Missing command line arguments")); + assertThat(result.getTextWrittenToStandardError(), containsString("Missing required student information")); } } diff --git a/projects-parent/originals-parent/student/src/main/java/edu/pdx/cs/joy/student/Student.java b/projects-parent/originals-parent/student/src/main/java/edu/pdx/cs/joy/student/Student.java index 0ed29e91d..fe478efc5 100644 --- a/projects-parent/originals-parent/student/src/main/java/edu/pdx/cs/joy/student/Student.java +++ b/projects-parent/originals-parent/student/src/main/java/edu/pdx/cs/joy/student/Student.java @@ -5,7 +5,7 @@ import java.util.ArrayList; /** - * This class is represents a Student. + * This class represents a Student. */ public class Student extends Human { @@ -20,7 +20,7 @@ public class Student extends Human { * @param gpa * The student's grade point average * @param gender - * The student's gender ("male", "female", or "other", case insensitive) + * The student's gender ("male", "female", or "other", case-insensitive) */ public Student(String name, ArrayList classes, double gpa, String gender) { super(name); @@ -48,6 +48,6 @@ public String toString() { * standard out by invoking its toString method. */ public static void main(String[] args) { - System.err.println("Missing command line arguments"); + System.err.println("Missing required student information"); } } \ No newline at end of file From 50995af7c3fbbc442c8b49e18ec14d2b3ca84ea5 Mon Sep 17 00:00:00 2001 From: David Whitlock Date: Fri, 22 Aug 2025 08:39:15 -0700 Subject: [PATCH 24/77] Prevent Project0, the Student project, from being submitted. --- grader/src/main/resources/edu/pdx/cs/joy/grader/project-names | 1 - 1 file changed, 1 deletion(-) diff --git a/grader/src/main/resources/edu/pdx/cs/joy/grader/project-names b/grader/src/main/resources/edu/pdx/cs/joy/grader/project-names index 7e7df7f20..29b5d24b1 100644 --- a/grader/src/main/resources/edu/pdx/cs/joy/grader/project-names +++ b/grader/src/main/resources/edu/pdx/cs/joy/grader/project-names @@ -1,4 +1,3 @@ -Project0 Project1 Project2 Project3 From 7e122dcc41e762b353aa628c6187ef6d1e384982 Mon Sep 17 00:00:00 2001 From: David Whitlock Date: Fri, 22 Aug 2025 08:53:35 -0700 Subject: [PATCH 25/77] Adjust a test now that Project0 can't be submitted. --- grader/src/test/java/edu/pdx/cs/joy/grader/SubmitTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/grader/src/test/java/edu/pdx/cs/joy/grader/SubmitTest.java b/grader/src/test/java/edu/pdx/cs/joy/grader/SubmitTest.java index 190b203e0..5319bd620 100644 --- a/grader/src/test/java/edu/pdx/cs/joy/grader/SubmitTest.java +++ b/grader/src/test/java/edu/pdx/cs/joy/grader/SubmitTest.java @@ -310,7 +310,7 @@ public void invalidProjectNameThrowsIllegalStateException() { @Test public void validateProjectName() { - List.of("koans", "Project0", "Project4").forEach( + List.of("koans", "Project1", "Project4").forEach( projectName -> { Submit submit = new Submit(); submit.setProjectName(projectName); From 7140087727be41bb2cfdffd49bde72b88aceb5a9 Mon Sep 17 00:00:00 2001 From: David Whitlock Date: Thu, 27 Nov 2025 14:03:03 -0800 Subject: [PATCH 26/77] Upgrade to version 3.3.4 of the Maven wrapper and version 3.9.11 of Maven. --- .mvn/wrapper/maven-wrapper.properties | 20 +- mvnw | 50 +- mvnw.cmd | 56 ++- .../.mvn/wrapper/maven-wrapper.properties | 2 +- .../main/resources/archetype-resources/mvnw | 475 +++++++++--------- .../resources/archetype-resources/mvnw.cmd | 56 ++- .../.mvn/wrapper/maven-wrapper.properties | 2 +- .../main/resources/archetype-resources/mvnw | 475 +++++++++--------- .../resources/archetype-resources/mvnw.cmd | 56 ++- .../.mvn/wrapper/maven-wrapper.properties | 2 +- .../main/resources/archetype-resources/mvnw | 475 +++++++++--------- .../resources/archetype-resources/mvnw.cmd | 56 ++- .../.mvn/wrapper/maven-wrapper.properties | 2 +- .../main/resources/archetype-resources/mvnw | 475 +++++++++--------- .../resources/archetype-resources/mvnw.cmd | 56 ++- .../.mvn/wrapper/maven-wrapper.properties | 2 +- .../main/resources/archetype-resources/mvnw | 50 +- .../resources/archetype-resources/mvnw.cmd | 56 ++- .../.mvn/wrapper/maven-wrapper.properties | 2 +- .../main/resources/archetype-resources/mvnw | 475 +++++++++--------- .../resources/archetype-resources/mvnw.cmd | 56 ++- .../.mvn/wrapper/maven-wrapper.properties | 2 +- .../main/resources/archetype-resources/mvnw | 475 +++++++++--------- .../resources/archetype-resources/mvnw.cmd | 56 ++- .../.mvn/wrapper/maven-wrapper.properties | 2 +- .../main/resources/archetype-resources/mvnw | 475 +++++++++--------- .../resources/archetype-resources/mvnw.cmd | 56 ++- .../.mvn/wrapper/maven-wrapper.properties | 2 +- .../main/resources/archetype-resources/mvnw | 475 +++++++++--------- .../resources/archetype-resources/mvnw.cmd | 56 ++- .../.mvn/wrapper/maven-wrapper.properties | 20 +- .../originals-parent/airline-web/mvnw | 50 +- .../originals-parent/airline-web/mvnw.cmd | 56 ++- .../.mvn/wrapper/maven-wrapper.properties | 20 +- projects-parent/originals-parent/airline/mvnw | 50 +- .../originals-parent/airline/mvnw.cmd | 56 ++- .../.mvn/wrapper/maven-wrapper.properties | 2 +- .../originals-parent/apptbook-web/mvnw | 50 +- .../originals-parent/apptbook-web/mvnw.cmd | 56 ++- .../.mvn/wrapper/maven-wrapper.properties | 2 +- .../originals-parent/apptbook/mvnw | 50 +- .../originals-parent/apptbook/mvnw.cmd | 56 ++- .../.mvn/wrapper/maven-wrapper.properties | 2 +- projects-parent/originals-parent/kata/mvnw | 50 +- .../originals-parent/kata/mvnw.cmd | 56 ++- .../.mvn/wrapper/maven-wrapper.properties | 2 +- .../originals-parent/phonebill-web/mvnw | 50 +- .../originals-parent/phonebill-web/mvnw.cmd | 56 ++- .../.mvn/wrapper/maven-wrapper.properties | 2 +- .../originals-parent/phonebill/mvnw | 50 +- .../originals-parent/phonebill/mvnw.cmd | 56 ++- .../.mvn/wrapper/maven-wrapper.properties | 2 +- projects-parent/originals-parent/student/mvnw | 50 +- .../originals-parent/student/mvnw.cmd | 56 ++- 54 files changed, 3155 insertions(+), 2243 deletions(-) diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties index d58dfb70b..c0bcafe98 100644 --- a/.mvn/wrapper/maven-wrapper.properties +++ b/.mvn/wrapper/maven-wrapper.properties @@ -1,19 +1,3 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. -wrapperVersion=3.3.2 +wrapperVersion=3.3.4 distributionType=only-script -distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.11/apache-maven-3.9.11-bin.zip diff --git a/mvnw b/mvnw index 19529ddf8..bd8896bf2 100755 --- a/mvnw +++ b/mvnw @@ -19,7 +19,7 @@ # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- -# Apache Maven Wrapper startup batch script, version 3.3.2 +# Apache Maven Wrapper startup batch script, version 3.3.4 # # Optional ENV vars # ----------------- @@ -105,14 +105,17 @@ trim() { printf "%s" "${1}" | tr -d '[:space:]' } +scriptDir="$(dirname "$0")" +scriptName="$(basename "$0")" + # parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties while IFS="=" read -r key value; do case "${key-}" in distributionUrl) distributionUrl=$(trim "${value-}") ;; distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; esac -done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" -[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties" +done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" case "${distributionUrl##*/}" in maven-mvnd-*bin.*) @@ -130,7 +133,7 @@ maven-mvnd-*bin.*) distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" ;; maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; -*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; esac # apply MVNW_REPOURL and calculate MAVEN_HOME @@ -227,7 +230,7 @@ if [ -n "${distributionSha256Sum-}" ]; then echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 exit 1 elif command -v sha256sum >/dev/null; then - if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then distributionSha256Result=true fi elif command -v shasum >/dev/null; then @@ -252,8 +255,41 @@ if command -v unzip >/dev/null; then else tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" fi -printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" -mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +actualDistributionDir="" + +# First try the expected directory name (for regular distributions) +if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then + if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then + actualDistributionDir="$distributionUrlNameMain" + fi +fi + +# If not found, search for any directory with the Maven executable (for snapshots) +if [ -z "$actualDistributionDir" ]; then + # enable globbing to iterate over items + set +f + for dir in "$TMP_DOWNLOAD_DIR"/*; do + if [ -d "$dir" ]; then + if [ -f "$dir/bin/$MVN_CMD" ]; then + actualDistributionDir="$(basename "$dir")" + break + fi + fi + done + set -f +fi + +if [ -z "$actualDistributionDir" ]; then + verbose "Contents of $TMP_DOWNLOAD_DIR:" + verbose "$(ls -la "$TMP_DOWNLOAD_DIR")" + die "Could not find Maven distribution directory in extracted archive" +fi + +verbose "Found extracted Maven distribution directory: $actualDistributionDir" +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" clean || : exec_maven "$@" diff --git a/mvnw.cmd b/mvnw.cmd index 249bdf382..92450f932 100644 --- a/mvnw.cmd +++ b/mvnw.cmd @@ -19,7 +19,7 @@ @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- -@REM Apache Maven Wrapper startup batch script, version 3.3.2 +@REM Apache Maven Wrapper startup batch script, version 3.3.4 @REM @REM Optional ENV vars @REM MVNW_REPOURL - repo url base for downloading maven distribution @@ -40,7 +40,7 @@ @SET __MVNW_ARG0_NAME__= @SET MVNW_USERNAME= @SET MVNW_PASSWORD= -@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) +@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*) @echo Cannot start maven from wrapper >&2 && exit /b 1 @GOTO :EOF : end batch / begin powershell #> @@ -73,16 +73,30 @@ switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { # apply MVNW_REPOURL and calculate MAVEN_HOME # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ if ($env:MVNW_REPOURL) { - $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } - $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" + $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')" } $distributionUrlName = $distributionUrl -replace '^.*/','' $distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' -$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" + +$MAVEN_M2_PATH = "$HOME/.m2" if ($env:MAVEN_USER_HOME) { - $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" + $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME" +} + +if (-not (Test-Path -Path $MAVEN_M2_PATH)) { + New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null +} + +$MAVEN_WRAPPER_DISTS = $null +if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) { + $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists" +} else { + $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists" } -$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' + +$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain" +$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' $MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { @@ -134,7 +148,33 @@ if ($distributionSha256Sum) { # unzip and move Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null -Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +$actualDistributionDir = "" + +# First try the expected directory name (for regular distributions) +$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain" +$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD" +if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) { + $actualDistributionDir = $distributionUrlNameMain +} + +# If not found, search for any directory with the Maven executable (for snapshots) +if (!$actualDistributionDir) { + Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object { + $testPath = Join-Path $_.FullName "bin/$MVN_CMD" + if (Test-Path -Path $testPath -PathType Leaf) { + $actualDistributionDir = $_.Name + } + } +} + +if (!$actualDistributionDir) { + Write-Error "Could not find Maven distribution directory in extracted archive" +} + +Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir" +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null try { Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null } catch { diff --git a/projects-parent/archetypes-parent/airline-archetype/src/main/resources/archetype-resources/.mvn/wrapper/maven-wrapper.properties b/projects-parent/archetypes-parent/airline-archetype/src/main/resources/archetype-resources/.mvn/wrapper/maven-wrapper.properties index 20c81b37a..c6bdae900 100644 --- a/projects-parent/archetypes-parent/airline-archetype/src/main/resources/archetype-resources/.mvn/wrapper/maven-wrapper.properties +++ b/projects-parent/archetypes-parent/airline-archetype/src/main/resources/archetype-resources/.mvn/wrapper/maven-wrapper.properties @@ -17,6 +17,6 @@ ${symbol_pound} "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY ${symbol_pound} KIND, either express or implied. See the License for the ${symbol_pound} specific language governing permissions and limitations ${symbol_pound} under the License. -wrapperVersion=3.3.2 +wrapperVersion=3.3.4 distributionType=only-script distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip diff --git a/projects-parent/archetypes-parent/airline-archetype/src/main/resources/archetype-resources/mvnw b/projects-parent/archetypes-parent/airline-archetype/src/main/resources/archetype-resources/mvnw index 41c0f0c23..bd8896bf2 100755 --- a/projects-parent/archetypes-parent/airline-archetype/src/main/resources/archetype-resources/mvnw +++ b/projects-parent/archetypes-parent/airline-archetype/src/main/resources/archetype-resources/mvnw @@ -19,292 +19,277 @@ # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- -# Maven Start Up Batch script -# -# Required ENV vars: -# ------------------ -# JAVA_HOME - location of a JDK home dir +# Apache Maven Wrapper startup batch script, version 3.3.4 # # Optional ENV vars # ----------------- -# M2_HOME - location of maven2's installed home dir -# MAVEN_OPTS - parameters passed to the Java VM when running Maven -# e.g. to debug Maven itself, use -# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output # ---------------------------------------------------------------------------- -if [ -z "$MAVEN_SKIP_RC" ] ; then - - if [ -f /etc/mavenrc ] ; then - . /etc/mavenrc - fi +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x - if [ -f "$HOME/.mavenrc" ] ; then - . "$HOME/.mavenrc" - fi +# OS specific support. +native_path() { printf %s\\n "$1"; } +case "$(uname)" in +CYGWIN* | MINGW*) + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } + ;; +esac -fi +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + if [ -n "${JAVA_HOME-}" ]; then + if [ -x "$JAVA_HOME/jre/sh/java" ]; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACCMD="$JAVA_HOME/jre/sh/javac" + else + JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" -# OS specific support. $var _must_ be set to either true or false. -cygwin=false; -darwin=false; -mingw=false -case "`uname`" in - CYGWIN*) cygwin=true ;; - MINGW*) mingw=true;; - Darwin*) darwin=true - # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home - # See https://developer.apple.com/library/mac/qa/qa1170/_index.html - if [ -z "$JAVA_HOME" ]; then - if [ -x "/usr/libexec/java_home" ]; then - export JAVA_HOME="`/usr/libexec/java_home`" - else - export JAVA_HOME="/Library/Java/Home" + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 fi fi - ;; -esac - -if [ -z "$JAVA_HOME" ] ; then - if [ -r /etc/gentoo-release ] ; then - JAVA_HOME=`java-config --jre-home` + else + JAVACMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v java + )" || : + JAVACCMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v javac + )" || : + + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 + fi fi -fi - -if [ -z "$M2_HOME" ] ; then - ## resolve links - $0 may be a link to maven's home - PRG="$0" +} - # need this for relative symlinks - while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG="`dirname "$PRG"`/$link" - fi +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + char="${str%"${str#?}"}" + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) + str="${str#?}" done + printf %x\\n $h +} - saveddir=`pwd` +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } - M2_HOME=`dirname "$PRG"`/.. +die() { + printf %s\\n "$1" >&2 + exit 1 +} - # make it fully qualified - M2_HOME=`cd "$M2_HOME" && pwd` +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} - cd "$saveddir" - # echo Using m2 at $M2_HOME -fi +scriptDir="$(dirname "$0")" +scriptName="$(basename "$0")" + +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl=$(trim "${value-}") ;; + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; + esac +done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" + +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac -# For Cygwin, ensure paths are in UNIX format before anything is touched -if $cygwin ; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --unix "$M2_HOME"` - [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --unix "$JAVA_HOME"` - [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --unix "$CLASSPATH"` -fi +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" +} -# For Mingw, ensure paths are in UNIX format before anything is touched -if $mingw ; then - [ -n "$M2_HOME" ] && - M2_HOME="`(cd "$M2_HOME"; pwd)`" - [ -n "$JAVA_HOME" ] && - JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" fi -if [ -z "$JAVA_HOME" ]; then - javaExecutable="`which javac`" - if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then - # readlink(1) is not available as standard on Solaris 10. - readLink=`which readlink` - if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then - if $darwin ; then - javaHome="`dirname \"$javaExecutable\"`" - javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" - else - javaExecutable="`readlink -f \"$javaExecutable\"`" - fi - javaHome="`dirname \"$javaExecutable\"`" - javaHome=`expr "$javaHome" : '\(.*\)/bin'` - JAVA_HOME="$javaHome" - export JAVA_HOME - fi - fi -fi +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac -if [ -z "$JAVACMD" ] ; then - if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - else - JAVACMD="`which java`" - fi +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT +else + die "cannot create temp dir" fi -if [ ! -x "$JAVACMD" ] ; then - echo "Error: JAVA_HOME is not defined correctly." >&2 - echo " We cannot execute $JAVACMD" >&2 - exit 1 -fi +mkdir -p -- "${MAVEN_HOME%/*}" -if [ -z "$JAVA_HOME" ] ; then - echo "Warning: JAVA_HOME environment variable is not set." +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" fi -CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v -# traverses directory structure from process work directory to filesystem root -# first directory with .mvn subdirectory is considered project base directory -find_maven_basedir() { +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac - if [ -z "$1" ] - then - echo "Path not specified to find_maven_basedir" - return 1 - fi +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" +fi - basedir="$1" - wdir="$1" - while [ "$wdir" != '/' ] ; do - if [ -d "$wdir"/.mvn ] ; then - basedir=$wdir - break +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then + distributionSha256Result=true fi - # workaround for JBEAP-8937 (on Solaris 10/Sparc) - if [ -d "${wdir}" ]; then - wdir=`cd "$wdir/.."; pwd` + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true fi - # end of workaround - done - echo "${basedir}" -} - -# concatenates all lines of a file -concat_lines() { - if [ -f "$1" ]; then - echo "$(tr -s '\n' ' ' < "$1")" + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 + exit 1 fi -} - -BASE_DIR=`find_maven_basedir "$(pwd)"` -if [ -z "$BASE_DIR" ]; then - exit 1; fi -########################################################################################## -# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central -# This allows using the maven wrapper in projects that prohibit checking in binary data. -########################################################################################## -if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found .mvn/wrapper/maven-wrapper.jar" - fi +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" else - if [ "$MVNW_VERBOSE" = true ]; then - echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." - fi - if [ -n "$MVNW_REPOURL" ]; then - jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" - else - jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" - fi - while IFS="=" read key value; do - case "$key" in (wrapperUrl) jarUrl="$value"; break ;; - esac - done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" - if [ "$MVNW_VERBOSE" = true ]; then - echo "Downloading from: $jarUrl" - fi - wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" - if $cygwin; then - wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` - fi + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" +fi - if command -v wget > /dev/null; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found wget ... using wget" - fi - if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then - wget "$jarUrl" -O "$wrapperJarPath" - else - wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" - fi - elif command -v curl > /dev/null; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found curl ... using curl" - fi - if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then - curl -o "$wrapperJarPath" "$jarUrl" -f - else - curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f - fi +# Find the actual extracted directory name (handles snapshots where filename != directory name) +actualDistributionDir="" - else - if [ "$MVNW_VERBOSE" = true ]; then - echo "Falling back to using Java to download" - fi - javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" - # For Cygwin, switch paths to Windows format before running javac - if $cygwin; then - javaClass=`cygpath --path --windows "$javaClass"` - fi - if [ -e "$javaClass" ]; then - if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then - if [ "$MVNW_VERBOSE" = true ]; then - echo " - Compiling MavenWrapperDownloader.java ..." - fi - # Compiling the Java class - ("$JAVA_HOME/bin/javac" "$javaClass") - fi - if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then - # Running the downloader - if [ "$MVNW_VERBOSE" = true ]; then - echo " - Running MavenWrapperDownloader.java ..." - fi - ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") - fi - fi - fi +# First try the expected directory name (for regular distributions) +if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then + if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then + actualDistributionDir="$distributionUrlNameMain" + fi fi -########################################################################################## -# End of extension -########################################################################################## -export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} -if [ "$MVNW_VERBOSE" = true ]; then - echo $MAVEN_PROJECTBASEDIR -fi -MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" - -# For Cygwin, switch paths to Windows format before running java -if $cygwin; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --path --windows "$M2_HOME"` - [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` - [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --windows "$CLASSPATH"` - [ -n "$MAVEN_PROJECTBASEDIR" ] && - MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +# If not found, search for any directory with the Maven executable (for snapshots) +if [ -z "$actualDistributionDir" ]; then + # enable globbing to iterate over items + set +f + for dir in "$TMP_DOWNLOAD_DIR"/*; do + if [ -d "$dir" ]; then + if [ -f "$dir/bin/$MVN_CMD" ]; then + actualDistributionDir="$(basename "$dir")" + break + fi + fi + done + set -f fi -# Provide a "standardized" way to retrieve the CLI args that will -# work with both Windows and non-Windows executions. -MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" -export MAVEN_CMD_LINE_ARGS +if [ -z "$actualDistributionDir" ]; then + verbose "Contents of $TMP_DOWNLOAD_DIR:" + verbose "$(ls -la "$TMP_DOWNLOAD_DIR")" + die "Could not find Maven distribution directory in extracted archive" +fi -WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain +verbose "Found extracted Maven distribution directory: $actualDistributionDir" +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" -exec "$JAVACMD" \ - $MAVEN_OPTS \ - -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ - ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" +clean || : +exec_maven "$@" diff --git a/projects-parent/archetypes-parent/airline-archetype/src/main/resources/archetype-resources/mvnw.cmd b/projects-parent/archetypes-parent/airline-archetype/src/main/resources/archetype-resources/mvnw.cmd index 249bdf382..92450f932 100644 --- a/projects-parent/archetypes-parent/airline-archetype/src/main/resources/archetype-resources/mvnw.cmd +++ b/projects-parent/archetypes-parent/airline-archetype/src/main/resources/archetype-resources/mvnw.cmd @@ -19,7 +19,7 @@ @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- -@REM Apache Maven Wrapper startup batch script, version 3.3.2 +@REM Apache Maven Wrapper startup batch script, version 3.3.4 @REM @REM Optional ENV vars @REM MVNW_REPOURL - repo url base for downloading maven distribution @@ -40,7 +40,7 @@ @SET __MVNW_ARG0_NAME__= @SET MVNW_USERNAME= @SET MVNW_PASSWORD= -@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) +@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*) @echo Cannot start maven from wrapper >&2 && exit /b 1 @GOTO :EOF : end batch / begin powershell #> @@ -73,16 +73,30 @@ switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { # apply MVNW_REPOURL and calculate MAVEN_HOME # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ if ($env:MVNW_REPOURL) { - $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } - $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" + $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')" } $distributionUrlName = $distributionUrl -replace '^.*/','' $distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' -$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" + +$MAVEN_M2_PATH = "$HOME/.m2" if ($env:MAVEN_USER_HOME) { - $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" + $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME" +} + +if (-not (Test-Path -Path $MAVEN_M2_PATH)) { + New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null +} + +$MAVEN_WRAPPER_DISTS = $null +if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) { + $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists" +} else { + $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists" } -$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' + +$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain" +$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' $MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { @@ -134,7 +148,33 @@ if ($distributionSha256Sum) { # unzip and move Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null -Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +$actualDistributionDir = "" + +# First try the expected directory name (for regular distributions) +$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain" +$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD" +if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) { + $actualDistributionDir = $distributionUrlNameMain +} + +# If not found, search for any directory with the Maven executable (for snapshots) +if (!$actualDistributionDir) { + Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object { + $testPath = Join-Path $_.FullName "bin/$MVN_CMD" + if (Test-Path -Path $testPath -PathType Leaf) { + $actualDistributionDir = $_.Name + } + } +} + +if (!$actualDistributionDir) { + Write-Error "Could not find Maven distribution directory in extracted archive" +} + +Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir" +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null try { Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null } catch { diff --git a/projects-parent/archetypes-parent/airline-web-archetype/src/main/resources/archetype-resources/.mvn/wrapper/maven-wrapper.properties b/projects-parent/archetypes-parent/airline-web-archetype/src/main/resources/archetype-resources/.mvn/wrapper/maven-wrapper.properties index 20c81b37a..c6bdae900 100644 --- a/projects-parent/archetypes-parent/airline-web-archetype/src/main/resources/archetype-resources/.mvn/wrapper/maven-wrapper.properties +++ b/projects-parent/archetypes-parent/airline-web-archetype/src/main/resources/archetype-resources/.mvn/wrapper/maven-wrapper.properties @@ -17,6 +17,6 @@ ${symbol_pound} "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY ${symbol_pound} KIND, either express or implied. See the License for the ${symbol_pound} specific language governing permissions and limitations ${symbol_pound} under the License. -wrapperVersion=3.3.2 +wrapperVersion=3.3.4 distributionType=only-script distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip diff --git a/projects-parent/archetypes-parent/airline-web-archetype/src/main/resources/archetype-resources/mvnw b/projects-parent/archetypes-parent/airline-web-archetype/src/main/resources/archetype-resources/mvnw index 41c0f0c23..bd8896bf2 100755 --- a/projects-parent/archetypes-parent/airline-web-archetype/src/main/resources/archetype-resources/mvnw +++ b/projects-parent/archetypes-parent/airline-web-archetype/src/main/resources/archetype-resources/mvnw @@ -19,292 +19,277 @@ # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- -# Maven Start Up Batch script -# -# Required ENV vars: -# ------------------ -# JAVA_HOME - location of a JDK home dir +# Apache Maven Wrapper startup batch script, version 3.3.4 # # Optional ENV vars # ----------------- -# M2_HOME - location of maven2's installed home dir -# MAVEN_OPTS - parameters passed to the Java VM when running Maven -# e.g. to debug Maven itself, use -# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output # ---------------------------------------------------------------------------- -if [ -z "$MAVEN_SKIP_RC" ] ; then - - if [ -f /etc/mavenrc ] ; then - . /etc/mavenrc - fi +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x - if [ -f "$HOME/.mavenrc" ] ; then - . "$HOME/.mavenrc" - fi +# OS specific support. +native_path() { printf %s\\n "$1"; } +case "$(uname)" in +CYGWIN* | MINGW*) + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } + ;; +esac -fi +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + if [ -n "${JAVA_HOME-}" ]; then + if [ -x "$JAVA_HOME/jre/sh/java" ]; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACCMD="$JAVA_HOME/jre/sh/javac" + else + JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" -# OS specific support. $var _must_ be set to either true or false. -cygwin=false; -darwin=false; -mingw=false -case "`uname`" in - CYGWIN*) cygwin=true ;; - MINGW*) mingw=true;; - Darwin*) darwin=true - # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home - # See https://developer.apple.com/library/mac/qa/qa1170/_index.html - if [ -z "$JAVA_HOME" ]; then - if [ -x "/usr/libexec/java_home" ]; then - export JAVA_HOME="`/usr/libexec/java_home`" - else - export JAVA_HOME="/Library/Java/Home" + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 fi fi - ;; -esac - -if [ -z "$JAVA_HOME" ] ; then - if [ -r /etc/gentoo-release ] ; then - JAVA_HOME=`java-config --jre-home` + else + JAVACMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v java + )" || : + JAVACCMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v javac + )" || : + + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 + fi fi -fi - -if [ -z "$M2_HOME" ] ; then - ## resolve links - $0 may be a link to maven's home - PRG="$0" +} - # need this for relative symlinks - while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG="`dirname "$PRG"`/$link" - fi +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + char="${str%"${str#?}"}" + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) + str="${str#?}" done + printf %x\\n $h +} - saveddir=`pwd` +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } - M2_HOME=`dirname "$PRG"`/.. +die() { + printf %s\\n "$1" >&2 + exit 1 +} - # make it fully qualified - M2_HOME=`cd "$M2_HOME" && pwd` +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} - cd "$saveddir" - # echo Using m2 at $M2_HOME -fi +scriptDir="$(dirname "$0")" +scriptName="$(basename "$0")" + +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl=$(trim "${value-}") ;; + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; + esac +done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" + +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac -# For Cygwin, ensure paths are in UNIX format before anything is touched -if $cygwin ; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --unix "$M2_HOME"` - [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --unix "$JAVA_HOME"` - [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --unix "$CLASSPATH"` -fi +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" +} -# For Mingw, ensure paths are in UNIX format before anything is touched -if $mingw ; then - [ -n "$M2_HOME" ] && - M2_HOME="`(cd "$M2_HOME"; pwd)`" - [ -n "$JAVA_HOME" ] && - JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" fi -if [ -z "$JAVA_HOME" ]; then - javaExecutable="`which javac`" - if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then - # readlink(1) is not available as standard on Solaris 10. - readLink=`which readlink` - if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then - if $darwin ; then - javaHome="`dirname \"$javaExecutable\"`" - javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" - else - javaExecutable="`readlink -f \"$javaExecutable\"`" - fi - javaHome="`dirname \"$javaExecutable\"`" - javaHome=`expr "$javaHome" : '\(.*\)/bin'` - JAVA_HOME="$javaHome" - export JAVA_HOME - fi - fi -fi +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac -if [ -z "$JAVACMD" ] ; then - if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - else - JAVACMD="`which java`" - fi +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT +else + die "cannot create temp dir" fi -if [ ! -x "$JAVACMD" ] ; then - echo "Error: JAVA_HOME is not defined correctly." >&2 - echo " We cannot execute $JAVACMD" >&2 - exit 1 -fi +mkdir -p -- "${MAVEN_HOME%/*}" -if [ -z "$JAVA_HOME" ] ; then - echo "Warning: JAVA_HOME environment variable is not set." +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" fi -CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v -# traverses directory structure from process work directory to filesystem root -# first directory with .mvn subdirectory is considered project base directory -find_maven_basedir() { +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac - if [ -z "$1" ] - then - echo "Path not specified to find_maven_basedir" - return 1 - fi +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" +fi - basedir="$1" - wdir="$1" - while [ "$wdir" != '/' ] ; do - if [ -d "$wdir"/.mvn ] ; then - basedir=$wdir - break +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then + distributionSha256Result=true fi - # workaround for JBEAP-8937 (on Solaris 10/Sparc) - if [ -d "${wdir}" ]; then - wdir=`cd "$wdir/.."; pwd` + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true fi - # end of workaround - done - echo "${basedir}" -} - -# concatenates all lines of a file -concat_lines() { - if [ -f "$1" ]; then - echo "$(tr -s '\n' ' ' < "$1")" + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 + exit 1 fi -} - -BASE_DIR=`find_maven_basedir "$(pwd)"` -if [ -z "$BASE_DIR" ]; then - exit 1; fi -########################################################################################## -# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central -# This allows using the maven wrapper in projects that prohibit checking in binary data. -########################################################################################## -if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found .mvn/wrapper/maven-wrapper.jar" - fi +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" else - if [ "$MVNW_VERBOSE" = true ]; then - echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." - fi - if [ -n "$MVNW_REPOURL" ]; then - jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" - else - jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" - fi - while IFS="=" read key value; do - case "$key" in (wrapperUrl) jarUrl="$value"; break ;; - esac - done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" - if [ "$MVNW_VERBOSE" = true ]; then - echo "Downloading from: $jarUrl" - fi - wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" - if $cygwin; then - wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` - fi + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" +fi - if command -v wget > /dev/null; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found wget ... using wget" - fi - if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then - wget "$jarUrl" -O "$wrapperJarPath" - else - wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" - fi - elif command -v curl > /dev/null; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found curl ... using curl" - fi - if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then - curl -o "$wrapperJarPath" "$jarUrl" -f - else - curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f - fi +# Find the actual extracted directory name (handles snapshots where filename != directory name) +actualDistributionDir="" - else - if [ "$MVNW_VERBOSE" = true ]; then - echo "Falling back to using Java to download" - fi - javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" - # For Cygwin, switch paths to Windows format before running javac - if $cygwin; then - javaClass=`cygpath --path --windows "$javaClass"` - fi - if [ -e "$javaClass" ]; then - if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then - if [ "$MVNW_VERBOSE" = true ]; then - echo " - Compiling MavenWrapperDownloader.java ..." - fi - # Compiling the Java class - ("$JAVA_HOME/bin/javac" "$javaClass") - fi - if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then - # Running the downloader - if [ "$MVNW_VERBOSE" = true ]; then - echo " - Running MavenWrapperDownloader.java ..." - fi - ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") - fi - fi - fi +# First try the expected directory name (for regular distributions) +if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then + if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then + actualDistributionDir="$distributionUrlNameMain" + fi fi -########################################################################################## -# End of extension -########################################################################################## -export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} -if [ "$MVNW_VERBOSE" = true ]; then - echo $MAVEN_PROJECTBASEDIR -fi -MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" - -# For Cygwin, switch paths to Windows format before running java -if $cygwin; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --path --windows "$M2_HOME"` - [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` - [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --windows "$CLASSPATH"` - [ -n "$MAVEN_PROJECTBASEDIR" ] && - MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +# If not found, search for any directory with the Maven executable (for snapshots) +if [ -z "$actualDistributionDir" ]; then + # enable globbing to iterate over items + set +f + for dir in "$TMP_DOWNLOAD_DIR"/*; do + if [ -d "$dir" ]; then + if [ -f "$dir/bin/$MVN_CMD" ]; then + actualDistributionDir="$(basename "$dir")" + break + fi + fi + done + set -f fi -# Provide a "standardized" way to retrieve the CLI args that will -# work with both Windows and non-Windows executions. -MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" -export MAVEN_CMD_LINE_ARGS +if [ -z "$actualDistributionDir" ]; then + verbose "Contents of $TMP_DOWNLOAD_DIR:" + verbose "$(ls -la "$TMP_DOWNLOAD_DIR")" + die "Could not find Maven distribution directory in extracted archive" +fi -WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain +verbose "Found extracted Maven distribution directory: $actualDistributionDir" +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" -exec "$JAVACMD" \ - $MAVEN_OPTS \ - -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ - ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" +clean || : +exec_maven "$@" diff --git a/projects-parent/archetypes-parent/airline-web-archetype/src/main/resources/archetype-resources/mvnw.cmd b/projects-parent/archetypes-parent/airline-web-archetype/src/main/resources/archetype-resources/mvnw.cmd index 249bdf382..92450f932 100644 --- a/projects-parent/archetypes-parent/airline-web-archetype/src/main/resources/archetype-resources/mvnw.cmd +++ b/projects-parent/archetypes-parent/airline-web-archetype/src/main/resources/archetype-resources/mvnw.cmd @@ -19,7 +19,7 @@ @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- -@REM Apache Maven Wrapper startup batch script, version 3.3.2 +@REM Apache Maven Wrapper startup batch script, version 3.3.4 @REM @REM Optional ENV vars @REM MVNW_REPOURL - repo url base for downloading maven distribution @@ -40,7 +40,7 @@ @SET __MVNW_ARG0_NAME__= @SET MVNW_USERNAME= @SET MVNW_PASSWORD= -@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) +@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*) @echo Cannot start maven from wrapper >&2 && exit /b 1 @GOTO :EOF : end batch / begin powershell #> @@ -73,16 +73,30 @@ switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { # apply MVNW_REPOURL and calculate MAVEN_HOME # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ if ($env:MVNW_REPOURL) { - $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } - $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" + $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')" } $distributionUrlName = $distributionUrl -replace '^.*/','' $distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' -$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" + +$MAVEN_M2_PATH = "$HOME/.m2" if ($env:MAVEN_USER_HOME) { - $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" + $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME" +} + +if (-not (Test-Path -Path $MAVEN_M2_PATH)) { + New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null +} + +$MAVEN_WRAPPER_DISTS = $null +if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) { + $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists" +} else { + $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists" } -$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' + +$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain" +$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' $MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { @@ -134,7 +148,33 @@ if ($distributionSha256Sum) { # unzip and move Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null -Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +$actualDistributionDir = "" + +# First try the expected directory name (for regular distributions) +$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain" +$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD" +if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) { + $actualDistributionDir = $distributionUrlNameMain +} + +# If not found, search for any directory with the Maven executable (for snapshots) +if (!$actualDistributionDir) { + Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object { + $testPath = Join-Path $_.FullName "bin/$MVN_CMD" + if (Test-Path -Path $testPath -PathType Leaf) { + $actualDistributionDir = $_.Name + } + } +} + +if (!$actualDistributionDir) { + Write-Error "Could not find Maven distribution directory in extracted archive" +} + +Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir" +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null try { Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null } catch { diff --git a/projects-parent/archetypes-parent/apptbook-archetype/src/main/resources/archetype-resources/.mvn/wrapper/maven-wrapper.properties b/projects-parent/archetypes-parent/apptbook-archetype/src/main/resources/archetype-resources/.mvn/wrapper/maven-wrapper.properties index 20c81b37a..c6bdae900 100644 --- a/projects-parent/archetypes-parent/apptbook-archetype/src/main/resources/archetype-resources/.mvn/wrapper/maven-wrapper.properties +++ b/projects-parent/archetypes-parent/apptbook-archetype/src/main/resources/archetype-resources/.mvn/wrapper/maven-wrapper.properties @@ -17,6 +17,6 @@ ${symbol_pound} "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY ${symbol_pound} KIND, either express or implied. See the License for the ${symbol_pound} specific language governing permissions and limitations ${symbol_pound} under the License. -wrapperVersion=3.3.2 +wrapperVersion=3.3.4 distributionType=only-script distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip diff --git a/projects-parent/archetypes-parent/apptbook-archetype/src/main/resources/archetype-resources/mvnw b/projects-parent/archetypes-parent/apptbook-archetype/src/main/resources/archetype-resources/mvnw index 41c0f0c23..bd8896bf2 100755 --- a/projects-parent/archetypes-parent/apptbook-archetype/src/main/resources/archetype-resources/mvnw +++ b/projects-parent/archetypes-parent/apptbook-archetype/src/main/resources/archetype-resources/mvnw @@ -19,292 +19,277 @@ # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- -# Maven Start Up Batch script -# -# Required ENV vars: -# ------------------ -# JAVA_HOME - location of a JDK home dir +# Apache Maven Wrapper startup batch script, version 3.3.4 # # Optional ENV vars # ----------------- -# M2_HOME - location of maven2's installed home dir -# MAVEN_OPTS - parameters passed to the Java VM when running Maven -# e.g. to debug Maven itself, use -# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output # ---------------------------------------------------------------------------- -if [ -z "$MAVEN_SKIP_RC" ] ; then - - if [ -f /etc/mavenrc ] ; then - . /etc/mavenrc - fi +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x - if [ -f "$HOME/.mavenrc" ] ; then - . "$HOME/.mavenrc" - fi +# OS specific support. +native_path() { printf %s\\n "$1"; } +case "$(uname)" in +CYGWIN* | MINGW*) + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } + ;; +esac -fi +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + if [ -n "${JAVA_HOME-}" ]; then + if [ -x "$JAVA_HOME/jre/sh/java" ]; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACCMD="$JAVA_HOME/jre/sh/javac" + else + JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" -# OS specific support. $var _must_ be set to either true or false. -cygwin=false; -darwin=false; -mingw=false -case "`uname`" in - CYGWIN*) cygwin=true ;; - MINGW*) mingw=true;; - Darwin*) darwin=true - # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home - # See https://developer.apple.com/library/mac/qa/qa1170/_index.html - if [ -z "$JAVA_HOME" ]; then - if [ -x "/usr/libexec/java_home" ]; then - export JAVA_HOME="`/usr/libexec/java_home`" - else - export JAVA_HOME="/Library/Java/Home" + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 fi fi - ;; -esac - -if [ -z "$JAVA_HOME" ] ; then - if [ -r /etc/gentoo-release ] ; then - JAVA_HOME=`java-config --jre-home` + else + JAVACMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v java + )" || : + JAVACCMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v javac + )" || : + + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 + fi fi -fi - -if [ -z "$M2_HOME" ] ; then - ## resolve links - $0 may be a link to maven's home - PRG="$0" +} - # need this for relative symlinks - while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG="`dirname "$PRG"`/$link" - fi +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + char="${str%"${str#?}"}" + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) + str="${str#?}" done + printf %x\\n $h +} - saveddir=`pwd` +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } - M2_HOME=`dirname "$PRG"`/.. +die() { + printf %s\\n "$1" >&2 + exit 1 +} - # make it fully qualified - M2_HOME=`cd "$M2_HOME" && pwd` +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} - cd "$saveddir" - # echo Using m2 at $M2_HOME -fi +scriptDir="$(dirname "$0")" +scriptName="$(basename "$0")" + +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl=$(trim "${value-}") ;; + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; + esac +done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" + +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac -# For Cygwin, ensure paths are in UNIX format before anything is touched -if $cygwin ; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --unix "$M2_HOME"` - [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --unix "$JAVA_HOME"` - [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --unix "$CLASSPATH"` -fi +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" +} -# For Mingw, ensure paths are in UNIX format before anything is touched -if $mingw ; then - [ -n "$M2_HOME" ] && - M2_HOME="`(cd "$M2_HOME"; pwd)`" - [ -n "$JAVA_HOME" ] && - JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" fi -if [ -z "$JAVA_HOME" ]; then - javaExecutable="`which javac`" - if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then - # readlink(1) is not available as standard on Solaris 10. - readLink=`which readlink` - if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then - if $darwin ; then - javaHome="`dirname \"$javaExecutable\"`" - javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" - else - javaExecutable="`readlink -f \"$javaExecutable\"`" - fi - javaHome="`dirname \"$javaExecutable\"`" - javaHome=`expr "$javaHome" : '\(.*\)/bin'` - JAVA_HOME="$javaHome" - export JAVA_HOME - fi - fi -fi +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac -if [ -z "$JAVACMD" ] ; then - if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - else - JAVACMD="`which java`" - fi +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT +else + die "cannot create temp dir" fi -if [ ! -x "$JAVACMD" ] ; then - echo "Error: JAVA_HOME is not defined correctly." >&2 - echo " We cannot execute $JAVACMD" >&2 - exit 1 -fi +mkdir -p -- "${MAVEN_HOME%/*}" -if [ -z "$JAVA_HOME" ] ; then - echo "Warning: JAVA_HOME environment variable is not set." +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" fi -CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v -# traverses directory structure from process work directory to filesystem root -# first directory with .mvn subdirectory is considered project base directory -find_maven_basedir() { +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac - if [ -z "$1" ] - then - echo "Path not specified to find_maven_basedir" - return 1 - fi +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" +fi - basedir="$1" - wdir="$1" - while [ "$wdir" != '/' ] ; do - if [ -d "$wdir"/.mvn ] ; then - basedir=$wdir - break +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then + distributionSha256Result=true fi - # workaround for JBEAP-8937 (on Solaris 10/Sparc) - if [ -d "${wdir}" ]; then - wdir=`cd "$wdir/.."; pwd` + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true fi - # end of workaround - done - echo "${basedir}" -} - -# concatenates all lines of a file -concat_lines() { - if [ -f "$1" ]; then - echo "$(tr -s '\n' ' ' < "$1")" + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 + exit 1 fi -} - -BASE_DIR=`find_maven_basedir "$(pwd)"` -if [ -z "$BASE_DIR" ]; then - exit 1; fi -########################################################################################## -# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central -# This allows using the maven wrapper in projects that prohibit checking in binary data. -########################################################################################## -if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found .mvn/wrapper/maven-wrapper.jar" - fi +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" else - if [ "$MVNW_VERBOSE" = true ]; then - echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." - fi - if [ -n "$MVNW_REPOURL" ]; then - jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" - else - jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" - fi - while IFS="=" read key value; do - case "$key" in (wrapperUrl) jarUrl="$value"; break ;; - esac - done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" - if [ "$MVNW_VERBOSE" = true ]; then - echo "Downloading from: $jarUrl" - fi - wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" - if $cygwin; then - wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` - fi + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" +fi - if command -v wget > /dev/null; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found wget ... using wget" - fi - if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then - wget "$jarUrl" -O "$wrapperJarPath" - else - wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" - fi - elif command -v curl > /dev/null; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found curl ... using curl" - fi - if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then - curl -o "$wrapperJarPath" "$jarUrl" -f - else - curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f - fi +# Find the actual extracted directory name (handles snapshots where filename != directory name) +actualDistributionDir="" - else - if [ "$MVNW_VERBOSE" = true ]; then - echo "Falling back to using Java to download" - fi - javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" - # For Cygwin, switch paths to Windows format before running javac - if $cygwin; then - javaClass=`cygpath --path --windows "$javaClass"` - fi - if [ -e "$javaClass" ]; then - if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then - if [ "$MVNW_VERBOSE" = true ]; then - echo " - Compiling MavenWrapperDownloader.java ..." - fi - # Compiling the Java class - ("$JAVA_HOME/bin/javac" "$javaClass") - fi - if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then - # Running the downloader - if [ "$MVNW_VERBOSE" = true ]; then - echo " - Running MavenWrapperDownloader.java ..." - fi - ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") - fi - fi - fi +# First try the expected directory name (for regular distributions) +if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then + if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then + actualDistributionDir="$distributionUrlNameMain" + fi fi -########################################################################################## -# End of extension -########################################################################################## -export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} -if [ "$MVNW_VERBOSE" = true ]; then - echo $MAVEN_PROJECTBASEDIR -fi -MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" - -# For Cygwin, switch paths to Windows format before running java -if $cygwin; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --path --windows "$M2_HOME"` - [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` - [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --windows "$CLASSPATH"` - [ -n "$MAVEN_PROJECTBASEDIR" ] && - MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +# If not found, search for any directory with the Maven executable (for snapshots) +if [ -z "$actualDistributionDir" ]; then + # enable globbing to iterate over items + set +f + for dir in "$TMP_DOWNLOAD_DIR"/*; do + if [ -d "$dir" ]; then + if [ -f "$dir/bin/$MVN_CMD" ]; then + actualDistributionDir="$(basename "$dir")" + break + fi + fi + done + set -f fi -# Provide a "standardized" way to retrieve the CLI args that will -# work with both Windows and non-Windows executions. -MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" -export MAVEN_CMD_LINE_ARGS +if [ -z "$actualDistributionDir" ]; then + verbose "Contents of $TMP_DOWNLOAD_DIR:" + verbose "$(ls -la "$TMP_DOWNLOAD_DIR")" + die "Could not find Maven distribution directory in extracted archive" +fi -WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain +verbose "Found extracted Maven distribution directory: $actualDistributionDir" +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" -exec "$JAVACMD" \ - $MAVEN_OPTS \ - -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ - ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" +clean || : +exec_maven "$@" diff --git a/projects-parent/archetypes-parent/apptbook-archetype/src/main/resources/archetype-resources/mvnw.cmd b/projects-parent/archetypes-parent/apptbook-archetype/src/main/resources/archetype-resources/mvnw.cmd index 249bdf382..92450f932 100644 --- a/projects-parent/archetypes-parent/apptbook-archetype/src/main/resources/archetype-resources/mvnw.cmd +++ b/projects-parent/archetypes-parent/apptbook-archetype/src/main/resources/archetype-resources/mvnw.cmd @@ -19,7 +19,7 @@ @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- -@REM Apache Maven Wrapper startup batch script, version 3.3.2 +@REM Apache Maven Wrapper startup batch script, version 3.3.4 @REM @REM Optional ENV vars @REM MVNW_REPOURL - repo url base for downloading maven distribution @@ -40,7 +40,7 @@ @SET __MVNW_ARG0_NAME__= @SET MVNW_USERNAME= @SET MVNW_PASSWORD= -@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) +@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*) @echo Cannot start maven from wrapper >&2 && exit /b 1 @GOTO :EOF : end batch / begin powershell #> @@ -73,16 +73,30 @@ switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { # apply MVNW_REPOURL and calculate MAVEN_HOME # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ if ($env:MVNW_REPOURL) { - $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } - $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" + $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')" } $distributionUrlName = $distributionUrl -replace '^.*/','' $distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' -$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" + +$MAVEN_M2_PATH = "$HOME/.m2" if ($env:MAVEN_USER_HOME) { - $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" + $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME" +} + +if (-not (Test-Path -Path $MAVEN_M2_PATH)) { + New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null +} + +$MAVEN_WRAPPER_DISTS = $null +if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) { + $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists" +} else { + $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists" } -$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' + +$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain" +$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' $MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { @@ -134,7 +148,33 @@ if ($distributionSha256Sum) { # unzip and move Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null -Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +$actualDistributionDir = "" + +# First try the expected directory name (for regular distributions) +$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain" +$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD" +if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) { + $actualDistributionDir = $distributionUrlNameMain +} + +# If not found, search for any directory with the Maven executable (for snapshots) +if (!$actualDistributionDir) { + Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object { + $testPath = Join-Path $_.FullName "bin/$MVN_CMD" + if (Test-Path -Path $testPath -PathType Leaf) { + $actualDistributionDir = $_.Name + } + } +} + +if (!$actualDistributionDir) { + Write-Error "Could not find Maven distribution directory in extracted archive" +} + +Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir" +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null try { Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null } catch { diff --git a/projects-parent/archetypes-parent/apptbook-web-archetype/src/main/resources/archetype-resources/.mvn/wrapper/maven-wrapper.properties b/projects-parent/archetypes-parent/apptbook-web-archetype/src/main/resources/archetype-resources/.mvn/wrapper/maven-wrapper.properties index 20c81b37a..c6bdae900 100644 --- a/projects-parent/archetypes-parent/apptbook-web-archetype/src/main/resources/archetype-resources/.mvn/wrapper/maven-wrapper.properties +++ b/projects-parent/archetypes-parent/apptbook-web-archetype/src/main/resources/archetype-resources/.mvn/wrapper/maven-wrapper.properties @@ -17,6 +17,6 @@ ${symbol_pound} "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY ${symbol_pound} KIND, either express or implied. See the License for the ${symbol_pound} specific language governing permissions and limitations ${symbol_pound} under the License. -wrapperVersion=3.3.2 +wrapperVersion=3.3.4 distributionType=only-script distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip diff --git a/projects-parent/archetypes-parent/apptbook-web-archetype/src/main/resources/archetype-resources/mvnw b/projects-parent/archetypes-parent/apptbook-web-archetype/src/main/resources/archetype-resources/mvnw index 41c0f0c23..bd8896bf2 100755 --- a/projects-parent/archetypes-parent/apptbook-web-archetype/src/main/resources/archetype-resources/mvnw +++ b/projects-parent/archetypes-parent/apptbook-web-archetype/src/main/resources/archetype-resources/mvnw @@ -19,292 +19,277 @@ # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- -# Maven Start Up Batch script -# -# Required ENV vars: -# ------------------ -# JAVA_HOME - location of a JDK home dir +# Apache Maven Wrapper startup batch script, version 3.3.4 # # Optional ENV vars # ----------------- -# M2_HOME - location of maven2's installed home dir -# MAVEN_OPTS - parameters passed to the Java VM when running Maven -# e.g. to debug Maven itself, use -# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output # ---------------------------------------------------------------------------- -if [ -z "$MAVEN_SKIP_RC" ] ; then - - if [ -f /etc/mavenrc ] ; then - . /etc/mavenrc - fi +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x - if [ -f "$HOME/.mavenrc" ] ; then - . "$HOME/.mavenrc" - fi +# OS specific support. +native_path() { printf %s\\n "$1"; } +case "$(uname)" in +CYGWIN* | MINGW*) + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } + ;; +esac -fi +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + if [ -n "${JAVA_HOME-}" ]; then + if [ -x "$JAVA_HOME/jre/sh/java" ]; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACCMD="$JAVA_HOME/jre/sh/javac" + else + JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" -# OS specific support. $var _must_ be set to either true or false. -cygwin=false; -darwin=false; -mingw=false -case "`uname`" in - CYGWIN*) cygwin=true ;; - MINGW*) mingw=true;; - Darwin*) darwin=true - # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home - # See https://developer.apple.com/library/mac/qa/qa1170/_index.html - if [ -z "$JAVA_HOME" ]; then - if [ -x "/usr/libexec/java_home" ]; then - export JAVA_HOME="`/usr/libexec/java_home`" - else - export JAVA_HOME="/Library/Java/Home" + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 fi fi - ;; -esac - -if [ -z "$JAVA_HOME" ] ; then - if [ -r /etc/gentoo-release ] ; then - JAVA_HOME=`java-config --jre-home` + else + JAVACMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v java + )" || : + JAVACCMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v javac + )" || : + + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 + fi fi -fi - -if [ -z "$M2_HOME" ] ; then - ## resolve links - $0 may be a link to maven's home - PRG="$0" +} - # need this for relative symlinks - while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG="`dirname "$PRG"`/$link" - fi +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + char="${str%"${str#?}"}" + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) + str="${str#?}" done + printf %x\\n $h +} - saveddir=`pwd` +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } - M2_HOME=`dirname "$PRG"`/.. +die() { + printf %s\\n "$1" >&2 + exit 1 +} - # make it fully qualified - M2_HOME=`cd "$M2_HOME" && pwd` +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} - cd "$saveddir" - # echo Using m2 at $M2_HOME -fi +scriptDir="$(dirname "$0")" +scriptName="$(basename "$0")" + +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl=$(trim "${value-}") ;; + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; + esac +done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" + +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac -# For Cygwin, ensure paths are in UNIX format before anything is touched -if $cygwin ; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --unix "$M2_HOME"` - [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --unix "$JAVA_HOME"` - [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --unix "$CLASSPATH"` -fi +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" +} -# For Mingw, ensure paths are in UNIX format before anything is touched -if $mingw ; then - [ -n "$M2_HOME" ] && - M2_HOME="`(cd "$M2_HOME"; pwd)`" - [ -n "$JAVA_HOME" ] && - JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" fi -if [ -z "$JAVA_HOME" ]; then - javaExecutable="`which javac`" - if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then - # readlink(1) is not available as standard on Solaris 10. - readLink=`which readlink` - if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then - if $darwin ; then - javaHome="`dirname \"$javaExecutable\"`" - javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" - else - javaExecutable="`readlink -f \"$javaExecutable\"`" - fi - javaHome="`dirname \"$javaExecutable\"`" - javaHome=`expr "$javaHome" : '\(.*\)/bin'` - JAVA_HOME="$javaHome" - export JAVA_HOME - fi - fi -fi +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac -if [ -z "$JAVACMD" ] ; then - if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - else - JAVACMD="`which java`" - fi +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT +else + die "cannot create temp dir" fi -if [ ! -x "$JAVACMD" ] ; then - echo "Error: JAVA_HOME is not defined correctly." >&2 - echo " We cannot execute $JAVACMD" >&2 - exit 1 -fi +mkdir -p -- "${MAVEN_HOME%/*}" -if [ -z "$JAVA_HOME" ] ; then - echo "Warning: JAVA_HOME environment variable is not set." +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" fi -CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v -# traverses directory structure from process work directory to filesystem root -# first directory with .mvn subdirectory is considered project base directory -find_maven_basedir() { +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac - if [ -z "$1" ] - then - echo "Path not specified to find_maven_basedir" - return 1 - fi +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" +fi - basedir="$1" - wdir="$1" - while [ "$wdir" != '/' ] ; do - if [ -d "$wdir"/.mvn ] ; then - basedir=$wdir - break +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then + distributionSha256Result=true fi - # workaround for JBEAP-8937 (on Solaris 10/Sparc) - if [ -d "${wdir}" ]; then - wdir=`cd "$wdir/.."; pwd` + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true fi - # end of workaround - done - echo "${basedir}" -} - -# concatenates all lines of a file -concat_lines() { - if [ -f "$1" ]; then - echo "$(tr -s '\n' ' ' < "$1")" + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 + exit 1 fi -} - -BASE_DIR=`find_maven_basedir "$(pwd)"` -if [ -z "$BASE_DIR" ]; then - exit 1; fi -########################################################################################## -# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central -# This allows using the maven wrapper in projects that prohibit checking in binary data. -########################################################################################## -if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found .mvn/wrapper/maven-wrapper.jar" - fi +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" else - if [ "$MVNW_VERBOSE" = true ]; then - echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." - fi - if [ -n "$MVNW_REPOURL" ]; then - jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" - else - jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" - fi - while IFS="=" read key value; do - case "$key" in (wrapperUrl) jarUrl="$value"; break ;; - esac - done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" - if [ "$MVNW_VERBOSE" = true ]; then - echo "Downloading from: $jarUrl" - fi - wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" - if $cygwin; then - wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` - fi + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" +fi - if command -v wget > /dev/null; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found wget ... using wget" - fi - if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then - wget "$jarUrl" -O "$wrapperJarPath" - else - wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" - fi - elif command -v curl > /dev/null; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found curl ... using curl" - fi - if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then - curl -o "$wrapperJarPath" "$jarUrl" -f - else - curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f - fi +# Find the actual extracted directory name (handles snapshots where filename != directory name) +actualDistributionDir="" - else - if [ "$MVNW_VERBOSE" = true ]; then - echo "Falling back to using Java to download" - fi - javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" - # For Cygwin, switch paths to Windows format before running javac - if $cygwin; then - javaClass=`cygpath --path --windows "$javaClass"` - fi - if [ -e "$javaClass" ]; then - if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then - if [ "$MVNW_VERBOSE" = true ]; then - echo " - Compiling MavenWrapperDownloader.java ..." - fi - # Compiling the Java class - ("$JAVA_HOME/bin/javac" "$javaClass") - fi - if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then - # Running the downloader - if [ "$MVNW_VERBOSE" = true ]; then - echo " - Running MavenWrapperDownloader.java ..." - fi - ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") - fi - fi - fi +# First try the expected directory name (for regular distributions) +if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then + if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then + actualDistributionDir="$distributionUrlNameMain" + fi fi -########################################################################################## -# End of extension -########################################################################################## -export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} -if [ "$MVNW_VERBOSE" = true ]; then - echo $MAVEN_PROJECTBASEDIR -fi -MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" - -# For Cygwin, switch paths to Windows format before running java -if $cygwin; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --path --windows "$M2_HOME"` - [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` - [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --windows "$CLASSPATH"` - [ -n "$MAVEN_PROJECTBASEDIR" ] && - MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +# If not found, search for any directory with the Maven executable (for snapshots) +if [ -z "$actualDistributionDir" ]; then + # enable globbing to iterate over items + set +f + for dir in "$TMP_DOWNLOAD_DIR"/*; do + if [ -d "$dir" ]; then + if [ -f "$dir/bin/$MVN_CMD" ]; then + actualDistributionDir="$(basename "$dir")" + break + fi + fi + done + set -f fi -# Provide a "standardized" way to retrieve the CLI args that will -# work with both Windows and non-Windows executions. -MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" -export MAVEN_CMD_LINE_ARGS +if [ -z "$actualDistributionDir" ]; then + verbose "Contents of $TMP_DOWNLOAD_DIR:" + verbose "$(ls -la "$TMP_DOWNLOAD_DIR")" + die "Could not find Maven distribution directory in extracted archive" +fi -WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain +verbose "Found extracted Maven distribution directory: $actualDistributionDir" +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" -exec "$JAVACMD" \ - $MAVEN_OPTS \ - -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ - ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" +clean || : +exec_maven "$@" diff --git a/projects-parent/archetypes-parent/apptbook-web-archetype/src/main/resources/archetype-resources/mvnw.cmd b/projects-parent/archetypes-parent/apptbook-web-archetype/src/main/resources/archetype-resources/mvnw.cmd index 249bdf382..92450f932 100644 --- a/projects-parent/archetypes-parent/apptbook-web-archetype/src/main/resources/archetype-resources/mvnw.cmd +++ b/projects-parent/archetypes-parent/apptbook-web-archetype/src/main/resources/archetype-resources/mvnw.cmd @@ -19,7 +19,7 @@ @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- -@REM Apache Maven Wrapper startup batch script, version 3.3.2 +@REM Apache Maven Wrapper startup batch script, version 3.3.4 @REM @REM Optional ENV vars @REM MVNW_REPOURL - repo url base for downloading maven distribution @@ -40,7 +40,7 @@ @SET __MVNW_ARG0_NAME__= @SET MVNW_USERNAME= @SET MVNW_PASSWORD= -@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) +@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*) @echo Cannot start maven from wrapper >&2 && exit /b 1 @GOTO :EOF : end batch / begin powershell #> @@ -73,16 +73,30 @@ switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { # apply MVNW_REPOURL and calculate MAVEN_HOME # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ if ($env:MVNW_REPOURL) { - $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } - $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" + $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')" } $distributionUrlName = $distributionUrl -replace '^.*/','' $distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' -$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" + +$MAVEN_M2_PATH = "$HOME/.m2" if ($env:MAVEN_USER_HOME) { - $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" + $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME" +} + +if (-not (Test-Path -Path $MAVEN_M2_PATH)) { + New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null +} + +$MAVEN_WRAPPER_DISTS = $null +if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) { + $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists" +} else { + $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists" } -$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' + +$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain" +$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' $MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { @@ -134,7 +148,33 @@ if ($distributionSha256Sum) { # unzip and move Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null -Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +$actualDistributionDir = "" + +# First try the expected directory name (for regular distributions) +$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain" +$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD" +if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) { + $actualDistributionDir = $distributionUrlNameMain +} + +# If not found, search for any directory with the Maven executable (for snapshots) +if (!$actualDistributionDir) { + Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object { + $testPath = Join-Path $_.FullName "bin/$MVN_CMD" + if (Test-Path -Path $testPath -PathType Leaf) { + $actualDistributionDir = $_.Name + } + } +} + +if (!$actualDistributionDir) { + Write-Error "Could not find Maven distribution directory in extracted archive" +} + +Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir" +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null try { Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null } catch { diff --git a/projects-parent/archetypes-parent/java-koans-archetype/src/main/resources/archetype-resources/.mvn/wrapper/maven-wrapper.properties b/projects-parent/archetypes-parent/java-koans-archetype/src/main/resources/archetype-resources/.mvn/wrapper/maven-wrapper.properties index 20c81b37a..c6bdae900 100644 --- a/projects-parent/archetypes-parent/java-koans-archetype/src/main/resources/archetype-resources/.mvn/wrapper/maven-wrapper.properties +++ b/projects-parent/archetypes-parent/java-koans-archetype/src/main/resources/archetype-resources/.mvn/wrapper/maven-wrapper.properties @@ -17,6 +17,6 @@ ${symbol_pound} "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY ${symbol_pound} KIND, either express or implied. See the License for the ${symbol_pound} specific language governing permissions and limitations ${symbol_pound} under the License. -wrapperVersion=3.3.2 +wrapperVersion=3.3.4 distributionType=only-script distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip diff --git a/projects-parent/archetypes-parent/java-koans-archetype/src/main/resources/archetype-resources/mvnw b/projects-parent/archetypes-parent/java-koans-archetype/src/main/resources/archetype-resources/mvnw index 19529ddf8..bd8896bf2 100755 --- a/projects-parent/archetypes-parent/java-koans-archetype/src/main/resources/archetype-resources/mvnw +++ b/projects-parent/archetypes-parent/java-koans-archetype/src/main/resources/archetype-resources/mvnw @@ -19,7 +19,7 @@ # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- -# Apache Maven Wrapper startup batch script, version 3.3.2 +# Apache Maven Wrapper startup batch script, version 3.3.4 # # Optional ENV vars # ----------------- @@ -105,14 +105,17 @@ trim() { printf "%s" "${1}" | tr -d '[:space:]' } +scriptDir="$(dirname "$0")" +scriptName="$(basename "$0")" + # parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties while IFS="=" read -r key value; do case "${key-}" in distributionUrl) distributionUrl=$(trim "${value-}") ;; distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; esac -done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" -[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties" +done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" case "${distributionUrl##*/}" in maven-mvnd-*bin.*) @@ -130,7 +133,7 @@ maven-mvnd-*bin.*) distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" ;; maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; -*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; esac # apply MVNW_REPOURL and calculate MAVEN_HOME @@ -227,7 +230,7 @@ if [ -n "${distributionSha256Sum-}" ]; then echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 exit 1 elif command -v sha256sum >/dev/null; then - if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then distributionSha256Result=true fi elif command -v shasum >/dev/null; then @@ -252,8 +255,41 @@ if command -v unzip >/dev/null; then else tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" fi -printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" -mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +actualDistributionDir="" + +# First try the expected directory name (for regular distributions) +if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then + if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then + actualDistributionDir="$distributionUrlNameMain" + fi +fi + +# If not found, search for any directory with the Maven executable (for snapshots) +if [ -z "$actualDistributionDir" ]; then + # enable globbing to iterate over items + set +f + for dir in "$TMP_DOWNLOAD_DIR"/*; do + if [ -d "$dir" ]; then + if [ -f "$dir/bin/$MVN_CMD" ]; then + actualDistributionDir="$(basename "$dir")" + break + fi + fi + done + set -f +fi + +if [ -z "$actualDistributionDir" ]; then + verbose "Contents of $TMP_DOWNLOAD_DIR:" + verbose "$(ls -la "$TMP_DOWNLOAD_DIR")" + die "Could not find Maven distribution directory in extracted archive" +fi + +verbose "Found extracted Maven distribution directory: $actualDistributionDir" +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" clean || : exec_maven "$@" diff --git a/projects-parent/archetypes-parent/java-koans-archetype/src/main/resources/archetype-resources/mvnw.cmd b/projects-parent/archetypes-parent/java-koans-archetype/src/main/resources/archetype-resources/mvnw.cmd index 249bdf382..92450f932 100644 --- a/projects-parent/archetypes-parent/java-koans-archetype/src/main/resources/archetype-resources/mvnw.cmd +++ b/projects-parent/archetypes-parent/java-koans-archetype/src/main/resources/archetype-resources/mvnw.cmd @@ -19,7 +19,7 @@ @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- -@REM Apache Maven Wrapper startup batch script, version 3.3.2 +@REM Apache Maven Wrapper startup batch script, version 3.3.4 @REM @REM Optional ENV vars @REM MVNW_REPOURL - repo url base for downloading maven distribution @@ -40,7 +40,7 @@ @SET __MVNW_ARG0_NAME__= @SET MVNW_USERNAME= @SET MVNW_PASSWORD= -@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) +@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*) @echo Cannot start maven from wrapper >&2 && exit /b 1 @GOTO :EOF : end batch / begin powershell #> @@ -73,16 +73,30 @@ switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { # apply MVNW_REPOURL and calculate MAVEN_HOME # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ if ($env:MVNW_REPOURL) { - $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } - $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" + $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')" } $distributionUrlName = $distributionUrl -replace '^.*/','' $distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' -$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" + +$MAVEN_M2_PATH = "$HOME/.m2" if ($env:MAVEN_USER_HOME) { - $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" + $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME" +} + +if (-not (Test-Path -Path $MAVEN_M2_PATH)) { + New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null +} + +$MAVEN_WRAPPER_DISTS = $null +if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) { + $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists" +} else { + $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists" } -$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' + +$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain" +$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' $MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { @@ -134,7 +148,33 @@ if ($distributionSha256Sum) { # unzip and move Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null -Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +$actualDistributionDir = "" + +# First try the expected directory name (for regular distributions) +$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain" +$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD" +if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) { + $actualDistributionDir = $distributionUrlNameMain +} + +# If not found, search for any directory with the Maven executable (for snapshots) +if (!$actualDistributionDir) { + Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object { + $testPath = Join-Path $_.FullName "bin/$MVN_CMD" + if (Test-Path -Path $testPath -PathType Leaf) { + $actualDistributionDir = $_.Name + } + } +} + +if (!$actualDistributionDir) { + Write-Error "Could not find Maven distribution directory in extracted archive" +} + +Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir" +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null try { Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null } catch { diff --git a/projects-parent/archetypes-parent/kata-archetype/src/main/resources/archetype-resources/.mvn/wrapper/maven-wrapper.properties b/projects-parent/archetypes-parent/kata-archetype/src/main/resources/archetype-resources/.mvn/wrapper/maven-wrapper.properties index 20c81b37a..c6bdae900 100644 --- a/projects-parent/archetypes-parent/kata-archetype/src/main/resources/archetype-resources/.mvn/wrapper/maven-wrapper.properties +++ b/projects-parent/archetypes-parent/kata-archetype/src/main/resources/archetype-resources/.mvn/wrapper/maven-wrapper.properties @@ -17,6 +17,6 @@ ${symbol_pound} "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY ${symbol_pound} KIND, either express or implied. See the License for the ${symbol_pound} specific language governing permissions and limitations ${symbol_pound} under the License. -wrapperVersion=3.3.2 +wrapperVersion=3.3.4 distributionType=only-script distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip diff --git a/projects-parent/archetypes-parent/kata-archetype/src/main/resources/archetype-resources/mvnw b/projects-parent/archetypes-parent/kata-archetype/src/main/resources/archetype-resources/mvnw index 41c0f0c23..bd8896bf2 100644 --- a/projects-parent/archetypes-parent/kata-archetype/src/main/resources/archetype-resources/mvnw +++ b/projects-parent/archetypes-parent/kata-archetype/src/main/resources/archetype-resources/mvnw @@ -19,292 +19,277 @@ # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- -# Maven Start Up Batch script -# -# Required ENV vars: -# ------------------ -# JAVA_HOME - location of a JDK home dir +# Apache Maven Wrapper startup batch script, version 3.3.4 # # Optional ENV vars # ----------------- -# M2_HOME - location of maven2's installed home dir -# MAVEN_OPTS - parameters passed to the Java VM when running Maven -# e.g. to debug Maven itself, use -# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output # ---------------------------------------------------------------------------- -if [ -z "$MAVEN_SKIP_RC" ] ; then - - if [ -f /etc/mavenrc ] ; then - . /etc/mavenrc - fi +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x - if [ -f "$HOME/.mavenrc" ] ; then - . "$HOME/.mavenrc" - fi +# OS specific support. +native_path() { printf %s\\n "$1"; } +case "$(uname)" in +CYGWIN* | MINGW*) + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } + ;; +esac -fi +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + if [ -n "${JAVA_HOME-}" ]; then + if [ -x "$JAVA_HOME/jre/sh/java" ]; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACCMD="$JAVA_HOME/jre/sh/javac" + else + JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" -# OS specific support. $var _must_ be set to either true or false. -cygwin=false; -darwin=false; -mingw=false -case "`uname`" in - CYGWIN*) cygwin=true ;; - MINGW*) mingw=true;; - Darwin*) darwin=true - # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home - # See https://developer.apple.com/library/mac/qa/qa1170/_index.html - if [ -z "$JAVA_HOME" ]; then - if [ -x "/usr/libexec/java_home" ]; then - export JAVA_HOME="`/usr/libexec/java_home`" - else - export JAVA_HOME="/Library/Java/Home" + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 fi fi - ;; -esac - -if [ -z "$JAVA_HOME" ] ; then - if [ -r /etc/gentoo-release ] ; then - JAVA_HOME=`java-config --jre-home` + else + JAVACMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v java + )" || : + JAVACCMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v javac + )" || : + + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 + fi fi -fi - -if [ -z "$M2_HOME" ] ; then - ## resolve links - $0 may be a link to maven's home - PRG="$0" +} - # need this for relative symlinks - while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG="`dirname "$PRG"`/$link" - fi +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + char="${str%"${str#?}"}" + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) + str="${str#?}" done + printf %x\\n $h +} - saveddir=`pwd` +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } - M2_HOME=`dirname "$PRG"`/.. +die() { + printf %s\\n "$1" >&2 + exit 1 +} - # make it fully qualified - M2_HOME=`cd "$M2_HOME" && pwd` +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} - cd "$saveddir" - # echo Using m2 at $M2_HOME -fi +scriptDir="$(dirname "$0")" +scriptName="$(basename "$0")" + +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl=$(trim "${value-}") ;; + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; + esac +done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" + +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac -# For Cygwin, ensure paths are in UNIX format before anything is touched -if $cygwin ; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --unix "$M2_HOME"` - [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --unix "$JAVA_HOME"` - [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --unix "$CLASSPATH"` -fi +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" +} -# For Mingw, ensure paths are in UNIX format before anything is touched -if $mingw ; then - [ -n "$M2_HOME" ] && - M2_HOME="`(cd "$M2_HOME"; pwd)`" - [ -n "$JAVA_HOME" ] && - JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" fi -if [ -z "$JAVA_HOME" ]; then - javaExecutable="`which javac`" - if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then - # readlink(1) is not available as standard on Solaris 10. - readLink=`which readlink` - if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then - if $darwin ; then - javaHome="`dirname \"$javaExecutable\"`" - javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" - else - javaExecutable="`readlink -f \"$javaExecutable\"`" - fi - javaHome="`dirname \"$javaExecutable\"`" - javaHome=`expr "$javaHome" : '\(.*\)/bin'` - JAVA_HOME="$javaHome" - export JAVA_HOME - fi - fi -fi +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac -if [ -z "$JAVACMD" ] ; then - if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - else - JAVACMD="`which java`" - fi +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT +else + die "cannot create temp dir" fi -if [ ! -x "$JAVACMD" ] ; then - echo "Error: JAVA_HOME is not defined correctly." >&2 - echo " We cannot execute $JAVACMD" >&2 - exit 1 -fi +mkdir -p -- "${MAVEN_HOME%/*}" -if [ -z "$JAVA_HOME" ] ; then - echo "Warning: JAVA_HOME environment variable is not set." +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" fi -CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v -# traverses directory structure from process work directory to filesystem root -# first directory with .mvn subdirectory is considered project base directory -find_maven_basedir() { +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac - if [ -z "$1" ] - then - echo "Path not specified to find_maven_basedir" - return 1 - fi +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" +fi - basedir="$1" - wdir="$1" - while [ "$wdir" != '/' ] ; do - if [ -d "$wdir"/.mvn ] ; then - basedir=$wdir - break +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then + distributionSha256Result=true fi - # workaround for JBEAP-8937 (on Solaris 10/Sparc) - if [ -d "${wdir}" ]; then - wdir=`cd "$wdir/.."; pwd` + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true fi - # end of workaround - done - echo "${basedir}" -} - -# concatenates all lines of a file -concat_lines() { - if [ -f "$1" ]; then - echo "$(tr -s '\n' ' ' < "$1")" + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 + exit 1 fi -} - -BASE_DIR=`find_maven_basedir "$(pwd)"` -if [ -z "$BASE_DIR" ]; then - exit 1; fi -########################################################################################## -# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central -# This allows using the maven wrapper in projects that prohibit checking in binary data. -########################################################################################## -if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found .mvn/wrapper/maven-wrapper.jar" - fi +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" else - if [ "$MVNW_VERBOSE" = true ]; then - echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." - fi - if [ -n "$MVNW_REPOURL" ]; then - jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" - else - jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" - fi - while IFS="=" read key value; do - case "$key" in (wrapperUrl) jarUrl="$value"; break ;; - esac - done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" - if [ "$MVNW_VERBOSE" = true ]; then - echo "Downloading from: $jarUrl" - fi - wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" - if $cygwin; then - wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` - fi + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" +fi - if command -v wget > /dev/null; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found wget ... using wget" - fi - if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then - wget "$jarUrl" -O "$wrapperJarPath" - else - wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" - fi - elif command -v curl > /dev/null; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found curl ... using curl" - fi - if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then - curl -o "$wrapperJarPath" "$jarUrl" -f - else - curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f - fi +# Find the actual extracted directory name (handles snapshots where filename != directory name) +actualDistributionDir="" - else - if [ "$MVNW_VERBOSE" = true ]; then - echo "Falling back to using Java to download" - fi - javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" - # For Cygwin, switch paths to Windows format before running javac - if $cygwin; then - javaClass=`cygpath --path --windows "$javaClass"` - fi - if [ -e "$javaClass" ]; then - if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then - if [ "$MVNW_VERBOSE" = true ]; then - echo " - Compiling MavenWrapperDownloader.java ..." - fi - # Compiling the Java class - ("$JAVA_HOME/bin/javac" "$javaClass") - fi - if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then - # Running the downloader - if [ "$MVNW_VERBOSE" = true ]; then - echo " - Running MavenWrapperDownloader.java ..." - fi - ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") - fi - fi - fi +# First try the expected directory name (for regular distributions) +if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then + if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then + actualDistributionDir="$distributionUrlNameMain" + fi fi -########################################################################################## -# End of extension -########################################################################################## -export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} -if [ "$MVNW_VERBOSE" = true ]; then - echo $MAVEN_PROJECTBASEDIR -fi -MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" - -# For Cygwin, switch paths to Windows format before running java -if $cygwin; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --path --windows "$M2_HOME"` - [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` - [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --windows "$CLASSPATH"` - [ -n "$MAVEN_PROJECTBASEDIR" ] && - MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +# If not found, search for any directory with the Maven executable (for snapshots) +if [ -z "$actualDistributionDir" ]; then + # enable globbing to iterate over items + set +f + for dir in "$TMP_DOWNLOAD_DIR"/*; do + if [ -d "$dir" ]; then + if [ -f "$dir/bin/$MVN_CMD" ]; then + actualDistributionDir="$(basename "$dir")" + break + fi + fi + done + set -f fi -# Provide a "standardized" way to retrieve the CLI args that will -# work with both Windows and non-Windows executions. -MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" -export MAVEN_CMD_LINE_ARGS +if [ -z "$actualDistributionDir" ]; then + verbose "Contents of $TMP_DOWNLOAD_DIR:" + verbose "$(ls -la "$TMP_DOWNLOAD_DIR")" + die "Could not find Maven distribution directory in extracted archive" +fi -WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain +verbose "Found extracted Maven distribution directory: $actualDistributionDir" +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" -exec "$JAVACMD" \ - $MAVEN_OPTS \ - -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ - ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" +clean || : +exec_maven "$@" diff --git a/projects-parent/archetypes-parent/kata-archetype/src/main/resources/archetype-resources/mvnw.cmd b/projects-parent/archetypes-parent/kata-archetype/src/main/resources/archetype-resources/mvnw.cmd index 249bdf382..92450f932 100644 --- a/projects-parent/archetypes-parent/kata-archetype/src/main/resources/archetype-resources/mvnw.cmd +++ b/projects-parent/archetypes-parent/kata-archetype/src/main/resources/archetype-resources/mvnw.cmd @@ -19,7 +19,7 @@ @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- -@REM Apache Maven Wrapper startup batch script, version 3.3.2 +@REM Apache Maven Wrapper startup batch script, version 3.3.4 @REM @REM Optional ENV vars @REM MVNW_REPOURL - repo url base for downloading maven distribution @@ -40,7 +40,7 @@ @SET __MVNW_ARG0_NAME__= @SET MVNW_USERNAME= @SET MVNW_PASSWORD= -@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) +@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*) @echo Cannot start maven from wrapper >&2 && exit /b 1 @GOTO :EOF : end batch / begin powershell #> @@ -73,16 +73,30 @@ switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { # apply MVNW_REPOURL and calculate MAVEN_HOME # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ if ($env:MVNW_REPOURL) { - $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } - $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" + $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')" } $distributionUrlName = $distributionUrl -replace '^.*/','' $distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' -$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" + +$MAVEN_M2_PATH = "$HOME/.m2" if ($env:MAVEN_USER_HOME) { - $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" + $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME" +} + +if (-not (Test-Path -Path $MAVEN_M2_PATH)) { + New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null +} + +$MAVEN_WRAPPER_DISTS = $null +if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) { + $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists" +} else { + $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists" } -$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' + +$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain" +$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' $MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { @@ -134,7 +148,33 @@ if ($distributionSha256Sum) { # unzip and move Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null -Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +$actualDistributionDir = "" + +# First try the expected directory name (for regular distributions) +$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain" +$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD" +if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) { + $actualDistributionDir = $distributionUrlNameMain +} + +# If not found, search for any directory with the Maven executable (for snapshots) +if (!$actualDistributionDir) { + Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object { + $testPath = Join-Path $_.FullName "bin/$MVN_CMD" + if (Test-Path -Path $testPath -PathType Leaf) { + $actualDistributionDir = $_.Name + } + } +} + +if (!$actualDistributionDir) { + Write-Error "Could not find Maven distribution directory in extracted archive" +} + +Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir" +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null try { Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null } catch { diff --git a/projects-parent/archetypes-parent/phonebill-archetype/src/main/resources/archetype-resources/.mvn/wrapper/maven-wrapper.properties b/projects-parent/archetypes-parent/phonebill-archetype/src/main/resources/archetype-resources/.mvn/wrapper/maven-wrapper.properties index 20c81b37a..c6bdae900 100644 --- a/projects-parent/archetypes-parent/phonebill-archetype/src/main/resources/archetype-resources/.mvn/wrapper/maven-wrapper.properties +++ b/projects-parent/archetypes-parent/phonebill-archetype/src/main/resources/archetype-resources/.mvn/wrapper/maven-wrapper.properties @@ -17,6 +17,6 @@ ${symbol_pound} "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY ${symbol_pound} KIND, either express or implied. See the License for the ${symbol_pound} specific language governing permissions and limitations ${symbol_pound} under the License. -wrapperVersion=3.3.2 +wrapperVersion=3.3.4 distributionType=only-script distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip diff --git a/projects-parent/archetypes-parent/phonebill-archetype/src/main/resources/archetype-resources/mvnw b/projects-parent/archetypes-parent/phonebill-archetype/src/main/resources/archetype-resources/mvnw index 41c0f0c23..bd8896bf2 100755 --- a/projects-parent/archetypes-parent/phonebill-archetype/src/main/resources/archetype-resources/mvnw +++ b/projects-parent/archetypes-parent/phonebill-archetype/src/main/resources/archetype-resources/mvnw @@ -19,292 +19,277 @@ # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- -# Maven Start Up Batch script -# -# Required ENV vars: -# ------------------ -# JAVA_HOME - location of a JDK home dir +# Apache Maven Wrapper startup batch script, version 3.3.4 # # Optional ENV vars # ----------------- -# M2_HOME - location of maven2's installed home dir -# MAVEN_OPTS - parameters passed to the Java VM when running Maven -# e.g. to debug Maven itself, use -# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output # ---------------------------------------------------------------------------- -if [ -z "$MAVEN_SKIP_RC" ] ; then - - if [ -f /etc/mavenrc ] ; then - . /etc/mavenrc - fi +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x - if [ -f "$HOME/.mavenrc" ] ; then - . "$HOME/.mavenrc" - fi +# OS specific support. +native_path() { printf %s\\n "$1"; } +case "$(uname)" in +CYGWIN* | MINGW*) + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } + ;; +esac -fi +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + if [ -n "${JAVA_HOME-}" ]; then + if [ -x "$JAVA_HOME/jre/sh/java" ]; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACCMD="$JAVA_HOME/jre/sh/javac" + else + JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" -# OS specific support. $var _must_ be set to either true or false. -cygwin=false; -darwin=false; -mingw=false -case "`uname`" in - CYGWIN*) cygwin=true ;; - MINGW*) mingw=true;; - Darwin*) darwin=true - # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home - # See https://developer.apple.com/library/mac/qa/qa1170/_index.html - if [ -z "$JAVA_HOME" ]; then - if [ -x "/usr/libexec/java_home" ]; then - export JAVA_HOME="`/usr/libexec/java_home`" - else - export JAVA_HOME="/Library/Java/Home" + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 fi fi - ;; -esac - -if [ -z "$JAVA_HOME" ] ; then - if [ -r /etc/gentoo-release ] ; then - JAVA_HOME=`java-config --jre-home` + else + JAVACMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v java + )" || : + JAVACCMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v javac + )" || : + + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 + fi fi -fi - -if [ -z "$M2_HOME" ] ; then - ## resolve links - $0 may be a link to maven's home - PRG="$0" +} - # need this for relative symlinks - while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG="`dirname "$PRG"`/$link" - fi +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + char="${str%"${str#?}"}" + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) + str="${str#?}" done + printf %x\\n $h +} - saveddir=`pwd` +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } - M2_HOME=`dirname "$PRG"`/.. +die() { + printf %s\\n "$1" >&2 + exit 1 +} - # make it fully qualified - M2_HOME=`cd "$M2_HOME" && pwd` +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} - cd "$saveddir" - # echo Using m2 at $M2_HOME -fi +scriptDir="$(dirname "$0")" +scriptName="$(basename "$0")" + +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl=$(trim "${value-}") ;; + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; + esac +done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" + +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac -# For Cygwin, ensure paths are in UNIX format before anything is touched -if $cygwin ; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --unix "$M2_HOME"` - [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --unix "$JAVA_HOME"` - [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --unix "$CLASSPATH"` -fi +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" +} -# For Mingw, ensure paths are in UNIX format before anything is touched -if $mingw ; then - [ -n "$M2_HOME" ] && - M2_HOME="`(cd "$M2_HOME"; pwd)`" - [ -n "$JAVA_HOME" ] && - JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" fi -if [ -z "$JAVA_HOME" ]; then - javaExecutable="`which javac`" - if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then - # readlink(1) is not available as standard on Solaris 10. - readLink=`which readlink` - if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then - if $darwin ; then - javaHome="`dirname \"$javaExecutable\"`" - javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" - else - javaExecutable="`readlink -f \"$javaExecutable\"`" - fi - javaHome="`dirname \"$javaExecutable\"`" - javaHome=`expr "$javaHome" : '\(.*\)/bin'` - JAVA_HOME="$javaHome" - export JAVA_HOME - fi - fi -fi +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac -if [ -z "$JAVACMD" ] ; then - if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - else - JAVACMD="`which java`" - fi +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT +else + die "cannot create temp dir" fi -if [ ! -x "$JAVACMD" ] ; then - echo "Error: JAVA_HOME is not defined correctly." >&2 - echo " We cannot execute $JAVACMD" >&2 - exit 1 -fi +mkdir -p -- "${MAVEN_HOME%/*}" -if [ -z "$JAVA_HOME" ] ; then - echo "Warning: JAVA_HOME environment variable is not set." +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" fi -CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v -# traverses directory structure from process work directory to filesystem root -# first directory with .mvn subdirectory is considered project base directory -find_maven_basedir() { +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac - if [ -z "$1" ] - then - echo "Path not specified to find_maven_basedir" - return 1 - fi +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" +fi - basedir="$1" - wdir="$1" - while [ "$wdir" != '/' ] ; do - if [ -d "$wdir"/.mvn ] ; then - basedir=$wdir - break +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then + distributionSha256Result=true fi - # workaround for JBEAP-8937 (on Solaris 10/Sparc) - if [ -d "${wdir}" ]; then - wdir=`cd "$wdir/.."; pwd` + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true fi - # end of workaround - done - echo "${basedir}" -} - -# concatenates all lines of a file -concat_lines() { - if [ -f "$1" ]; then - echo "$(tr -s '\n' ' ' < "$1")" + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 + exit 1 fi -} - -BASE_DIR=`find_maven_basedir "$(pwd)"` -if [ -z "$BASE_DIR" ]; then - exit 1; fi -########################################################################################## -# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central -# This allows using the maven wrapper in projects that prohibit checking in binary data. -########################################################################################## -if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found .mvn/wrapper/maven-wrapper.jar" - fi +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" else - if [ "$MVNW_VERBOSE" = true ]; then - echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." - fi - if [ -n "$MVNW_REPOURL" ]; then - jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" - else - jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" - fi - while IFS="=" read key value; do - case "$key" in (wrapperUrl) jarUrl="$value"; break ;; - esac - done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" - if [ "$MVNW_VERBOSE" = true ]; then - echo "Downloading from: $jarUrl" - fi - wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" - if $cygwin; then - wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` - fi + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" +fi - if command -v wget > /dev/null; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found wget ... using wget" - fi - if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then - wget "$jarUrl" -O "$wrapperJarPath" - else - wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" - fi - elif command -v curl > /dev/null; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found curl ... using curl" - fi - if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then - curl -o "$wrapperJarPath" "$jarUrl" -f - else - curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f - fi +# Find the actual extracted directory name (handles snapshots where filename != directory name) +actualDistributionDir="" - else - if [ "$MVNW_VERBOSE" = true ]; then - echo "Falling back to using Java to download" - fi - javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" - # For Cygwin, switch paths to Windows format before running javac - if $cygwin; then - javaClass=`cygpath --path --windows "$javaClass"` - fi - if [ -e "$javaClass" ]; then - if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then - if [ "$MVNW_VERBOSE" = true ]; then - echo " - Compiling MavenWrapperDownloader.java ..." - fi - # Compiling the Java class - ("$JAVA_HOME/bin/javac" "$javaClass") - fi - if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then - # Running the downloader - if [ "$MVNW_VERBOSE" = true ]; then - echo " - Running MavenWrapperDownloader.java ..." - fi - ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") - fi - fi - fi +# First try the expected directory name (for regular distributions) +if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then + if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then + actualDistributionDir="$distributionUrlNameMain" + fi fi -########################################################################################## -# End of extension -########################################################################################## -export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} -if [ "$MVNW_VERBOSE" = true ]; then - echo $MAVEN_PROJECTBASEDIR -fi -MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" - -# For Cygwin, switch paths to Windows format before running java -if $cygwin; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --path --windows "$M2_HOME"` - [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` - [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --windows "$CLASSPATH"` - [ -n "$MAVEN_PROJECTBASEDIR" ] && - MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +# If not found, search for any directory with the Maven executable (for snapshots) +if [ -z "$actualDistributionDir" ]; then + # enable globbing to iterate over items + set +f + for dir in "$TMP_DOWNLOAD_DIR"/*; do + if [ -d "$dir" ]; then + if [ -f "$dir/bin/$MVN_CMD" ]; then + actualDistributionDir="$(basename "$dir")" + break + fi + fi + done + set -f fi -# Provide a "standardized" way to retrieve the CLI args that will -# work with both Windows and non-Windows executions. -MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" -export MAVEN_CMD_LINE_ARGS +if [ -z "$actualDistributionDir" ]; then + verbose "Contents of $TMP_DOWNLOAD_DIR:" + verbose "$(ls -la "$TMP_DOWNLOAD_DIR")" + die "Could not find Maven distribution directory in extracted archive" +fi -WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain +verbose "Found extracted Maven distribution directory: $actualDistributionDir" +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" -exec "$JAVACMD" \ - $MAVEN_OPTS \ - -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ - ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" +clean || : +exec_maven "$@" diff --git a/projects-parent/archetypes-parent/phonebill-archetype/src/main/resources/archetype-resources/mvnw.cmd b/projects-parent/archetypes-parent/phonebill-archetype/src/main/resources/archetype-resources/mvnw.cmd index 249bdf382..92450f932 100644 --- a/projects-parent/archetypes-parent/phonebill-archetype/src/main/resources/archetype-resources/mvnw.cmd +++ b/projects-parent/archetypes-parent/phonebill-archetype/src/main/resources/archetype-resources/mvnw.cmd @@ -19,7 +19,7 @@ @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- -@REM Apache Maven Wrapper startup batch script, version 3.3.2 +@REM Apache Maven Wrapper startup batch script, version 3.3.4 @REM @REM Optional ENV vars @REM MVNW_REPOURL - repo url base for downloading maven distribution @@ -40,7 +40,7 @@ @SET __MVNW_ARG0_NAME__= @SET MVNW_USERNAME= @SET MVNW_PASSWORD= -@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) +@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*) @echo Cannot start maven from wrapper >&2 && exit /b 1 @GOTO :EOF : end batch / begin powershell #> @@ -73,16 +73,30 @@ switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { # apply MVNW_REPOURL and calculate MAVEN_HOME # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ if ($env:MVNW_REPOURL) { - $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } - $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" + $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')" } $distributionUrlName = $distributionUrl -replace '^.*/','' $distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' -$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" + +$MAVEN_M2_PATH = "$HOME/.m2" if ($env:MAVEN_USER_HOME) { - $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" + $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME" +} + +if (-not (Test-Path -Path $MAVEN_M2_PATH)) { + New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null +} + +$MAVEN_WRAPPER_DISTS = $null +if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) { + $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists" +} else { + $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists" } -$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' + +$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain" +$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' $MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { @@ -134,7 +148,33 @@ if ($distributionSha256Sum) { # unzip and move Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null -Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +$actualDistributionDir = "" + +# First try the expected directory name (for regular distributions) +$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain" +$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD" +if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) { + $actualDistributionDir = $distributionUrlNameMain +} + +# If not found, search for any directory with the Maven executable (for snapshots) +if (!$actualDistributionDir) { + Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object { + $testPath = Join-Path $_.FullName "bin/$MVN_CMD" + if (Test-Path -Path $testPath -PathType Leaf) { + $actualDistributionDir = $_.Name + } + } +} + +if (!$actualDistributionDir) { + Write-Error "Could not find Maven distribution directory in extracted archive" +} + +Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir" +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null try { Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null } catch { diff --git a/projects-parent/archetypes-parent/phonebill-web-archetype/src/main/resources/archetype-resources/.mvn/wrapper/maven-wrapper.properties b/projects-parent/archetypes-parent/phonebill-web-archetype/src/main/resources/archetype-resources/.mvn/wrapper/maven-wrapper.properties index 20c81b37a..c6bdae900 100644 --- a/projects-parent/archetypes-parent/phonebill-web-archetype/src/main/resources/archetype-resources/.mvn/wrapper/maven-wrapper.properties +++ b/projects-parent/archetypes-parent/phonebill-web-archetype/src/main/resources/archetype-resources/.mvn/wrapper/maven-wrapper.properties @@ -17,6 +17,6 @@ ${symbol_pound} "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY ${symbol_pound} KIND, either express or implied. See the License for the ${symbol_pound} specific language governing permissions and limitations ${symbol_pound} under the License. -wrapperVersion=3.3.2 +wrapperVersion=3.3.4 distributionType=only-script distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip diff --git a/projects-parent/archetypes-parent/phonebill-web-archetype/src/main/resources/archetype-resources/mvnw b/projects-parent/archetypes-parent/phonebill-web-archetype/src/main/resources/archetype-resources/mvnw index 41c0f0c23..bd8896bf2 100755 --- a/projects-parent/archetypes-parent/phonebill-web-archetype/src/main/resources/archetype-resources/mvnw +++ b/projects-parent/archetypes-parent/phonebill-web-archetype/src/main/resources/archetype-resources/mvnw @@ -19,292 +19,277 @@ # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- -# Maven Start Up Batch script -# -# Required ENV vars: -# ------------------ -# JAVA_HOME - location of a JDK home dir +# Apache Maven Wrapper startup batch script, version 3.3.4 # # Optional ENV vars # ----------------- -# M2_HOME - location of maven2's installed home dir -# MAVEN_OPTS - parameters passed to the Java VM when running Maven -# e.g. to debug Maven itself, use -# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output # ---------------------------------------------------------------------------- -if [ -z "$MAVEN_SKIP_RC" ] ; then - - if [ -f /etc/mavenrc ] ; then - . /etc/mavenrc - fi +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x - if [ -f "$HOME/.mavenrc" ] ; then - . "$HOME/.mavenrc" - fi +# OS specific support. +native_path() { printf %s\\n "$1"; } +case "$(uname)" in +CYGWIN* | MINGW*) + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } + ;; +esac -fi +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + if [ -n "${JAVA_HOME-}" ]; then + if [ -x "$JAVA_HOME/jre/sh/java" ]; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACCMD="$JAVA_HOME/jre/sh/javac" + else + JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" -# OS specific support. $var _must_ be set to either true or false. -cygwin=false; -darwin=false; -mingw=false -case "`uname`" in - CYGWIN*) cygwin=true ;; - MINGW*) mingw=true;; - Darwin*) darwin=true - # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home - # See https://developer.apple.com/library/mac/qa/qa1170/_index.html - if [ -z "$JAVA_HOME" ]; then - if [ -x "/usr/libexec/java_home" ]; then - export JAVA_HOME="`/usr/libexec/java_home`" - else - export JAVA_HOME="/Library/Java/Home" + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 fi fi - ;; -esac - -if [ -z "$JAVA_HOME" ] ; then - if [ -r /etc/gentoo-release ] ; then - JAVA_HOME=`java-config --jre-home` + else + JAVACMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v java + )" || : + JAVACCMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v javac + )" || : + + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 + fi fi -fi - -if [ -z "$M2_HOME" ] ; then - ## resolve links - $0 may be a link to maven's home - PRG="$0" +} - # need this for relative symlinks - while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG="`dirname "$PRG"`/$link" - fi +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + char="${str%"${str#?}"}" + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) + str="${str#?}" done + printf %x\\n $h +} - saveddir=`pwd` +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } - M2_HOME=`dirname "$PRG"`/.. +die() { + printf %s\\n "$1" >&2 + exit 1 +} - # make it fully qualified - M2_HOME=`cd "$M2_HOME" && pwd` +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} - cd "$saveddir" - # echo Using m2 at $M2_HOME -fi +scriptDir="$(dirname "$0")" +scriptName="$(basename "$0")" + +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl=$(trim "${value-}") ;; + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; + esac +done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" + +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac -# For Cygwin, ensure paths are in UNIX format before anything is touched -if $cygwin ; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --unix "$M2_HOME"` - [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --unix "$JAVA_HOME"` - [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --unix "$CLASSPATH"` -fi +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" +} -# For Mingw, ensure paths are in UNIX format before anything is touched -if $mingw ; then - [ -n "$M2_HOME" ] && - M2_HOME="`(cd "$M2_HOME"; pwd)`" - [ -n "$JAVA_HOME" ] && - JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" fi -if [ -z "$JAVA_HOME" ]; then - javaExecutable="`which javac`" - if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then - # readlink(1) is not available as standard on Solaris 10. - readLink=`which readlink` - if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then - if $darwin ; then - javaHome="`dirname \"$javaExecutable\"`" - javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" - else - javaExecutable="`readlink -f \"$javaExecutable\"`" - fi - javaHome="`dirname \"$javaExecutable\"`" - javaHome=`expr "$javaHome" : '\(.*\)/bin'` - JAVA_HOME="$javaHome" - export JAVA_HOME - fi - fi -fi +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac -if [ -z "$JAVACMD" ] ; then - if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - else - JAVACMD="`which java`" - fi +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT +else + die "cannot create temp dir" fi -if [ ! -x "$JAVACMD" ] ; then - echo "Error: JAVA_HOME is not defined correctly." >&2 - echo " We cannot execute $JAVACMD" >&2 - exit 1 -fi +mkdir -p -- "${MAVEN_HOME%/*}" -if [ -z "$JAVA_HOME" ] ; then - echo "Warning: JAVA_HOME environment variable is not set." +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" fi -CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v -# traverses directory structure from process work directory to filesystem root -# first directory with .mvn subdirectory is considered project base directory -find_maven_basedir() { +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac - if [ -z "$1" ] - then - echo "Path not specified to find_maven_basedir" - return 1 - fi +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" +fi - basedir="$1" - wdir="$1" - while [ "$wdir" != '/' ] ; do - if [ -d "$wdir"/.mvn ] ; then - basedir=$wdir - break +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then + distributionSha256Result=true fi - # workaround for JBEAP-8937 (on Solaris 10/Sparc) - if [ -d "${wdir}" ]; then - wdir=`cd "$wdir/.."; pwd` + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true fi - # end of workaround - done - echo "${basedir}" -} - -# concatenates all lines of a file -concat_lines() { - if [ -f "$1" ]; then - echo "$(tr -s '\n' ' ' < "$1")" + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 + exit 1 fi -} - -BASE_DIR=`find_maven_basedir "$(pwd)"` -if [ -z "$BASE_DIR" ]; then - exit 1; fi -########################################################################################## -# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central -# This allows using the maven wrapper in projects that prohibit checking in binary data. -########################################################################################## -if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found .mvn/wrapper/maven-wrapper.jar" - fi +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" else - if [ "$MVNW_VERBOSE" = true ]; then - echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." - fi - if [ -n "$MVNW_REPOURL" ]; then - jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" - else - jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" - fi - while IFS="=" read key value; do - case "$key" in (wrapperUrl) jarUrl="$value"; break ;; - esac - done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" - if [ "$MVNW_VERBOSE" = true ]; then - echo "Downloading from: $jarUrl" - fi - wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" - if $cygwin; then - wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` - fi + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" +fi - if command -v wget > /dev/null; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found wget ... using wget" - fi - if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then - wget "$jarUrl" -O "$wrapperJarPath" - else - wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" - fi - elif command -v curl > /dev/null; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found curl ... using curl" - fi - if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then - curl -o "$wrapperJarPath" "$jarUrl" -f - else - curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f - fi +# Find the actual extracted directory name (handles snapshots where filename != directory name) +actualDistributionDir="" - else - if [ "$MVNW_VERBOSE" = true ]; then - echo "Falling back to using Java to download" - fi - javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" - # For Cygwin, switch paths to Windows format before running javac - if $cygwin; then - javaClass=`cygpath --path --windows "$javaClass"` - fi - if [ -e "$javaClass" ]; then - if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then - if [ "$MVNW_VERBOSE" = true ]; then - echo " - Compiling MavenWrapperDownloader.java ..." - fi - # Compiling the Java class - ("$JAVA_HOME/bin/javac" "$javaClass") - fi - if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then - # Running the downloader - if [ "$MVNW_VERBOSE" = true ]; then - echo " - Running MavenWrapperDownloader.java ..." - fi - ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") - fi - fi - fi +# First try the expected directory name (for regular distributions) +if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then + if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then + actualDistributionDir="$distributionUrlNameMain" + fi fi -########################################################################################## -# End of extension -########################################################################################## -export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} -if [ "$MVNW_VERBOSE" = true ]; then - echo $MAVEN_PROJECTBASEDIR -fi -MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" - -# For Cygwin, switch paths to Windows format before running java -if $cygwin; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --path --windows "$M2_HOME"` - [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` - [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --windows "$CLASSPATH"` - [ -n "$MAVEN_PROJECTBASEDIR" ] && - MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +# If not found, search for any directory with the Maven executable (for snapshots) +if [ -z "$actualDistributionDir" ]; then + # enable globbing to iterate over items + set +f + for dir in "$TMP_DOWNLOAD_DIR"/*; do + if [ -d "$dir" ]; then + if [ -f "$dir/bin/$MVN_CMD" ]; then + actualDistributionDir="$(basename "$dir")" + break + fi + fi + done + set -f fi -# Provide a "standardized" way to retrieve the CLI args that will -# work with both Windows and non-Windows executions. -MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" -export MAVEN_CMD_LINE_ARGS +if [ -z "$actualDistributionDir" ]; then + verbose "Contents of $TMP_DOWNLOAD_DIR:" + verbose "$(ls -la "$TMP_DOWNLOAD_DIR")" + die "Could not find Maven distribution directory in extracted archive" +fi -WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain +verbose "Found extracted Maven distribution directory: $actualDistributionDir" +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" -exec "$JAVACMD" \ - $MAVEN_OPTS \ - -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ - ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" +clean || : +exec_maven "$@" diff --git a/projects-parent/archetypes-parent/phonebill-web-archetype/src/main/resources/archetype-resources/mvnw.cmd b/projects-parent/archetypes-parent/phonebill-web-archetype/src/main/resources/archetype-resources/mvnw.cmd index 249bdf382..92450f932 100644 --- a/projects-parent/archetypes-parent/phonebill-web-archetype/src/main/resources/archetype-resources/mvnw.cmd +++ b/projects-parent/archetypes-parent/phonebill-web-archetype/src/main/resources/archetype-resources/mvnw.cmd @@ -19,7 +19,7 @@ @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- -@REM Apache Maven Wrapper startup batch script, version 3.3.2 +@REM Apache Maven Wrapper startup batch script, version 3.3.4 @REM @REM Optional ENV vars @REM MVNW_REPOURL - repo url base for downloading maven distribution @@ -40,7 +40,7 @@ @SET __MVNW_ARG0_NAME__= @SET MVNW_USERNAME= @SET MVNW_PASSWORD= -@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) +@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*) @echo Cannot start maven from wrapper >&2 && exit /b 1 @GOTO :EOF : end batch / begin powershell #> @@ -73,16 +73,30 @@ switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { # apply MVNW_REPOURL and calculate MAVEN_HOME # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ if ($env:MVNW_REPOURL) { - $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } - $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" + $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')" } $distributionUrlName = $distributionUrl -replace '^.*/','' $distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' -$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" + +$MAVEN_M2_PATH = "$HOME/.m2" if ($env:MAVEN_USER_HOME) { - $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" + $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME" +} + +if (-not (Test-Path -Path $MAVEN_M2_PATH)) { + New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null +} + +$MAVEN_WRAPPER_DISTS = $null +if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) { + $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists" +} else { + $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists" } -$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' + +$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain" +$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' $MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { @@ -134,7 +148,33 @@ if ($distributionSha256Sum) { # unzip and move Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null -Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +$actualDistributionDir = "" + +# First try the expected directory name (for regular distributions) +$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain" +$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD" +if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) { + $actualDistributionDir = $distributionUrlNameMain +} + +# If not found, search for any directory with the Maven executable (for snapshots) +if (!$actualDistributionDir) { + Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object { + $testPath = Join-Path $_.FullName "bin/$MVN_CMD" + if (Test-Path -Path $testPath -PathType Leaf) { + $actualDistributionDir = $_.Name + } + } +} + +if (!$actualDistributionDir) { + Write-Error "Could not find Maven distribution directory in extracted archive" +} + +Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir" +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null try { Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null } catch { diff --git a/projects-parent/archetypes-parent/student-archetype/src/main/resources/archetype-resources/.mvn/wrapper/maven-wrapper.properties b/projects-parent/archetypes-parent/student-archetype/src/main/resources/archetype-resources/.mvn/wrapper/maven-wrapper.properties index 20c81b37a..c6bdae900 100644 --- a/projects-parent/archetypes-parent/student-archetype/src/main/resources/archetype-resources/.mvn/wrapper/maven-wrapper.properties +++ b/projects-parent/archetypes-parent/student-archetype/src/main/resources/archetype-resources/.mvn/wrapper/maven-wrapper.properties @@ -17,6 +17,6 @@ ${symbol_pound} "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY ${symbol_pound} KIND, either express or implied. See the License for the ${symbol_pound} specific language governing permissions and limitations ${symbol_pound} under the License. -wrapperVersion=3.3.2 +wrapperVersion=3.3.4 distributionType=only-script distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip diff --git a/projects-parent/archetypes-parent/student-archetype/src/main/resources/archetype-resources/mvnw b/projects-parent/archetypes-parent/student-archetype/src/main/resources/archetype-resources/mvnw index 41c0f0c23..bd8896bf2 100755 --- a/projects-parent/archetypes-parent/student-archetype/src/main/resources/archetype-resources/mvnw +++ b/projects-parent/archetypes-parent/student-archetype/src/main/resources/archetype-resources/mvnw @@ -19,292 +19,277 @@ # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- -# Maven Start Up Batch script -# -# Required ENV vars: -# ------------------ -# JAVA_HOME - location of a JDK home dir +# Apache Maven Wrapper startup batch script, version 3.3.4 # # Optional ENV vars # ----------------- -# M2_HOME - location of maven2's installed home dir -# MAVEN_OPTS - parameters passed to the Java VM when running Maven -# e.g. to debug Maven itself, use -# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output # ---------------------------------------------------------------------------- -if [ -z "$MAVEN_SKIP_RC" ] ; then - - if [ -f /etc/mavenrc ] ; then - . /etc/mavenrc - fi +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x - if [ -f "$HOME/.mavenrc" ] ; then - . "$HOME/.mavenrc" - fi +# OS specific support. +native_path() { printf %s\\n "$1"; } +case "$(uname)" in +CYGWIN* | MINGW*) + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } + ;; +esac -fi +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + if [ -n "${JAVA_HOME-}" ]; then + if [ -x "$JAVA_HOME/jre/sh/java" ]; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACCMD="$JAVA_HOME/jre/sh/javac" + else + JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" -# OS specific support. $var _must_ be set to either true or false. -cygwin=false; -darwin=false; -mingw=false -case "`uname`" in - CYGWIN*) cygwin=true ;; - MINGW*) mingw=true;; - Darwin*) darwin=true - # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home - # See https://developer.apple.com/library/mac/qa/qa1170/_index.html - if [ -z "$JAVA_HOME" ]; then - if [ -x "/usr/libexec/java_home" ]; then - export JAVA_HOME="`/usr/libexec/java_home`" - else - export JAVA_HOME="/Library/Java/Home" + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 fi fi - ;; -esac - -if [ -z "$JAVA_HOME" ] ; then - if [ -r /etc/gentoo-release ] ; then - JAVA_HOME=`java-config --jre-home` + else + JAVACMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v java + )" || : + JAVACCMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v javac + )" || : + + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 + fi fi -fi - -if [ -z "$M2_HOME" ] ; then - ## resolve links - $0 may be a link to maven's home - PRG="$0" +} - # need this for relative symlinks - while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG="`dirname "$PRG"`/$link" - fi +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + char="${str%"${str#?}"}" + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) + str="${str#?}" done + printf %x\\n $h +} - saveddir=`pwd` +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } - M2_HOME=`dirname "$PRG"`/.. +die() { + printf %s\\n "$1" >&2 + exit 1 +} - # make it fully qualified - M2_HOME=`cd "$M2_HOME" && pwd` +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} - cd "$saveddir" - # echo Using m2 at $M2_HOME -fi +scriptDir="$(dirname "$0")" +scriptName="$(basename "$0")" + +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl=$(trim "${value-}") ;; + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; + esac +done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" + +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac -# For Cygwin, ensure paths are in UNIX format before anything is touched -if $cygwin ; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --unix "$M2_HOME"` - [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --unix "$JAVA_HOME"` - [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --unix "$CLASSPATH"` -fi +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" +} -# For Mingw, ensure paths are in UNIX format before anything is touched -if $mingw ; then - [ -n "$M2_HOME" ] && - M2_HOME="`(cd "$M2_HOME"; pwd)`" - [ -n "$JAVA_HOME" ] && - JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" fi -if [ -z "$JAVA_HOME" ]; then - javaExecutable="`which javac`" - if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then - # readlink(1) is not available as standard on Solaris 10. - readLink=`which readlink` - if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then - if $darwin ; then - javaHome="`dirname \"$javaExecutable\"`" - javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" - else - javaExecutable="`readlink -f \"$javaExecutable\"`" - fi - javaHome="`dirname \"$javaExecutable\"`" - javaHome=`expr "$javaHome" : '\(.*\)/bin'` - JAVA_HOME="$javaHome" - export JAVA_HOME - fi - fi -fi +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac -if [ -z "$JAVACMD" ] ; then - if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - else - JAVACMD="`which java`" - fi +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT +else + die "cannot create temp dir" fi -if [ ! -x "$JAVACMD" ] ; then - echo "Error: JAVA_HOME is not defined correctly." >&2 - echo " We cannot execute $JAVACMD" >&2 - exit 1 -fi +mkdir -p -- "${MAVEN_HOME%/*}" -if [ -z "$JAVA_HOME" ] ; then - echo "Warning: JAVA_HOME environment variable is not set." +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" fi -CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v -# traverses directory structure from process work directory to filesystem root -# first directory with .mvn subdirectory is considered project base directory -find_maven_basedir() { +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac - if [ -z "$1" ] - then - echo "Path not specified to find_maven_basedir" - return 1 - fi +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" +fi - basedir="$1" - wdir="$1" - while [ "$wdir" != '/' ] ; do - if [ -d "$wdir"/.mvn ] ; then - basedir=$wdir - break +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then + distributionSha256Result=true fi - # workaround for JBEAP-8937 (on Solaris 10/Sparc) - if [ -d "${wdir}" ]; then - wdir=`cd "$wdir/.."; pwd` + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true fi - # end of workaround - done - echo "${basedir}" -} - -# concatenates all lines of a file -concat_lines() { - if [ -f "$1" ]; then - echo "$(tr -s '\n' ' ' < "$1")" + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 + exit 1 fi -} - -BASE_DIR=`find_maven_basedir "$(pwd)"` -if [ -z "$BASE_DIR" ]; then - exit 1; fi -########################################################################################## -# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central -# This allows using the maven wrapper in projects that prohibit checking in binary data. -########################################################################################## -if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found .mvn/wrapper/maven-wrapper.jar" - fi +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" else - if [ "$MVNW_VERBOSE" = true ]; then - echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." - fi - if [ -n "$MVNW_REPOURL" ]; then - jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" - else - jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" - fi - while IFS="=" read key value; do - case "$key" in (wrapperUrl) jarUrl="$value"; break ;; - esac - done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" - if [ "$MVNW_VERBOSE" = true ]; then - echo "Downloading from: $jarUrl" - fi - wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" - if $cygwin; then - wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` - fi + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" +fi - if command -v wget > /dev/null; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found wget ... using wget" - fi - if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then - wget "$jarUrl" -O "$wrapperJarPath" - else - wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" - fi - elif command -v curl > /dev/null; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found curl ... using curl" - fi - if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then - curl -o "$wrapperJarPath" "$jarUrl" -f - else - curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f - fi +# Find the actual extracted directory name (handles snapshots where filename != directory name) +actualDistributionDir="" - else - if [ "$MVNW_VERBOSE" = true ]; then - echo "Falling back to using Java to download" - fi - javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" - # For Cygwin, switch paths to Windows format before running javac - if $cygwin; then - javaClass=`cygpath --path --windows "$javaClass"` - fi - if [ -e "$javaClass" ]; then - if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then - if [ "$MVNW_VERBOSE" = true ]; then - echo " - Compiling MavenWrapperDownloader.java ..." - fi - # Compiling the Java class - ("$JAVA_HOME/bin/javac" "$javaClass") - fi - if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then - # Running the downloader - if [ "$MVNW_VERBOSE" = true ]; then - echo " - Running MavenWrapperDownloader.java ..." - fi - ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") - fi - fi - fi +# First try the expected directory name (for regular distributions) +if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then + if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then + actualDistributionDir="$distributionUrlNameMain" + fi fi -########################################################################################## -# End of extension -########################################################################################## -export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} -if [ "$MVNW_VERBOSE" = true ]; then - echo $MAVEN_PROJECTBASEDIR -fi -MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" - -# For Cygwin, switch paths to Windows format before running java -if $cygwin; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --path --windows "$M2_HOME"` - [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` - [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --windows "$CLASSPATH"` - [ -n "$MAVEN_PROJECTBASEDIR" ] && - MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +# If not found, search for any directory with the Maven executable (for snapshots) +if [ -z "$actualDistributionDir" ]; then + # enable globbing to iterate over items + set +f + for dir in "$TMP_DOWNLOAD_DIR"/*; do + if [ -d "$dir" ]; then + if [ -f "$dir/bin/$MVN_CMD" ]; then + actualDistributionDir="$(basename "$dir")" + break + fi + fi + done + set -f fi -# Provide a "standardized" way to retrieve the CLI args that will -# work with both Windows and non-Windows executions. -MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" -export MAVEN_CMD_LINE_ARGS +if [ -z "$actualDistributionDir" ]; then + verbose "Contents of $TMP_DOWNLOAD_DIR:" + verbose "$(ls -la "$TMP_DOWNLOAD_DIR")" + die "Could not find Maven distribution directory in extracted archive" +fi -WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain +verbose "Found extracted Maven distribution directory: $actualDistributionDir" +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" -exec "$JAVACMD" \ - $MAVEN_OPTS \ - -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ - ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" +clean || : +exec_maven "$@" diff --git a/projects-parent/archetypes-parent/student-archetype/src/main/resources/archetype-resources/mvnw.cmd b/projects-parent/archetypes-parent/student-archetype/src/main/resources/archetype-resources/mvnw.cmd index 249bdf382..92450f932 100644 --- a/projects-parent/archetypes-parent/student-archetype/src/main/resources/archetype-resources/mvnw.cmd +++ b/projects-parent/archetypes-parent/student-archetype/src/main/resources/archetype-resources/mvnw.cmd @@ -19,7 +19,7 @@ @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- -@REM Apache Maven Wrapper startup batch script, version 3.3.2 +@REM Apache Maven Wrapper startup batch script, version 3.3.4 @REM @REM Optional ENV vars @REM MVNW_REPOURL - repo url base for downloading maven distribution @@ -40,7 +40,7 @@ @SET __MVNW_ARG0_NAME__= @SET MVNW_USERNAME= @SET MVNW_PASSWORD= -@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) +@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*) @echo Cannot start maven from wrapper >&2 && exit /b 1 @GOTO :EOF : end batch / begin powershell #> @@ -73,16 +73,30 @@ switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { # apply MVNW_REPOURL and calculate MAVEN_HOME # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ if ($env:MVNW_REPOURL) { - $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } - $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" + $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')" } $distributionUrlName = $distributionUrl -replace '^.*/','' $distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' -$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" + +$MAVEN_M2_PATH = "$HOME/.m2" if ($env:MAVEN_USER_HOME) { - $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" + $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME" +} + +if (-not (Test-Path -Path $MAVEN_M2_PATH)) { + New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null +} + +$MAVEN_WRAPPER_DISTS = $null +if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) { + $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists" +} else { + $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists" } -$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' + +$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain" +$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' $MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { @@ -134,7 +148,33 @@ if ($distributionSha256Sum) { # unzip and move Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null -Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +$actualDistributionDir = "" + +# First try the expected directory name (for regular distributions) +$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain" +$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD" +if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) { + $actualDistributionDir = $distributionUrlNameMain +} + +# If not found, search for any directory with the Maven executable (for snapshots) +if (!$actualDistributionDir) { + Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object { + $testPath = Join-Path $_.FullName "bin/$MVN_CMD" + if (Test-Path -Path $testPath -PathType Leaf) { + $actualDistributionDir = $_.Name + } + } +} + +if (!$actualDistributionDir) { + Write-Error "Could not find Maven distribution directory in extracted archive" +} + +Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir" +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null try { Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null } catch { diff --git a/projects-parent/originals-parent/airline-web/.mvn/wrapper/maven-wrapper.properties b/projects-parent/originals-parent/airline-web/.mvn/wrapper/maven-wrapper.properties index d58dfb70b..c0bcafe98 100644 --- a/projects-parent/originals-parent/airline-web/.mvn/wrapper/maven-wrapper.properties +++ b/projects-parent/originals-parent/airline-web/.mvn/wrapper/maven-wrapper.properties @@ -1,19 +1,3 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. -wrapperVersion=3.3.2 +wrapperVersion=3.3.4 distributionType=only-script -distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.11/apache-maven-3.9.11-bin.zip diff --git a/projects-parent/originals-parent/airline-web/mvnw b/projects-parent/originals-parent/airline-web/mvnw index 19529ddf8..bd8896bf2 100755 --- a/projects-parent/originals-parent/airline-web/mvnw +++ b/projects-parent/originals-parent/airline-web/mvnw @@ -19,7 +19,7 @@ # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- -# Apache Maven Wrapper startup batch script, version 3.3.2 +# Apache Maven Wrapper startup batch script, version 3.3.4 # # Optional ENV vars # ----------------- @@ -105,14 +105,17 @@ trim() { printf "%s" "${1}" | tr -d '[:space:]' } +scriptDir="$(dirname "$0")" +scriptName="$(basename "$0")" + # parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties while IFS="=" read -r key value; do case "${key-}" in distributionUrl) distributionUrl=$(trim "${value-}") ;; distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; esac -done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" -[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties" +done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" case "${distributionUrl##*/}" in maven-mvnd-*bin.*) @@ -130,7 +133,7 @@ maven-mvnd-*bin.*) distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" ;; maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; -*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; esac # apply MVNW_REPOURL and calculate MAVEN_HOME @@ -227,7 +230,7 @@ if [ -n "${distributionSha256Sum-}" ]; then echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 exit 1 elif command -v sha256sum >/dev/null; then - if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then distributionSha256Result=true fi elif command -v shasum >/dev/null; then @@ -252,8 +255,41 @@ if command -v unzip >/dev/null; then else tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" fi -printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" -mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +actualDistributionDir="" + +# First try the expected directory name (for regular distributions) +if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then + if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then + actualDistributionDir="$distributionUrlNameMain" + fi +fi + +# If not found, search for any directory with the Maven executable (for snapshots) +if [ -z "$actualDistributionDir" ]; then + # enable globbing to iterate over items + set +f + for dir in "$TMP_DOWNLOAD_DIR"/*; do + if [ -d "$dir" ]; then + if [ -f "$dir/bin/$MVN_CMD" ]; then + actualDistributionDir="$(basename "$dir")" + break + fi + fi + done + set -f +fi + +if [ -z "$actualDistributionDir" ]; then + verbose "Contents of $TMP_DOWNLOAD_DIR:" + verbose "$(ls -la "$TMP_DOWNLOAD_DIR")" + die "Could not find Maven distribution directory in extracted archive" +fi + +verbose "Found extracted Maven distribution directory: $actualDistributionDir" +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" clean || : exec_maven "$@" diff --git a/projects-parent/originals-parent/airline-web/mvnw.cmd b/projects-parent/originals-parent/airline-web/mvnw.cmd index 249bdf382..92450f932 100644 --- a/projects-parent/originals-parent/airline-web/mvnw.cmd +++ b/projects-parent/originals-parent/airline-web/mvnw.cmd @@ -19,7 +19,7 @@ @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- -@REM Apache Maven Wrapper startup batch script, version 3.3.2 +@REM Apache Maven Wrapper startup batch script, version 3.3.4 @REM @REM Optional ENV vars @REM MVNW_REPOURL - repo url base for downloading maven distribution @@ -40,7 +40,7 @@ @SET __MVNW_ARG0_NAME__= @SET MVNW_USERNAME= @SET MVNW_PASSWORD= -@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) +@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*) @echo Cannot start maven from wrapper >&2 && exit /b 1 @GOTO :EOF : end batch / begin powershell #> @@ -73,16 +73,30 @@ switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { # apply MVNW_REPOURL and calculate MAVEN_HOME # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ if ($env:MVNW_REPOURL) { - $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } - $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" + $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')" } $distributionUrlName = $distributionUrl -replace '^.*/','' $distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' -$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" + +$MAVEN_M2_PATH = "$HOME/.m2" if ($env:MAVEN_USER_HOME) { - $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" + $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME" +} + +if (-not (Test-Path -Path $MAVEN_M2_PATH)) { + New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null +} + +$MAVEN_WRAPPER_DISTS = $null +if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) { + $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists" +} else { + $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists" } -$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' + +$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain" +$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' $MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { @@ -134,7 +148,33 @@ if ($distributionSha256Sum) { # unzip and move Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null -Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +$actualDistributionDir = "" + +# First try the expected directory name (for regular distributions) +$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain" +$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD" +if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) { + $actualDistributionDir = $distributionUrlNameMain +} + +# If not found, search for any directory with the Maven executable (for snapshots) +if (!$actualDistributionDir) { + Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object { + $testPath = Join-Path $_.FullName "bin/$MVN_CMD" + if (Test-Path -Path $testPath -PathType Leaf) { + $actualDistributionDir = $_.Name + } + } +} + +if (!$actualDistributionDir) { + Write-Error "Could not find Maven distribution directory in extracted archive" +} + +Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir" +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null try { Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null } catch { diff --git a/projects-parent/originals-parent/airline/.mvn/wrapper/maven-wrapper.properties b/projects-parent/originals-parent/airline/.mvn/wrapper/maven-wrapper.properties index d58dfb70b..c0bcafe98 100644 --- a/projects-parent/originals-parent/airline/.mvn/wrapper/maven-wrapper.properties +++ b/projects-parent/originals-parent/airline/.mvn/wrapper/maven-wrapper.properties @@ -1,19 +1,3 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. -wrapperVersion=3.3.2 +wrapperVersion=3.3.4 distributionType=only-script -distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.11/apache-maven-3.9.11-bin.zip diff --git a/projects-parent/originals-parent/airline/mvnw b/projects-parent/originals-parent/airline/mvnw index 19529ddf8..bd8896bf2 100755 --- a/projects-parent/originals-parent/airline/mvnw +++ b/projects-parent/originals-parent/airline/mvnw @@ -19,7 +19,7 @@ # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- -# Apache Maven Wrapper startup batch script, version 3.3.2 +# Apache Maven Wrapper startup batch script, version 3.3.4 # # Optional ENV vars # ----------------- @@ -105,14 +105,17 @@ trim() { printf "%s" "${1}" | tr -d '[:space:]' } +scriptDir="$(dirname "$0")" +scriptName="$(basename "$0")" + # parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties while IFS="=" read -r key value; do case "${key-}" in distributionUrl) distributionUrl=$(trim "${value-}") ;; distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; esac -done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" -[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties" +done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" case "${distributionUrl##*/}" in maven-mvnd-*bin.*) @@ -130,7 +133,7 @@ maven-mvnd-*bin.*) distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" ;; maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; -*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; esac # apply MVNW_REPOURL and calculate MAVEN_HOME @@ -227,7 +230,7 @@ if [ -n "${distributionSha256Sum-}" ]; then echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 exit 1 elif command -v sha256sum >/dev/null; then - if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then distributionSha256Result=true fi elif command -v shasum >/dev/null; then @@ -252,8 +255,41 @@ if command -v unzip >/dev/null; then else tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" fi -printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" -mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +actualDistributionDir="" + +# First try the expected directory name (for regular distributions) +if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then + if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then + actualDistributionDir="$distributionUrlNameMain" + fi +fi + +# If not found, search for any directory with the Maven executable (for snapshots) +if [ -z "$actualDistributionDir" ]; then + # enable globbing to iterate over items + set +f + for dir in "$TMP_DOWNLOAD_DIR"/*; do + if [ -d "$dir" ]; then + if [ -f "$dir/bin/$MVN_CMD" ]; then + actualDistributionDir="$(basename "$dir")" + break + fi + fi + done + set -f +fi + +if [ -z "$actualDistributionDir" ]; then + verbose "Contents of $TMP_DOWNLOAD_DIR:" + verbose "$(ls -la "$TMP_DOWNLOAD_DIR")" + die "Could not find Maven distribution directory in extracted archive" +fi + +verbose "Found extracted Maven distribution directory: $actualDistributionDir" +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" clean || : exec_maven "$@" diff --git a/projects-parent/originals-parent/airline/mvnw.cmd b/projects-parent/originals-parent/airline/mvnw.cmd index 249bdf382..92450f932 100644 --- a/projects-parent/originals-parent/airline/mvnw.cmd +++ b/projects-parent/originals-parent/airline/mvnw.cmd @@ -19,7 +19,7 @@ @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- -@REM Apache Maven Wrapper startup batch script, version 3.3.2 +@REM Apache Maven Wrapper startup batch script, version 3.3.4 @REM @REM Optional ENV vars @REM MVNW_REPOURL - repo url base for downloading maven distribution @@ -40,7 +40,7 @@ @SET __MVNW_ARG0_NAME__= @SET MVNW_USERNAME= @SET MVNW_PASSWORD= -@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) +@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*) @echo Cannot start maven from wrapper >&2 && exit /b 1 @GOTO :EOF : end batch / begin powershell #> @@ -73,16 +73,30 @@ switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { # apply MVNW_REPOURL and calculate MAVEN_HOME # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ if ($env:MVNW_REPOURL) { - $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } - $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" + $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')" } $distributionUrlName = $distributionUrl -replace '^.*/','' $distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' -$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" + +$MAVEN_M2_PATH = "$HOME/.m2" if ($env:MAVEN_USER_HOME) { - $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" + $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME" +} + +if (-not (Test-Path -Path $MAVEN_M2_PATH)) { + New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null +} + +$MAVEN_WRAPPER_DISTS = $null +if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) { + $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists" +} else { + $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists" } -$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' + +$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain" +$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' $MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { @@ -134,7 +148,33 @@ if ($distributionSha256Sum) { # unzip and move Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null -Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +$actualDistributionDir = "" + +# First try the expected directory name (for regular distributions) +$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain" +$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD" +if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) { + $actualDistributionDir = $distributionUrlNameMain +} + +# If not found, search for any directory with the Maven executable (for snapshots) +if (!$actualDistributionDir) { + Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object { + $testPath = Join-Path $_.FullName "bin/$MVN_CMD" + if (Test-Path -Path $testPath -PathType Leaf) { + $actualDistributionDir = $_.Name + } + } +} + +if (!$actualDistributionDir) { + Write-Error "Could not find Maven distribution directory in extracted archive" +} + +Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir" +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null try { Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null } catch { diff --git a/projects-parent/originals-parent/apptbook-web/.mvn/wrapper/maven-wrapper.properties b/projects-parent/originals-parent/apptbook-web/.mvn/wrapper/maven-wrapper.properties index d58dfb70b..6b04698d3 100644 --- a/projects-parent/originals-parent/apptbook-web/.mvn/wrapper/maven-wrapper.properties +++ b/projects-parent/originals-parent/apptbook-web/.mvn/wrapper/maven-wrapper.properties @@ -14,6 +14,6 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. -wrapperVersion=3.3.2 +wrapperVersion=3.3.4 distributionType=only-script distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip diff --git a/projects-parent/originals-parent/apptbook-web/mvnw b/projects-parent/originals-parent/apptbook-web/mvnw index 19529ddf8..bd8896bf2 100755 --- a/projects-parent/originals-parent/apptbook-web/mvnw +++ b/projects-parent/originals-parent/apptbook-web/mvnw @@ -19,7 +19,7 @@ # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- -# Apache Maven Wrapper startup batch script, version 3.3.2 +# Apache Maven Wrapper startup batch script, version 3.3.4 # # Optional ENV vars # ----------------- @@ -105,14 +105,17 @@ trim() { printf "%s" "${1}" | tr -d '[:space:]' } +scriptDir="$(dirname "$0")" +scriptName="$(basename "$0")" + # parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties while IFS="=" read -r key value; do case "${key-}" in distributionUrl) distributionUrl=$(trim "${value-}") ;; distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; esac -done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" -[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties" +done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" case "${distributionUrl##*/}" in maven-mvnd-*bin.*) @@ -130,7 +133,7 @@ maven-mvnd-*bin.*) distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" ;; maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; -*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; esac # apply MVNW_REPOURL and calculate MAVEN_HOME @@ -227,7 +230,7 @@ if [ -n "${distributionSha256Sum-}" ]; then echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 exit 1 elif command -v sha256sum >/dev/null; then - if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then distributionSha256Result=true fi elif command -v shasum >/dev/null; then @@ -252,8 +255,41 @@ if command -v unzip >/dev/null; then else tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" fi -printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" -mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +actualDistributionDir="" + +# First try the expected directory name (for regular distributions) +if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then + if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then + actualDistributionDir="$distributionUrlNameMain" + fi +fi + +# If not found, search for any directory with the Maven executable (for snapshots) +if [ -z "$actualDistributionDir" ]; then + # enable globbing to iterate over items + set +f + for dir in "$TMP_DOWNLOAD_DIR"/*; do + if [ -d "$dir" ]; then + if [ -f "$dir/bin/$MVN_CMD" ]; then + actualDistributionDir="$(basename "$dir")" + break + fi + fi + done + set -f +fi + +if [ -z "$actualDistributionDir" ]; then + verbose "Contents of $TMP_DOWNLOAD_DIR:" + verbose "$(ls -la "$TMP_DOWNLOAD_DIR")" + die "Could not find Maven distribution directory in extracted archive" +fi + +verbose "Found extracted Maven distribution directory: $actualDistributionDir" +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" clean || : exec_maven "$@" diff --git a/projects-parent/originals-parent/apptbook-web/mvnw.cmd b/projects-parent/originals-parent/apptbook-web/mvnw.cmd index 249bdf382..92450f932 100644 --- a/projects-parent/originals-parent/apptbook-web/mvnw.cmd +++ b/projects-parent/originals-parent/apptbook-web/mvnw.cmd @@ -19,7 +19,7 @@ @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- -@REM Apache Maven Wrapper startup batch script, version 3.3.2 +@REM Apache Maven Wrapper startup batch script, version 3.3.4 @REM @REM Optional ENV vars @REM MVNW_REPOURL - repo url base for downloading maven distribution @@ -40,7 +40,7 @@ @SET __MVNW_ARG0_NAME__= @SET MVNW_USERNAME= @SET MVNW_PASSWORD= -@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) +@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*) @echo Cannot start maven from wrapper >&2 && exit /b 1 @GOTO :EOF : end batch / begin powershell #> @@ -73,16 +73,30 @@ switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { # apply MVNW_REPOURL and calculate MAVEN_HOME # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ if ($env:MVNW_REPOURL) { - $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } - $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" + $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')" } $distributionUrlName = $distributionUrl -replace '^.*/','' $distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' -$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" + +$MAVEN_M2_PATH = "$HOME/.m2" if ($env:MAVEN_USER_HOME) { - $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" + $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME" +} + +if (-not (Test-Path -Path $MAVEN_M2_PATH)) { + New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null +} + +$MAVEN_WRAPPER_DISTS = $null +if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) { + $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists" +} else { + $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists" } -$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' + +$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain" +$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' $MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { @@ -134,7 +148,33 @@ if ($distributionSha256Sum) { # unzip and move Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null -Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +$actualDistributionDir = "" + +# First try the expected directory name (for regular distributions) +$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain" +$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD" +if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) { + $actualDistributionDir = $distributionUrlNameMain +} + +# If not found, search for any directory with the Maven executable (for snapshots) +if (!$actualDistributionDir) { + Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object { + $testPath = Join-Path $_.FullName "bin/$MVN_CMD" + if (Test-Path -Path $testPath -PathType Leaf) { + $actualDistributionDir = $_.Name + } + } +} + +if (!$actualDistributionDir) { + Write-Error "Could not find Maven distribution directory in extracted archive" +} + +Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir" +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null try { Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null } catch { diff --git a/projects-parent/originals-parent/apptbook/.mvn/wrapper/maven-wrapper.properties b/projects-parent/originals-parent/apptbook/.mvn/wrapper/maven-wrapper.properties index d58dfb70b..6b04698d3 100644 --- a/projects-parent/originals-parent/apptbook/.mvn/wrapper/maven-wrapper.properties +++ b/projects-parent/originals-parent/apptbook/.mvn/wrapper/maven-wrapper.properties @@ -14,6 +14,6 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. -wrapperVersion=3.3.2 +wrapperVersion=3.3.4 distributionType=only-script distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip diff --git a/projects-parent/originals-parent/apptbook/mvnw b/projects-parent/originals-parent/apptbook/mvnw index 19529ddf8..bd8896bf2 100755 --- a/projects-parent/originals-parent/apptbook/mvnw +++ b/projects-parent/originals-parent/apptbook/mvnw @@ -19,7 +19,7 @@ # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- -# Apache Maven Wrapper startup batch script, version 3.3.2 +# Apache Maven Wrapper startup batch script, version 3.3.4 # # Optional ENV vars # ----------------- @@ -105,14 +105,17 @@ trim() { printf "%s" "${1}" | tr -d '[:space:]' } +scriptDir="$(dirname "$0")" +scriptName="$(basename "$0")" + # parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties while IFS="=" read -r key value; do case "${key-}" in distributionUrl) distributionUrl=$(trim "${value-}") ;; distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; esac -done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" -[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties" +done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" case "${distributionUrl##*/}" in maven-mvnd-*bin.*) @@ -130,7 +133,7 @@ maven-mvnd-*bin.*) distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" ;; maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; -*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; esac # apply MVNW_REPOURL and calculate MAVEN_HOME @@ -227,7 +230,7 @@ if [ -n "${distributionSha256Sum-}" ]; then echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 exit 1 elif command -v sha256sum >/dev/null; then - if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then distributionSha256Result=true fi elif command -v shasum >/dev/null; then @@ -252,8 +255,41 @@ if command -v unzip >/dev/null; then else tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" fi -printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" -mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +actualDistributionDir="" + +# First try the expected directory name (for regular distributions) +if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then + if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then + actualDistributionDir="$distributionUrlNameMain" + fi +fi + +# If not found, search for any directory with the Maven executable (for snapshots) +if [ -z "$actualDistributionDir" ]; then + # enable globbing to iterate over items + set +f + for dir in "$TMP_DOWNLOAD_DIR"/*; do + if [ -d "$dir" ]; then + if [ -f "$dir/bin/$MVN_CMD" ]; then + actualDistributionDir="$(basename "$dir")" + break + fi + fi + done + set -f +fi + +if [ -z "$actualDistributionDir" ]; then + verbose "Contents of $TMP_DOWNLOAD_DIR:" + verbose "$(ls -la "$TMP_DOWNLOAD_DIR")" + die "Could not find Maven distribution directory in extracted archive" +fi + +verbose "Found extracted Maven distribution directory: $actualDistributionDir" +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" clean || : exec_maven "$@" diff --git a/projects-parent/originals-parent/apptbook/mvnw.cmd b/projects-parent/originals-parent/apptbook/mvnw.cmd index 249bdf382..92450f932 100644 --- a/projects-parent/originals-parent/apptbook/mvnw.cmd +++ b/projects-parent/originals-parent/apptbook/mvnw.cmd @@ -19,7 +19,7 @@ @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- -@REM Apache Maven Wrapper startup batch script, version 3.3.2 +@REM Apache Maven Wrapper startup batch script, version 3.3.4 @REM @REM Optional ENV vars @REM MVNW_REPOURL - repo url base for downloading maven distribution @@ -40,7 +40,7 @@ @SET __MVNW_ARG0_NAME__= @SET MVNW_USERNAME= @SET MVNW_PASSWORD= -@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) +@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*) @echo Cannot start maven from wrapper >&2 && exit /b 1 @GOTO :EOF : end batch / begin powershell #> @@ -73,16 +73,30 @@ switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { # apply MVNW_REPOURL and calculate MAVEN_HOME # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ if ($env:MVNW_REPOURL) { - $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } - $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" + $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')" } $distributionUrlName = $distributionUrl -replace '^.*/','' $distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' -$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" + +$MAVEN_M2_PATH = "$HOME/.m2" if ($env:MAVEN_USER_HOME) { - $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" + $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME" +} + +if (-not (Test-Path -Path $MAVEN_M2_PATH)) { + New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null +} + +$MAVEN_WRAPPER_DISTS = $null +if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) { + $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists" +} else { + $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists" } -$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' + +$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain" +$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' $MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { @@ -134,7 +148,33 @@ if ($distributionSha256Sum) { # unzip and move Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null -Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +$actualDistributionDir = "" + +# First try the expected directory name (for regular distributions) +$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain" +$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD" +if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) { + $actualDistributionDir = $distributionUrlNameMain +} + +# If not found, search for any directory with the Maven executable (for snapshots) +if (!$actualDistributionDir) { + Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object { + $testPath = Join-Path $_.FullName "bin/$MVN_CMD" + if (Test-Path -Path $testPath -PathType Leaf) { + $actualDistributionDir = $_.Name + } + } +} + +if (!$actualDistributionDir) { + Write-Error "Could not find Maven distribution directory in extracted archive" +} + +Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir" +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null try { Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null } catch { diff --git a/projects-parent/originals-parent/kata/.mvn/wrapper/maven-wrapper.properties b/projects-parent/originals-parent/kata/.mvn/wrapper/maven-wrapper.properties index d58dfb70b..6b04698d3 100644 --- a/projects-parent/originals-parent/kata/.mvn/wrapper/maven-wrapper.properties +++ b/projects-parent/originals-parent/kata/.mvn/wrapper/maven-wrapper.properties @@ -14,6 +14,6 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. -wrapperVersion=3.3.2 +wrapperVersion=3.3.4 distributionType=only-script distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip diff --git a/projects-parent/originals-parent/kata/mvnw b/projects-parent/originals-parent/kata/mvnw index 19529ddf8..bd8896bf2 100755 --- a/projects-parent/originals-parent/kata/mvnw +++ b/projects-parent/originals-parent/kata/mvnw @@ -19,7 +19,7 @@ # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- -# Apache Maven Wrapper startup batch script, version 3.3.2 +# Apache Maven Wrapper startup batch script, version 3.3.4 # # Optional ENV vars # ----------------- @@ -105,14 +105,17 @@ trim() { printf "%s" "${1}" | tr -d '[:space:]' } +scriptDir="$(dirname "$0")" +scriptName="$(basename "$0")" + # parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties while IFS="=" read -r key value; do case "${key-}" in distributionUrl) distributionUrl=$(trim "${value-}") ;; distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; esac -done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" -[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties" +done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" case "${distributionUrl##*/}" in maven-mvnd-*bin.*) @@ -130,7 +133,7 @@ maven-mvnd-*bin.*) distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" ;; maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; -*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; esac # apply MVNW_REPOURL and calculate MAVEN_HOME @@ -227,7 +230,7 @@ if [ -n "${distributionSha256Sum-}" ]; then echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 exit 1 elif command -v sha256sum >/dev/null; then - if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then distributionSha256Result=true fi elif command -v shasum >/dev/null; then @@ -252,8 +255,41 @@ if command -v unzip >/dev/null; then else tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" fi -printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" -mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +actualDistributionDir="" + +# First try the expected directory name (for regular distributions) +if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then + if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then + actualDistributionDir="$distributionUrlNameMain" + fi +fi + +# If not found, search for any directory with the Maven executable (for snapshots) +if [ -z "$actualDistributionDir" ]; then + # enable globbing to iterate over items + set +f + for dir in "$TMP_DOWNLOAD_DIR"/*; do + if [ -d "$dir" ]; then + if [ -f "$dir/bin/$MVN_CMD" ]; then + actualDistributionDir="$(basename "$dir")" + break + fi + fi + done + set -f +fi + +if [ -z "$actualDistributionDir" ]; then + verbose "Contents of $TMP_DOWNLOAD_DIR:" + verbose "$(ls -la "$TMP_DOWNLOAD_DIR")" + die "Could not find Maven distribution directory in extracted archive" +fi + +verbose "Found extracted Maven distribution directory: $actualDistributionDir" +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" clean || : exec_maven "$@" diff --git a/projects-parent/originals-parent/kata/mvnw.cmd b/projects-parent/originals-parent/kata/mvnw.cmd index 249bdf382..92450f932 100644 --- a/projects-parent/originals-parent/kata/mvnw.cmd +++ b/projects-parent/originals-parent/kata/mvnw.cmd @@ -19,7 +19,7 @@ @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- -@REM Apache Maven Wrapper startup batch script, version 3.3.2 +@REM Apache Maven Wrapper startup batch script, version 3.3.4 @REM @REM Optional ENV vars @REM MVNW_REPOURL - repo url base for downloading maven distribution @@ -40,7 +40,7 @@ @SET __MVNW_ARG0_NAME__= @SET MVNW_USERNAME= @SET MVNW_PASSWORD= -@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) +@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*) @echo Cannot start maven from wrapper >&2 && exit /b 1 @GOTO :EOF : end batch / begin powershell #> @@ -73,16 +73,30 @@ switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { # apply MVNW_REPOURL and calculate MAVEN_HOME # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ if ($env:MVNW_REPOURL) { - $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } - $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" + $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')" } $distributionUrlName = $distributionUrl -replace '^.*/','' $distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' -$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" + +$MAVEN_M2_PATH = "$HOME/.m2" if ($env:MAVEN_USER_HOME) { - $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" + $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME" +} + +if (-not (Test-Path -Path $MAVEN_M2_PATH)) { + New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null +} + +$MAVEN_WRAPPER_DISTS = $null +if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) { + $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists" +} else { + $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists" } -$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' + +$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain" +$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' $MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { @@ -134,7 +148,33 @@ if ($distributionSha256Sum) { # unzip and move Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null -Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +$actualDistributionDir = "" + +# First try the expected directory name (for regular distributions) +$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain" +$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD" +if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) { + $actualDistributionDir = $distributionUrlNameMain +} + +# If not found, search for any directory with the Maven executable (for snapshots) +if (!$actualDistributionDir) { + Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object { + $testPath = Join-Path $_.FullName "bin/$MVN_CMD" + if (Test-Path -Path $testPath -PathType Leaf) { + $actualDistributionDir = $_.Name + } + } +} + +if (!$actualDistributionDir) { + Write-Error "Could not find Maven distribution directory in extracted archive" +} + +Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir" +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null try { Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null } catch { diff --git a/projects-parent/originals-parent/phonebill-web/.mvn/wrapper/maven-wrapper.properties b/projects-parent/originals-parent/phonebill-web/.mvn/wrapper/maven-wrapper.properties index d58dfb70b..6b04698d3 100644 --- a/projects-parent/originals-parent/phonebill-web/.mvn/wrapper/maven-wrapper.properties +++ b/projects-parent/originals-parent/phonebill-web/.mvn/wrapper/maven-wrapper.properties @@ -14,6 +14,6 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. -wrapperVersion=3.3.2 +wrapperVersion=3.3.4 distributionType=only-script distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip diff --git a/projects-parent/originals-parent/phonebill-web/mvnw b/projects-parent/originals-parent/phonebill-web/mvnw index 19529ddf8..bd8896bf2 100755 --- a/projects-parent/originals-parent/phonebill-web/mvnw +++ b/projects-parent/originals-parent/phonebill-web/mvnw @@ -19,7 +19,7 @@ # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- -# Apache Maven Wrapper startup batch script, version 3.3.2 +# Apache Maven Wrapper startup batch script, version 3.3.4 # # Optional ENV vars # ----------------- @@ -105,14 +105,17 @@ trim() { printf "%s" "${1}" | tr -d '[:space:]' } +scriptDir="$(dirname "$0")" +scriptName="$(basename "$0")" + # parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties while IFS="=" read -r key value; do case "${key-}" in distributionUrl) distributionUrl=$(trim "${value-}") ;; distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; esac -done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" -[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties" +done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" case "${distributionUrl##*/}" in maven-mvnd-*bin.*) @@ -130,7 +133,7 @@ maven-mvnd-*bin.*) distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" ;; maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; -*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; esac # apply MVNW_REPOURL and calculate MAVEN_HOME @@ -227,7 +230,7 @@ if [ -n "${distributionSha256Sum-}" ]; then echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 exit 1 elif command -v sha256sum >/dev/null; then - if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then distributionSha256Result=true fi elif command -v shasum >/dev/null; then @@ -252,8 +255,41 @@ if command -v unzip >/dev/null; then else tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" fi -printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" -mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +actualDistributionDir="" + +# First try the expected directory name (for regular distributions) +if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then + if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then + actualDistributionDir="$distributionUrlNameMain" + fi +fi + +# If not found, search for any directory with the Maven executable (for snapshots) +if [ -z "$actualDistributionDir" ]; then + # enable globbing to iterate over items + set +f + for dir in "$TMP_DOWNLOAD_DIR"/*; do + if [ -d "$dir" ]; then + if [ -f "$dir/bin/$MVN_CMD" ]; then + actualDistributionDir="$(basename "$dir")" + break + fi + fi + done + set -f +fi + +if [ -z "$actualDistributionDir" ]; then + verbose "Contents of $TMP_DOWNLOAD_DIR:" + verbose "$(ls -la "$TMP_DOWNLOAD_DIR")" + die "Could not find Maven distribution directory in extracted archive" +fi + +verbose "Found extracted Maven distribution directory: $actualDistributionDir" +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" clean || : exec_maven "$@" diff --git a/projects-parent/originals-parent/phonebill-web/mvnw.cmd b/projects-parent/originals-parent/phonebill-web/mvnw.cmd index 249bdf382..92450f932 100644 --- a/projects-parent/originals-parent/phonebill-web/mvnw.cmd +++ b/projects-parent/originals-parent/phonebill-web/mvnw.cmd @@ -19,7 +19,7 @@ @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- -@REM Apache Maven Wrapper startup batch script, version 3.3.2 +@REM Apache Maven Wrapper startup batch script, version 3.3.4 @REM @REM Optional ENV vars @REM MVNW_REPOURL - repo url base for downloading maven distribution @@ -40,7 +40,7 @@ @SET __MVNW_ARG0_NAME__= @SET MVNW_USERNAME= @SET MVNW_PASSWORD= -@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) +@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*) @echo Cannot start maven from wrapper >&2 && exit /b 1 @GOTO :EOF : end batch / begin powershell #> @@ -73,16 +73,30 @@ switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { # apply MVNW_REPOURL and calculate MAVEN_HOME # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ if ($env:MVNW_REPOURL) { - $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } - $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" + $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')" } $distributionUrlName = $distributionUrl -replace '^.*/','' $distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' -$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" + +$MAVEN_M2_PATH = "$HOME/.m2" if ($env:MAVEN_USER_HOME) { - $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" + $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME" +} + +if (-not (Test-Path -Path $MAVEN_M2_PATH)) { + New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null +} + +$MAVEN_WRAPPER_DISTS = $null +if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) { + $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists" +} else { + $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists" } -$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' + +$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain" +$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' $MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { @@ -134,7 +148,33 @@ if ($distributionSha256Sum) { # unzip and move Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null -Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +$actualDistributionDir = "" + +# First try the expected directory name (for regular distributions) +$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain" +$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD" +if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) { + $actualDistributionDir = $distributionUrlNameMain +} + +# If not found, search for any directory with the Maven executable (for snapshots) +if (!$actualDistributionDir) { + Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object { + $testPath = Join-Path $_.FullName "bin/$MVN_CMD" + if (Test-Path -Path $testPath -PathType Leaf) { + $actualDistributionDir = $_.Name + } + } +} + +if (!$actualDistributionDir) { + Write-Error "Could not find Maven distribution directory in extracted archive" +} + +Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir" +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null try { Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null } catch { diff --git a/projects-parent/originals-parent/phonebill/.mvn/wrapper/maven-wrapper.properties b/projects-parent/originals-parent/phonebill/.mvn/wrapper/maven-wrapper.properties index d58dfb70b..6b04698d3 100644 --- a/projects-parent/originals-parent/phonebill/.mvn/wrapper/maven-wrapper.properties +++ b/projects-parent/originals-parent/phonebill/.mvn/wrapper/maven-wrapper.properties @@ -14,6 +14,6 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. -wrapperVersion=3.3.2 +wrapperVersion=3.3.4 distributionType=only-script distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip diff --git a/projects-parent/originals-parent/phonebill/mvnw b/projects-parent/originals-parent/phonebill/mvnw index 19529ddf8..bd8896bf2 100755 --- a/projects-parent/originals-parent/phonebill/mvnw +++ b/projects-parent/originals-parent/phonebill/mvnw @@ -19,7 +19,7 @@ # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- -# Apache Maven Wrapper startup batch script, version 3.3.2 +# Apache Maven Wrapper startup batch script, version 3.3.4 # # Optional ENV vars # ----------------- @@ -105,14 +105,17 @@ trim() { printf "%s" "${1}" | tr -d '[:space:]' } +scriptDir="$(dirname "$0")" +scriptName="$(basename "$0")" + # parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties while IFS="=" read -r key value; do case "${key-}" in distributionUrl) distributionUrl=$(trim "${value-}") ;; distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; esac -done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" -[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties" +done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" case "${distributionUrl##*/}" in maven-mvnd-*bin.*) @@ -130,7 +133,7 @@ maven-mvnd-*bin.*) distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" ;; maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; -*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; esac # apply MVNW_REPOURL and calculate MAVEN_HOME @@ -227,7 +230,7 @@ if [ -n "${distributionSha256Sum-}" ]; then echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 exit 1 elif command -v sha256sum >/dev/null; then - if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then distributionSha256Result=true fi elif command -v shasum >/dev/null; then @@ -252,8 +255,41 @@ if command -v unzip >/dev/null; then else tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" fi -printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" -mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +actualDistributionDir="" + +# First try the expected directory name (for regular distributions) +if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then + if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then + actualDistributionDir="$distributionUrlNameMain" + fi +fi + +# If not found, search for any directory with the Maven executable (for snapshots) +if [ -z "$actualDistributionDir" ]; then + # enable globbing to iterate over items + set +f + for dir in "$TMP_DOWNLOAD_DIR"/*; do + if [ -d "$dir" ]; then + if [ -f "$dir/bin/$MVN_CMD" ]; then + actualDistributionDir="$(basename "$dir")" + break + fi + fi + done + set -f +fi + +if [ -z "$actualDistributionDir" ]; then + verbose "Contents of $TMP_DOWNLOAD_DIR:" + verbose "$(ls -la "$TMP_DOWNLOAD_DIR")" + die "Could not find Maven distribution directory in extracted archive" +fi + +verbose "Found extracted Maven distribution directory: $actualDistributionDir" +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" clean || : exec_maven "$@" diff --git a/projects-parent/originals-parent/phonebill/mvnw.cmd b/projects-parent/originals-parent/phonebill/mvnw.cmd index 249bdf382..92450f932 100644 --- a/projects-parent/originals-parent/phonebill/mvnw.cmd +++ b/projects-parent/originals-parent/phonebill/mvnw.cmd @@ -19,7 +19,7 @@ @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- -@REM Apache Maven Wrapper startup batch script, version 3.3.2 +@REM Apache Maven Wrapper startup batch script, version 3.3.4 @REM @REM Optional ENV vars @REM MVNW_REPOURL - repo url base for downloading maven distribution @@ -40,7 +40,7 @@ @SET __MVNW_ARG0_NAME__= @SET MVNW_USERNAME= @SET MVNW_PASSWORD= -@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) +@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*) @echo Cannot start maven from wrapper >&2 && exit /b 1 @GOTO :EOF : end batch / begin powershell #> @@ -73,16 +73,30 @@ switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { # apply MVNW_REPOURL and calculate MAVEN_HOME # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ if ($env:MVNW_REPOURL) { - $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } - $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" + $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')" } $distributionUrlName = $distributionUrl -replace '^.*/','' $distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' -$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" + +$MAVEN_M2_PATH = "$HOME/.m2" if ($env:MAVEN_USER_HOME) { - $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" + $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME" +} + +if (-not (Test-Path -Path $MAVEN_M2_PATH)) { + New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null +} + +$MAVEN_WRAPPER_DISTS = $null +if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) { + $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists" +} else { + $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists" } -$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' + +$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain" +$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' $MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { @@ -134,7 +148,33 @@ if ($distributionSha256Sum) { # unzip and move Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null -Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +$actualDistributionDir = "" + +# First try the expected directory name (for regular distributions) +$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain" +$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD" +if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) { + $actualDistributionDir = $distributionUrlNameMain +} + +# If not found, search for any directory with the Maven executable (for snapshots) +if (!$actualDistributionDir) { + Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object { + $testPath = Join-Path $_.FullName "bin/$MVN_CMD" + if (Test-Path -Path $testPath -PathType Leaf) { + $actualDistributionDir = $_.Name + } + } +} + +if (!$actualDistributionDir) { + Write-Error "Could not find Maven distribution directory in extracted archive" +} + +Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir" +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null try { Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null } catch { diff --git a/projects-parent/originals-parent/student/.mvn/wrapper/maven-wrapper.properties b/projects-parent/originals-parent/student/.mvn/wrapper/maven-wrapper.properties index d58dfb70b..6b04698d3 100644 --- a/projects-parent/originals-parent/student/.mvn/wrapper/maven-wrapper.properties +++ b/projects-parent/originals-parent/student/.mvn/wrapper/maven-wrapper.properties @@ -14,6 +14,6 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. -wrapperVersion=3.3.2 +wrapperVersion=3.3.4 distributionType=only-script distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip diff --git a/projects-parent/originals-parent/student/mvnw b/projects-parent/originals-parent/student/mvnw index 19529ddf8..bd8896bf2 100755 --- a/projects-parent/originals-parent/student/mvnw +++ b/projects-parent/originals-parent/student/mvnw @@ -19,7 +19,7 @@ # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- -# Apache Maven Wrapper startup batch script, version 3.3.2 +# Apache Maven Wrapper startup batch script, version 3.3.4 # # Optional ENV vars # ----------------- @@ -105,14 +105,17 @@ trim() { printf "%s" "${1}" | tr -d '[:space:]' } +scriptDir="$(dirname "$0")" +scriptName="$(basename "$0")" + # parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties while IFS="=" read -r key value; do case "${key-}" in distributionUrl) distributionUrl=$(trim "${value-}") ;; distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; esac -done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" -[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties" +done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" case "${distributionUrl##*/}" in maven-mvnd-*bin.*) @@ -130,7 +133,7 @@ maven-mvnd-*bin.*) distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" ;; maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; -*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; esac # apply MVNW_REPOURL and calculate MAVEN_HOME @@ -227,7 +230,7 @@ if [ -n "${distributionSha256Sum-}" ]; then echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 exit 1 elif command -v sha256sum >/dev/null; then - if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then distributionSha256Result=true fi elif command -v shasum >/dev/null; then @@ -252,8 +255,41 @@ if command -v unzip >/dev/null; then else tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" fi -printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" -mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +actualDistributionDir="" + +# First try the expected directory name (for regular distributions) +if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then + if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then + actualDistributionDir="$distributionUrlNameMain" + fi +fi + +# If not found, search for any directory with the Maven executable (for snapshots) +if [ -z "$actualDistributionDir" ]; then + # enable globbing to iterate over items + set +f + for dir in "$TMP_DOWNLOAD_DIR"/*; do + if [ -d "$dir" ]; then + if [ -f "$dir/bin/$MVN_CMD" ]; then + actualDistributionDir="$(basename "$dir")" + break + fi + fi + done + set -f +fi + +if [ -z "$actualDistributionDir" ]; then + verbose "Contents of $TMP_DOWNLOAD_DIR:" + verbose "$(ls -la "$TMP_DOWNLOAD_DIR")" + die "Could not find Maven distribution directory in extracted archive" +fi + +verbose "Found extracted Maven distribution directory: $actualDistributionDir" +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" clean || : exec_maven "$@" diff --git a/projects-parent/originals-parent/student/mvnw.cmd b/projects-parent/originals-parent/student/mvnw.cmd index 249bdf382..92450f932 100644 --- a/projects-parent/originals-parent/student/mvnw.cmd +++ b/projects-parent/originals-parent/student/mvnw.cmd @@ -19,7 +19,7 @@ @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- -@REM Apache Maven Wrapper startup batch script, version 3.3.2 +@REM Apache Maven Wrapper startup batch script, version 3.3.4 @REM @REM Optional ENV vars @REM MVNW_REPOURL - repo url base for downloading maven distribution @@ -40,7 +40,7 @@ @SET __MVNW_ARG0_NAME__= @SET MVNW_USERNAME= @SET MVNW_PASSWORD= -@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) +@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*) @echo Cannot start maven from wrapper >&2 && exit /b 1 @GOTO :EOF : end batch / begin powershell #> @@ -73,16 +73,30 @@ switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { # apply MVNW_REPOURL and calculate MAVEN_HOME # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ if ($env:MVNW_REPOURL) { - $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } - $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" + $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')" } $distributionUrlName = $distributionUrl -replace '^.*/','' $distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' -$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" + +$MAVEN_M2_PATH = "$HOME/.m2" if ($env:MAVEN_USER_HOME) { - $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" + $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME" +} + +if (-not (Test-Path -Path $MAVEN_M2_PATH)) { + New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null +} + +$MAVEN_WRAPPER_DISTS = $null +if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) { + $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists" +} else { + $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists" } -$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' + +$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain" +$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' $MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { @@ -134,7 +148,33 @@ if ($distributionSha256Sum) { # unzip and move Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null -Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +$actualDistributionDir = "" + +# First try the expected directory name (for regular distributions) +$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain" +$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD" +if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) { + $actualDistributionDir = $distributionUrlNameMain +} + +# If not found, search for any directory with the Maven executable (for snapshots) +if (!$actualDistributionDir) { + Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object { + $testPath = Join-Path $_.FullName "bin/$MVN_CMD" + if (Test-Path -Path $testPath -PathType Leaf) { + $actualDistributionDir = $_.Name + } + } +} + +if (!$actualDistributionDir) { + Write-Error "Could not find Maven distribution directory in extracted archive" +} + +Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir" +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null try { Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null } catch { From 3ffd51904d869ae0542a6d188591c2952754372f Mon Sep 17 00:00:00 2001 From: David Whitlock Date: Thu, 27 Nov 2025 15:12:02 -0800 Subject: [PATCH 27/77] Add example XML parsing code for the Phone Bill project. --- .../cs/joy/phonebill/PhoneBillXmlHelper.java | 16 ++++++ .../joy/phonebill/PhoneBillXmlHelperTest.java | 52 +++++++++++++++++++ .../cs/joy/phonebill/invalid-phonebill.xml | 20 +++++++ .../pdx/cs/joy/phonebill/valid-phonebill.xml | 33 ++++++++++++ .../edu/pdx/cs/joy/phonebill.dtd | 0 5 files changed, 121 insertions(+) create mode 100644 projects-parent/originals-parent/phonebill/src/main/java/edu/pdx/cs/joy/phonebill/PhoneBillXmlHelper.java create mode 100644 projects-parent/originals-parent/phonebill/src/test/java/edu/pdx/cs/joy/phonebill/PhoneBillXmlHelperTest.java create mode 100644 projects-parent/originals-parent/phonebill/src/test/resources/edu/pdx/cs/joy/phonebill/invalid-phonebill.xml create mode 100644 projects-parent/originals-parent/phonebill/src/test/resources/edu/pdx/cs/joy/phonebill/valid-phonebill.xml rename projects-parent/projects/src/main/{java => resources}/edu/pdx/cs/joy/phonebill.dtd (100%) 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 new file mode 100644 index 000000000..9c1796ca1 --- /dev/null +++ b/projects-parent/originals-parent/phonebill/src/main/java/edu/pdx/cs/joy/phonebill/PhoneBillXmlHelper.java @@ -0,0 +1,16 @@ +package edu.pdx.cs.joy.phonebill; + +import edu.pdx.cs.joy.ProjectXmlHelper; + +public class PhoneBillXmlHelper extends ProjectXmlHelper { + + /** + * The Public ID for the Family Tree DTD + */ + protected static final String PUBLIC_ID = + "-//Joy of Coding at PSU//DTD Phone Bill//EN"; + + protected PhoneBillXmlHelper() { + super(PUBLIC_ID, "phonebill.dtd"); + } +} diff --git a/projects-parent/originals-parent/phonebill/src/test/java/edu/pdx/cs/joy/phonebill/PhoneBillXmlHelperTest.java b/projects-parent/originals-parent/phonebill/src/test/java/edu/pdx/cs/joy/phonebill/PhoneBillXmlHelperTest.java new file mode 100644 index 000000000..7a60fb69d --- /dev/null +++ b/projects-parent/originals-parent/phonebill/src/test/java/edu/pdx/cs/joy/phonebill/PhoneBillXmlHelperTest.java @@ -0,0 +1,52 @@ +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 { + PhoneBillXmlHelper helper = new PhoneBillXmlHelper(); + + + DocumentBuilderFactory factory = + DocumentBuilderFactory.newInstance(); + factory.setValidating(true); + + DocumentBuilder builder = + factory.newDocumentBuilder(); + builder.setErrorHandler(helper); + builder.setEntityResolver(helper); + + builder.parse(this.getClass().getResourceAsStream("valid-phonebill.xml")); + } + + @Test + void cantParseInvalidXmlFile() throws ParserConfigurationException { + PhoneBillXmlHelper helper = new PhoneBillXmlHelper(); + + + DocumentBuilderFactory factory = + DocumentBuilderFactory.newInstance(); + factory.setValidating(true); + + DocumentBuilder builder = + factory.newDocumentBuilder(); + builder.setErrorHandler(helper); + builder.setEntityResolver(helper); + + assertThrows(SAXParseException.class, () -> + builder.parse(this.getClass().getResourceAsStream("invalid-phonebill.xml")) + ); + } + +} 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 new file mode 100644 index 000000000..c9c187ca6 --- /dev/null +++ b/projects-parent/originals-parent/phonebill/src/test/resources/edu/pdx/cs/joy/phonebill/invalid-phonebill.xml @@ -0,0 +1,20 @@ + + + + + + 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 new file mode 100644 index 000000000..a55047e32 --- /dev/null +++ b/projects-parent/originals-parent/phonebill/src/test/resources/edu/pdx/cs/joy/phonebill/valid-phonebill.xml @@ -0,0 +1,33 @@ + + + + + + Dave + + 503-245-2345 + 607-777-1369 + + + + + + + + + + 603-868-1932 + 765-497-8254 + + + + + + + + diff --git a/projects-parent/projects/src/main/java/edu/pdx/cs/joy/phonebill.dtd b/projects-parent/projects/src/main/resources/edu/pdx/cs/joy/phonebill.dtd similarity index 100% rename from projects-parent/projects/src/main/java/edu/pdx/cs/joy/phonebill.dtd rename to projects-parent/projects/src/main/resources/edu/pdx/cs/joy/phonebill.dtd From 3f5f2019ee22a13fe99d6992317c16f5d21158d4 Mon Sep 17 00:00:00 2001 From: David Whitlock Date: Thu, 27 Nov 2025 15:41:28 -0800 Subject: [PATCH 28/77] Update third-party library dependencies to their latest versions. --- pom.xml | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/pom.xml b/pom.xml index d1cc91911..485ccc259 100644 --- a/pom.xml +++ b/pom.xml @@ -57,37 +57,37 @@ 21 21 - 33.4.8-jre + 33.5.0-jre 7.0.0 6.1.0 UTF-8 UTF-8 - 7.13.0 + 7.18.0 2.0.4 3.5.3 5.12.2 3.0 - 5.17.0 - 3.5.3 + 5.20.0 + 3.5.4 3.4.2 - 3.6.0 + 3.6.1 3.14.0 - 11.0.25 - 4.9.3.0 - 0.8.13 - 3.6.0 + 11.0.26 + 4.9.8.2 + 0.8.14 + 3.6.1 3.3.1 3.9.0 - 3.5.3 + 3.5.4 3.6.0 - 10.23.1 + 12.1.2 3.4.0 - 3.11.2 - 3.26.0 - 2.18.0 + 3.12.0 + 3.28.0 + 2.20.1 3.5.0 0.75 From 030e476c1e89ccfacab678eb2ecaffac7f837cd4 Mon Sep 17 00:00:00 2001 From: David Whitlock Date: Fri, 28 Nov 2025 05:18:58 -0800 Subject: [PATCH 29/77] Include XML parsing example in the Phone Bill archetype. --- .../META-INF/maven/archetype-metadata.xml | 1 + .../src/main/java/PhoneBillXmlHelper.java | 19 +++++++ .../src/main/java/TextDumper.java | 2 - .../pdx/cs410J/__artifactId__/package.html | 6 ++ .../src/test/java/PhoneBillXmlHelperTest.java | 55 +++++++++++++++++++ .../pdx/cs410J/__artifactId__/package.html | 6 ++ .../invalid-__artifactId__.xml | 23 ++++++++ .../valid-__artifactId__.xml | 36 ++++++++++++ 8 files changed, 146 insertions(+), 2 deletions(-) create mode 100644 projects-parent/archetypes-parent/phonebill-archetype/src/main/resources/archetype-resources/src/main/java/PhoneBillXmlHelper.java create mode 100644 projects-parent/archetypes-parent/phonebill-archetype/src/main/resources/archetype-resources/src/main/javadoc/edu/pdx/cs410J/__artifactId__/package.html create mode 100644 projects-parent/archetypes-parent/phonebill-archetype/src/main/resources/archetype-resources/src/test/java/PhoneBillXmlHelperTest.java create mode 100644 projects-parent/archetypes-parent/phonebill-archetype/src/main/resources/archetype-resources/src/test/javadoc/edu/pdx/cs410J/__artifactId__/package.html create mode 100644 projects-parent/archetypes-parent/phonebill-archetype/src/main/resources/archetype-resources/src/test/resources/__packageInPathFormat__/invalid-__artifactId__.xml create mode 100644 projects-parent/archetypes-parent/phonebill-archetype/src/main/resources/archetype-resources/src/test/resources/__packageInPathFormat__/valid-__artifactId__.xml 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 2d6e6e6f3..1721a943e 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,6 +45,7 @@ src/test/resources **/*.txt + **/*.xml diff --git a/projects-parent/archetypes-parent/phonebill-archetype/src/main/resources/archetype-resources/src/main/java/PhoneBillXmlHelper.java b/projects-parent/archetypes-parent/phonebill-archetype/src/main/resources/archetype-resources/src/main/java/PhoneBillXmlHelper.java new file mode 100644 index 000000000..e1d005314 --- /dev/null +++ b/projects-parent/archetypes-parent/phonebill-archetype/src/main/resources/archetype-resources/src/main/java/PhoneBillXmlHelper.java @@ -0,0 +1,19 @@ +#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 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/archetypes-parent/phonebill-archetype/src/main/resources/archetype-resources/src/main/java/TextDumper.java b/projects-parent/archetypes-parent/phonebill-archetype/src/main/resources/archetype-resources/src/main/java/TextDumper.java index 4c2293a9e..2df0a2f93 100644 --- a/projects-parent/archetypes-parent/phonebill-archetype/src/main/resources/archetype-resources/src/main/java/TextDumper.java +++ b/projects-parent/archetypes-parent/phonebill-archetype/src/main/resources/archetype-resources/src/main/java/TextDumper.java @@ -3,10 +3,8 @@ #set( $symbol_escape = '\' ) package ${package}; -import edu.pdx.cs.joy.AppointmentBookDumper; import edu.pdx.cs.joy.PhoneBillDumper; -import java.io.IOException; import java.io.PrintWriter; import java.io.Writer; diff --git a/projects-parent/archetypes-parent/phonebill-archetype/src/main/resources/archetype-resources/src/main/javadoc/edu/pdx/cs410J/__artifactId__/package.html b/projects-parent/archetypes-parent/phonebill-archetype/src/main/resources/archetype-resources/src/main/javadoc/edu/pdx/cs410J/__artifactId__/package.html new file mode 100644 index 000000000..6d4adf245 --- /dev/null +++ b/projects-parent/archetypes-parent/phonebill-archetype/src/main/resources/archetype-resources/src/main/javadoc/edu/pdx/cs410J/__artifactId__/package.html @@ -0,0 +1,6 @@ +#set( $symbol_pound = '#' ) +#set( $symbol_dollar = '$' ) +#set( $symbol_escape = '\' ) + +

Contains the application classes for the Phone Bill project.

+ \ No newline at end of file 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 new file mode 100644 index 000000000..43cefe97f --- /dev/null +++ b/projects-parent/archetypes-parent/phonebill-archetype/src/main/resources/archetype-resources/src/test/java/PhoneBillXmlHelperTest.java @@ -0,0 +1,55 @@ +#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 { + PhoneBillXmlHelper helper = new PhoneBillXmlHelper(); + + + DocumentBuilderFactory factory = + DocumentBuilderFactory.newInstance(); + factory.setValidating(true); + + DocumentBuilder builder = + factory.newDocumentBuilder(); + builder.setErrorHandler(helper); + builder.setEntityResolver(helper); + + builder.parse(this.getClass().getResourceAsStream("valid-${artifactId}.xml")); + } + + @Test + void cantParseInvalidXmlFile() throws ParserConfigurationException { + PhoneBillXmlHelper helper = new PhoneBillXmlHelper(); + + + DocumentBuilderFactory factory = + DocumentBuilderFactory.newInstance(); + factory.setValidating(true); + + DocumentBuilder builder = + factory.newDocumentBuilder(); + builder.setErrorHandler(helper); + builder.setEntityResolver(helper); + + assertThrows(SAXParseException.class, () -> + builder.parse(this.getClass().getResourceAsStream("invalid-${artifactId}.xml")) + ); + } + +} diff --git a/projects-parent/archetypes-parent/phonebill-archetype/src/main/resources/archetype-resources/src/test/javadoc/edu/pdx/cs410J/__artifactId__/package.html b/projects-parent/archetypes-parent/phonebill-archetype/src/main/resources/archetype-resources/src/test/javadoc/edu/pdx/cs410J/__artifactId__/package.html new file mode 100644 index 000000000..a579da692 --- /dev/null +++ b/projects-parent/archetypes-parent/phonebill-archetype/src/main/resources/archetype-resources/src/test/javadoc/edu/pdx/cs410J/__artifactId__/package.html @@ -0,0 +1,6 @@ +#set( $symbol_pound = '#' ) +#set( $symbol_dollar = '$' ) +#set( $symbol_escape = '\' ) + +

Contains the test classes for the Phone Bill project.

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