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/.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/examples/pom.xml b/examples/pom.xml index 72da988e6..aaa03b532 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -3,28 +3,23 @@ joy io.github.davidwhitlock.joy - 1.2.3 + 1.2.4-SNAPSHOT 4.0.0 examples examples - 1.3.4 + 1.4.0-SNAPSHOT https://www.cs.pdx.edu/~whitlock - - io.github.davidwhitlock.joy - family - 1.1.5 - io.github.davidwhitlock.joy projects - 3.0.3 + 3.0.4-SNAPSHOT com.sun.mail jakarta.mail - 2.0.1 + 2.0.2 com.google.inject @@ -34,20 +29,39 @@ 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 + + com.h2database + h2 + ${h2.version} + io.github.davidwhitlock.joy projects - 3.0.3 + 3.0.4-SNAPSHOT tests test + + + + org.codehaus.mojo + build-helper-maven-plugin + ${build-helper-maven-plugin.version} + + + org.apache.maven.plugins + maven-failsafe-plugin + ${surefire.version} + + + diff --git a/examples/src/it/java/edu/pdx/cs/joy/jdbc/DepartmentDAOIT.java b/examples/src/it/java/edu/pdx/cs/joy/jdbc/DepartmentDAOIT.java new file mode 100644 index 000000000..24290e3b2 --- /dev/null +++ b/examples/src/it/java/edu/pdx/cs/joy/jdbc/DepartmentDAOIT.java @@ -0,0 +1,101 @@ +package edu.pdx.cs.joy.jdbc; + +import org.junit.jupiter.api.*; + +import java.io.File; +import java.sql.Connection; +import java.sql.SQLException; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; + +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class DepartmentDAOIT { + + private static final String TEST_DEPARTMENT_NAME = "Computer Science"; + private static int generatedDepartmentId; + + private static String dbFilePath; + private Connection connection; + private DepartmentDAO departmentDAO; + + @BeforeAll + public static void createTable() throws SQLException { + // Create database file in temporary directory + String tempDir = System.getProperty("java.io.tmpdir"); + dbFilePath = tempDir + File.separator + "DepartmentDAOIT.db"; + + // Connect to the file-based H2 database + Connection connection = H2DatabaseHelper.createFileBasedConnection(new File(dbFilePath)); + + // Create the departments table + DepartmentDAOImpl.createTable(connection); + + connection.close(); + } + + @BeforeEach + public void setUp() throws SQLException { + // Connect to the existing database file + connection = H2DatabaseHelper.createFileBasedConnection(new File(dbFilePath)); + departmentDAO = new DepartmentDAOImpl(connection); + } + + @AfterEach + public void tearDown() throws SQLException { + if (connection != null && !connection.isClosed()) { + connection.close(); + } + } + + @AfterAll + public static void cleanUp() throws SQLException { + // Connect one final time to drop the table and clean up + Connection connection = H2DatabaseHelper.createFileBasedConnection(new File(dbFilePath)); + DepartmentDAOImpl.dropTable(connection); + connection.close(); + + // Delete the database files + deleteIfExists(new File(dbFilePath + ".mv.db")); + deleteIfExists(new File(dbFilePath + ".trace.db")); + } + + private static void deleteIfExists(File file) { + if (file.exists()) { + assertThat(file.delete(), is(true)); + } + } + + @Test + @Order(1) + public void testPersistDepartment() throws SQLException { + // Create and persist a department (ID will be auto-generated) + Department department = new Department(TEST_DEPARTMENT_NAME); + departmentDAO.save(department); + + // Store the auto-generated ID for use in subsequent tests + generatedDepartmentId = department.getId(); + + // Verify that an ID was auto-generated + assertThat(generatedDepartmentId, is(greaterThan(0))); + + // Verify the department was saved by fetching it in the same test + Department fetchedDepartment = departmentDAO.findById(generatedDepartmentId); + assertThat(fetchedDepartment, is(notNullValue())); + assertThat(fetchedDepartment.getId(), is(equalTo(generatedDepartmentId))); + assertThat(fetchedDepartment.getName(), is(equalTo(TEST_DEPARTMENT_NAME))); + } + + @Test + @Order(2) + public void testFindPersistedDepartment() throws SQLException { + // Search for the department that was persisted in the previous test + // using the auto-generated ID + Department fetchedDepartment = departmentDAO.findById(generatedDepartmentId); + + // Validate that the department persisted between test methods + assertThat(fetchedDepartment, is(notNullValue())); + assertThat(fetchedDepartment.getId(), is(equalTo(generatedDepartmentId))); + assertThat(fetchedDepartment.getName(), is(equalTo(TEST_DEPARTMENT_NAME))); + } +} diff --git a/examples/src/it/java/edu/pdx/cs/joy/jdbc/ExecuteH2DatabaseStatementIT.java b/examples/src/it/java/edu/pdx/cs/joy/jdbc/ExecuteH2DatabaseStatementIT.java new file mode 100644 index 000000000..3a04a3acb --- /dev/null +++ b/examples/src/it/java/edu/pdx/cs/joy/jdbc/ExecuteH2DatabaseStatementIT.java @@ -0,0 +1,303 @@ +package edu.pdx.cs.joy.jdbc; + +import edu.pdx.cs.joy.InvokeMainTestCase; +import org.junit.jupiter.api.*; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.sql.Connection; +import java.sql.SQLException; +import java.time.LocalDate; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; + +/** + * Integration test for ExecuteH2DatabaseStatement that validates SQL execution + * against an H2 database and verifies output formatting. + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class ExecuteH2DatabaseStatementIT extends InvokeMainTestCase { + + private static File tempDbFile; + private static String dbFilePath; + + @BeforeAll + public static void setUp() throws IOException, SQLException { + // Create a temporary file for the database + tempDbFile = Files.createTempFile("ExecuteH2DatabaseStatementIT", ".db").toFile(); + dbFilePath = tempDbFile.getAbsolutePath(); + + // Remove the .db extension since H2 will add .mv.db + if (dbFilePath.endsWith(".db")) { + dbFilePath = dbFilePath.substring(0, dbFilePath.length() - 3); + } + + // Create and populate a test database + createAndPopulateDatabase(dbFilePath); + } + + @AfterAll + public static void tearDown() { + // Clean up database files + deleteIfExists(new File(dbFilePath + ".mv.db")); + deleteIfExists(new File(dbFilePath + ".trace.db")); + deleteIfExists(tempDbFile); + } + + private static void deleteIfExists(File file) { + if (file.exists()) { + file.delete(); + } + } + + /** + * Creates and populates a test database with departments, academic terms, and courses. + */ + private static void createAndPopulateDatabase(String dbPath) throws SQLException { + File dbFile = new File(dbPath); + + try (Connection connection = H2DatabaseHelper.createFileBasedConnection(dbFile)) { + // Create tables + DepartmentDAOImpl.createTable(connection); + AcademicTermDAOImpl.createTable(connection); + CourseDAOImpl.createTable(connection); + + // Create DAOs + DepartmentDAO departmentDAO = new DepartmentDAOImpl(connection); + AcademicTermDAO termDAO = new AcademicTermDAOImpl(connection); + CourseDAO courseDAO = new CourseDAOImpl(connection); + + // Insert departments + Department csDept = new Department("Computer Science"); + departmentDAO.save(csDept); + + Department mathDept = new Department("Mathematics"); + departmentDAO.save(mathDept); + + Department physicsDept = new Department("Physics"); + departmentDAO.save(physicsDept); + + // Insert academic terms + AcademicTerm fall2024 = new AcademicTerm("Fall 2024", + LocalDate.of(2024, 9, 1), LocalDate.of(2024, 12, 15)); + termDAO.save(fall2024); + + AcademicTerm spring2025 = new AcademicTerm("Spring 2025", + LocalDate.of(2025, 1, 15), LocalDate.of(2025, 5, 30)); + termDAO.save(spring2025); + + // Insert courses + Course javaCourse = new Course("Introduction to Java", csDept.getId(), 4); + courseDAO.save(javaCourse); + + Course dataStructures = new Course("Data Structures", csDept.getId(), 4); + courseDAO.save(dataStructures); + + Course calculus = new Course("Calculus I", mathDept.getId(), 4); + courseDAO.save(calculus); + + Course physics101 = new Course("Physics I", physicsDept.getId(), 5); + courseDAO.save(physics101); + } + } + + @Test + @Order(1) + public void testSelectAllDepartments() { + MainMethodResult result = invokeMain(ExecuteH2DatabaseStatement.class, dbFilePath, "SELECT * FROM departments ORDER BY id"); + String output = result.getTextWrittenToStandardOut(); + + // Validate output contains all departments + assertThat(output, containsString("Computer Science")); + assertThat(output, containsString("Mathematics")); + assertThat(output, containsString("Physics")); + assertThat(output, containsString("3 row(s) returned")); + + // Validate table formatting + assertThat(output, containsString("+")); + assertThat(output, containsString("|")); + assertThat(output, containsString("ID")); + assertThat(output, containsString("NAME")); + } + + @Test + @Order(2) + public void testSelectSingleDepartment() { + MainMethodResult result = invokeMain(ExecuteH2DatabaseStatement.class, dbFilePath, + "SELECT name FROM departments WHERE id = 1"); + String output = result.getTextWrittenToStandardOut(); + + assertThat(output, containsString("Computer Science")); + assertThat(output, containsString("1 row(s) returned")); + assertThat(output, containsString("NAME")); + } + + @Test + @Order(3) + public void testSelectAllCourses() { + MainMethodResult result = invokeMain(ExecuteH2DatabaseStatement.class, dbFilePath, + "SELECT * FROM courses ORDER BY id"); + String output = result.getTextWrittenToStandardOut(); + + assertThat(output, containsString("Introduction to Java")); + assertThat(output, containsString("Data Structures")); + assertThat(output, containsString("Calculus I")); + assertThat(output, containsString("Physics I")); + assertThat(output, containsString("4 row(s) returned")); + assertThat(output, containsString("TITLE")); + assertThat(output, containsString("CREDITS")); + } + + @Test + @Order(4) + public void testSelectAcademicTerms() { + MainMethodResult result = invokeMain(ExecuteH2DatabaseStatement.class, dbFilePath, + "SELECT id, name FROM academic_terms ORDER BY id"); + String output = result.getTextWrittenToStandardOut(); + + assertThat(output, containsString("Fall 2024")); + assertThat(output, containsString("Spring 2025")); + assertThat(output, containsString("2 row(s) returned")); + } + + @Test + @Order(5) + public void testSelectWithJoin() { + MainMethodResult result = invokeMain(ExecuteH2DatabaseStatement.class, dbFilePath, + "SELECT c.title, d.name FROM courses c JOIN departments d ON c.department_id = d.id WHERE d.name = 'Computer Science'"); + String output = result.getTextWrittenToStandardOut(); + + assertThat(output, containsString("Introduction to Java")); + assertThat(output, containsString("Data Structures")); + assertThat(output, containsString("2 row(s) returned")); + } + + @Test + @Order(6) + public void testInsertOperation() { + MainMethodResult result = invokeMain(ExecuteH2DatabaseStatement.class, dbFilePath, + "INSERT INTO departments (name) VALUES ('Engineering')"); + String output = result.getTextWrittenToStandardOut(); + + assertThat(output, containsString("Statement executed successfully")); + assertThat(output, containsString("Rows affected: 1")); + + // Verify the insert worked by querying + MainMethodResult queryResult = invokeMain(ExecuteH2DatabaseStatement.class, dbFilePath, + "SELECT * FROM departments WHERE name = 'Engineering'"); + String queryOutput = queryResult.getTextWrittenToStandardOut(); + assertThat(queryOutput, containsString("Engineering")); + assertThat(queryOutput, containsString("1 row(s) returned")); + } + + @Test + @Order(7) + public void testUpdateOperation() { + MainMethodResult result = invokeMain(ExecuteH2DatabaseStatement.class, dbFilePath, + "UPDATE departments SET name = 'Applied Mathematics' WHERE name = 'Mathematics'"); + String output = result.getTextWrittenToStandardOut(); + + assertThat(output, containsString("Statement executed successfully")); + assertThat(output, containsString("Rows affected: 1")); + + // Verify the update worked + MainMethodResult queryResult = invokeMain(ExecuteH2DatabaseStatement.class, dbFilePath, + "SELECT name FROM departments WHERE name = 'Applied Mathematics'"); + String queryOutput = queryResult.getTextWrittenToStandardOut(); + assertThat(queryOutput, containsString("Applied Mathematics")); + } + + @Test + @Order(8) + public void testDeleteOperation() { + // Delete the Engineering department that was added by testInsertOperation + // This avoids foreign key issues and doesn't affect other tests + MainMethodResult result = invokeMain(ExecuteH2DatabaseStatement.class, dbFilePath, + "DELETE FROM departments WHERE name = 'Engineering'"); + String output = result.getTextWrittenToStandardOut(); + + assertThat(output, containsString("Statement executed successfully")); + assertThat(output, containsString("Rows affected: 1")); + + // Verify the delete worked + MainMethodResult queryResult = invokeMain(ExecuteH2DatabaseStatement.class, dbFilePath, + "SELECT * FROM departments WHERE name = 'Engineering'"); + String queryOutput = queryResult.getTextWrittenToStandardOut(); + assertThat(queryOutput, containsString("No rows returned")); + } + + @Test + @Order(9) + public void testSelectNoResults() { + MainMethodResult result = invokeMain(ExecuteH2DatabaseStatement.class, dbFilePath, + "SELECT * FROM departments WHERE id = 999"); + String output = result.getTextWrittenToStandardOut(); + + assertThat(output, containsString("No rows returned")); + } + + @Test + @Order(10) + public void testMissingArguments() { + MainMethodResult result = invokeMain(ExecuteH2DatabaseStatement.class); + String errorOutput = result.getTextWrittenToStandardError(); + + assertThat(errorOutput, containsString("Missing required arguments")); + assertThat(errorOutput, containsString("Usage:")); + assertThat(errorOutput, containsString("SELECT:")); + assertThat(errorOutput, containsString("INSERT:")); + assertThat(errorOutput, containsString("UPDATE:")); + assertThat(errorOutput, containsString("DELETE:")); + } + + @Test + @Order(11) + public void testDatabasePathDisplayed() { + MainMethodResult result = invokeMain(ExecuteH2DatabaseStatement.class, dbFilePath, + "SELECT * FROM departments"); + String output = result.getTextWrittenToStandardOut(); + + assertThat(output, containsString("Connecting to H2 database:")); + assertThat(output, containsString(dbFilePath)); + } + + @Test + @Order(12) + public void testSqlStatementDisplayed() { + String sql = "SELECT * FROM departments"; + MainMethodResult result = invokeMain(ExecuteH2DatabaseStatement.class, dbFilePath, sql); + String output = result.getTextWrittenToStandardOut(); + + assertThat(output, containsString("Executing SQL:")); + assertThat(output, containsString(sql)); + } + + @Test + @Order(13) + public void testTableFormattingWithNullValues() { + // Insert a department with a course that has no credits (hypothetically, for null testing) + // Since our schema doesn't allow nulls, we'll just verify formatting works correctly + MainMethodResult result = invokeMain(ExecuteH2DatabaseStatement.class, dbFilePath, + "SELECT id, name FROM departments ORDER BY id"); + String output = result.getTextWrittenToStandardOut(); + + // Verify table has proper borders + assertThat(output, matchesPattern("(?s).*\\+[-+]+\\+.*")); + assertThat(output, matchesPattern("(?s).*\\|.*\\|.*")); + } + + @Test + @Order(14) + public void testCountQuery() { + MainMethodResult result = invokeMain(ExecuteH2DatabaseStatement.class, dbFilePath, + "SELECT COUNT(*) AS total FROM courses"); + String output = result.getTextWrittenToStandardOut(); + + assertThat(output, containsString("TOTAL")); + assertThat(output, containsString("4")); + assertThat(output, containsString("1 row(s) returned")); + } +} + diff --git a/examples/src/it/java/edu/pdx/cs/joy/jdbc/ManageDepartmentsIT.java b/examples/src/it/java/edu/pdx/cs/joy/jdbc/ManageDepartmentsIT.java new file mode 100644 index 000000000..84f87a703 --- /dev/null +++ b/examples/src/it/java/edu/pdx/cs/joy/jdbc/ManageDepartmentsIT.java @@ -0,0 +1,191 @@ +package edu.pdx.cs.joy.jdbc; + +import edu.pdx.cs.joy.InvokeMainTestCase; +import org.junit.jupiter.api.*; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; + +/** + * Integration tests for the ManageDepartments command-line program. + * These tests verify that all CRUD operations work correctly when invoked via the main method. + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class ManageDepartmentsIT extends InvokeMainTestCase { + + private static File tempDbFile; + private static String dbFilePath; + + @BeforeAll + public static void setUp() throws IOException { + tempDbFile = Files.createTempFile("ManageDepartmentsIT", ".db").toFile(); + dbFilePath = tempDbFile.getAbsolutePath(); + // Remove the .db extension since H2 will add .mv.db + if (dbFilePath.endsWith(".db")) { + dbFilePath = dbFilePath.substring(0, dbFilePath.length() - 3); + } + } + + @AfterAll + public static void tearDown() { + // Clean up database files + deleteIfExists(new File(dbFilePath + ".mv.db")); + deleteIfExists(new File(dbFilePath + ".trace.db")); + deleteIfExists(tempDbFile); + } + + private static void deleteIfExists(File file) { + if (file.exists()) { + file.delete(); + } + } + + @Test + @Order(1) + public void testCreateDepartment() { + MainMethodResult result = invokeMain(ManageDepartments.class, dbFilePath, "create", "Computer Science"); + + String output = result.getTextWrittenToStandardOut(); + assertThat(output, containsString("Successfully created department:")); + assertThat(output, containsString("Computer Science")); + assertThat(output, containsString("Auto-generated ID:")); + } + + @Test + @Order(2) + public void testCreateSecondDepartment() { + MainMethodResult result = invokeMain(ManageDepartments.class, dbFilePath, "create", "Mathematics"); + + String output = result.getTextWrittenToStandardOut(); + assertThat(output, containsString("Successfully created department:")); + assertThat(output, containsString("Mathematics")); + assertThat(output, containsString("Auto-generated ID:")); + } + + @Test + @Order(3) + public void testRetrieveDepartmentById() { + MainMethodResult result = invokeMain(ManageDepartments.class, dbFilePath, "retrieve", "1"); + + String output = result.getTextWrittenToStandardOut(); + assertThat(output, containsString("Found department:")); + assertThat(output, containsString("id=1")); + assertThat(output, containsString("Computer Science")); + } + + @Test + @Order(4) + public void testRetrieveNonExistentDepartment() { + MainMethodResult result = invokeMain(ManageDepartments.class, dbFilePath, "retrieve", "999"); + + String output = result.getTextWrittenToStandardOut(); + assertThat(output, containsString("No department found with ID: 999")); + } + + @Test + @Order(5) + public void testListAllDepartments() { + MainMethodResult result = invokeMain(ManageDepartments.class, dbFilePath, "list"); + + String output = result.getTextWrittenToStandardOut(); + assertThat(output, containsString("Found 2 department(s)")); + assertThat(output, containsString("Computer Science")); + assertThat(output, containsString("Mathematics")); + } + + @Test + @Order(6) + public void testUpdateDepartment() { + MainMethodResult result = invokeMain(ManageDepartments.class, dbFilePath, "update", "1", "CS Department"); + + String output = result.getTextWrittenToStandardOut(); + assertThat(output, containsString("Successfully updated department:")); + assertThat(output, containsString("id=1")); + assertThat(output, containsString("CS Department")); + } + + @Test + @Order(7) + public void testRetrieveUpdatedDepartment() { + MainMethodResult result = invokeMain(ManageDepartments.class, dbFilePath, "retrieve", "1"); + + String output = result.getTextWrittenToStandardOut(); + assertThat(output, containsString("CS Department")); + assertThat(output, not(containsString("Computer Science"))); + } + + @Test + @Order(8) + public void testDeleteDepartment() { + MainMethodResult result = invokeMain(ManageDepartments.class, dbFilePath, "delete", "2"); + + String output = result.getTextWrittenToStandardOut(); + assertThat(output, containsString("Successfully deleted department:")); + assertThat(output, containsString("id=2")); + assertThat(output, containsString("Mathematics")); + } + + @Test + @Order(9) + public void testListAfterDelete() { + MainMethodResult result = invokeMain(ManageDepartments.class, dbFilePath, "list"); + + String output = result.getTextWrittenToStandardOut(); + assertThat(output, containsString("Found 1 department(s)")); + assertThat(output, containsString("CS Department")); + assertThat(output, not(containsString("Mathematics"))); + } + + @Test + public void testMissingArguments() { + MainMethodResult result = invokeMain(ManageDepartments.class, dbFilePath); + + String errorOutput = result.getTextWrittenToStandardError(); + assertThat(errorOutput, containsString("Usage: java ManageDepartments")); + } + + @Test + public void testUnknownCommand() { + MainMethodResult result = invokeMain(ManageDepartments.class, dbFilePath, "invalid"); + + String errorOutput = result.getTextWrittenToStandardError(); + assertThat(errorOutput, containsString("Unknown command: invalid")); + assertThat(errorOutput, containsString("Usage:")); + } + + @Test + public void testCreateWithoutName() { + MainMethodResult result = invokeMain(ManageDepartments.class, dbFilePath, "create"); + + String errorOutput = result.getTextWrittenToStandardError(); + assertThat(errorOutput, containsString("Missing department name for create command")); + } + + @Test + public void testRetrieveWithoutId() { + MainMethodResult result = invokeMain(ManageDepartments.class, dbFilePath, "retrieve"); + + String errorOutput = result.getTextWrittenToStandardError(); + assertThat(errorOutput, containsString("Missing department ID for retrieve command")); + } + + @Test + public void testUpdateWithMissingArguments() { + MainMethodResult result = invokeMain(ManageDepartments.class, dbFilePath, "update", "1"); + + String errorOutput = result.getTextWrittenToStandardError(); + assertThat(errorOutput, containsString("Missing arguments for update command")); + } + + @Test + public void testDeleteWithoutId() { + MainMethodResult result = invokeMain(ManageDepartments.class, dbFilePath, "delete"); + + String errorOutput = result.getTextWrittenToStandardError(); + assertThat(errorOutput, containsString("Missing department ID for delete command")); + } +} diff --git a/examples/src/it/java/edu/pdx/cs/joy/jdbc/PrintH2DatabaseSchemaIT.java b/examples/src/it/java/edu/pdx/cs/joy/jdbc/PrintH2DatabaseSchemaIT.java new file mode 100644 index 000000000..6d2fdadcb --- /dev/null +++ b/examples/src/it/java/edu/pdx/cs/joy/jdbc/PrintH2DatabaseSchemaIT.java @@ -0,0 +1,186 @@ +package edu.pdx.cs.joy.jdbc; + +import edu.pdx.cs.joy.InvokeMainTestCase; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.sql.Connection; +import java.sql.SQLException; +import java.time.LocalDate; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; + +/** + * Integration test for PrintH2DatabaseSchema that validates the program correctly + * displays database schema information using the JDBC DatabaseMetaData API. + */ +public class PrintH2DatabaseSchemaIT extends InvokeMainTestCase { + + private static File tempDbFile; + private static String dbFilePath; + + @BeforeAll + public static void setUp() throws IOException, SQLException { + // Create a temporary file for the database + tempDbFile = Files.createTempFile("PrintH2DatabaseSchemaIT", ".db").toFile(); + dbFilePath = tempDbFile.getAbsolutePath(); + + // Remove the .db extension since H2 will add .mv.db + if (dbFilePath.endsWith(".db")) { + dbFilePath = dbFilePath.substring(0, dbFilePath.length() - 3); + } + + // Create a test database with Department, AcademicTerm, and Course tables + createTestDatabase(dbFilePath); + } + + @AfterAll + public static void tearDown() { + // Clean up database files + deleteIfExists(new File(dbFilePath + ".mv.db")); + deleteIfExists(new File(dbFilePath + ".trace.db")); + deleteIfExists(tempDbFile); + } + + private static void deleteIfExists(File file) { + if (file.exists()) { + file.delete(); + } + } + + /** + * Creates a test database with Department, AcademicTerm, and Course tables + * and populates them with sample data. + */ + private static void createTestDatabase(String dbPath) throws SQLException { + File dbFile = new File(dbPath); + + try (Connection connection = H2DatabaseHelper.createFileBasedConnection(dbFile)) { + // Create tables + DepartmentDAOImpl.createTable(connection); + AcademicTermDAOImpl.createTable(connection); + CourseDAOImpl.createTable(connection); + + // Create DAOs + DepartmentDAO departmentDAO = new DepartmentDAOImpl(connection); + AcademicTermDAO termDAO = new AcademicTermDAOImpl(connection); + CourseDAO courseDAO = new CourseDAOImpl(connection); + + // Insert sample departments + Department csDept = new Department("Computer Science"); + departmentDAO.save(csDept); + + Department mathDept = new Department("Mathematics"); + departmentDAO.save(mathDept); + + // Insert sample academic terms + AcademicTerm fall2024 = new AcademicTerm("Fall 2024", + LocalDate.of(2024, 9, 1), LocalDate.of(2024, 12, 15)); + termDAO.save(fall2024); + + AcademicTerm spring2025 = new AcademicTerm("Spring 2025", + LocalDate.of(2025, 1, 15), LocalDate.of(2025, 5, 30)); + termDAO.save(spring2025); + + // Insert sample courses + Course javaCourse = new Course("Introduction to Java", csDept.getId(), 4); + courseDAO.save(javaCourse); + + Course dataStructures = new Course("Data Structures", csDept.getId(), 4); + courseDAO.save(dataStructures); + + Course calculus = new Course("Calculus I", mathDept.getId(), 4); + courseDAO.save(calculus); + } + } + + @Test + public void testPrintDatabaseSchema() { + // Invoke the main method with the database file path + MainMethodResult result = invokeMain(PrintH2DatabaseSchema.class, dbFilePath); + + String output = result.getTextWrittenToStandardOut(); + + // Validate database information is printed + assertThat(output, containsString("=== Database Information ===")); + assertThat(output, containsString("Database Product: H2")); + assertThat(output, containsString("Driver Name: H2 JDBC Driver")); + + // Validate tables section is printed + assertThat(output, containsString("=== Tables ===")); + + // Validate DEPARTMENTS table is shown + assertThat(output, containsString("Table: DEPARTMENTS")); + assertThat(output, containsString("ID")); + assertThat(output, containsString("NAME")); + assertThat(output, containsString("Primary Keys:")); + + // Validate ACADEMIC_TERMS table is shown + assertThat(output, containsString("Table: ACADEMIC_TERMS")); + assertThat(output, containsString("START_DATE")); + assertThat(output, containsString("END_DATE")); + + // Validate COURSES table is shown + assertThat(output, containsString("Table: COURSES")); + assertThat(output, containsString("TITLE")); + assertThat(output, containsString("DEPARTMENT_ID")); + assertThat(output, containsString("CREDITS")); + + // Validate foreign key relationship is shown + assertThat(output, containsString("Foreign Keys:")); + assertThat(output, containsString("DEPARTMENTS")); + } + + @Test + public void testColumnsAreDisplayed() { + MainMethodResult result = invokeMain(PrintH2DatabaseSchema.class, dbFilePath); + String output = result.getTextWrittenToStandardOut(); + + // Verify that column details are displayed + assertThat(output, containsString("Columns:")); + assertThat(output, containsString("NOT NULL")); + + // Check for specific column types + assertThat(output, containsString("BIGINT")); + assertThat(output, containsString("CHARACTER VARYING") ); + assertThat(output, containsString("DATE")); + assertThat(output, containsString("INTEGER")); + } + + @Test + public void testIndexesAreDisplayed() { + MainMethodResult result = invokeMain(PrintH2DatabaseSchema.class, dbFilePath); + String output = result.getTextWrittenToStandardOut(); + + // Verify that index information is displayed + assertThat(output, containsString("Indexes:")); + assertThat(output, containsString("PRIMARY_KEY")); + } + + @Test + public void testMissingArgumentShowsUsage() { + // Invoke without arguments + MainMethodResult result = invokeMain(PrintH2DatabaseSchema.class); + + String errorOutput = result.getTextWrittenToStandardError(); + + // Validate error message and usage are shown + assertThat(errorOutput, containsString("Missing database file path argument")); + assertThat(errorOutput, containsString("Usage: java PrintH2DatabaseSchema")); + } + + @Test + public void testDatabaseFilePathIsDisplayed() { + MainMethodResult result = invokeMain(PrintH2DatabaseSchema.class, dbFilePath); + String output = result.getTextWrittenToStandardOut(); + + // Verify the database file path is shown + assertThat(output, containsString("Reading schema from H2 database:")); + assertThat(output, containsString(dbFilePath)); + } +} diff --git a/examples/src/main/java/edu/pdx/cs/joy/jdbc/AcademicTerm.java b/examples/src/main/java/edu/pdx/cs/joy/jdbc/AcademicTerm.java new file mode 100644 index 000000000..de65125d4 --- /dev/null +++ b/examples/src/main/java/edu/pdx/cs/joy/jdbc/AcademicTerm.java @@ -0,0 +1,154 @@ +package edu.pdx.cs.joy.jdbc; + +import java.time.LocalDate; + +/** + * Represents an academic term (such as Fall 2024 or Spring 2025) during which courses are offered. + * Each term has a unique ID, a name, and start and end dates. + */ +public class AcademicTerm { + private int id; + private String name; + private LocalDate startDate; + private LocalDate endDate; + + /** + * Creates a new AcademicTerm with the specified name, start date, and end date. + * + * @param name the name of the term (e.g., "Fall 2024") + * @param startDate the start date of the term + * @param endDate the end date of the term + */ + public AcademicTerm(String name, LocalDate startDate, LocalDate endDate) { + this.name = name; + this.startDate = startDate; + this.endDate = endDate; + } + + /** + * Creates a new AcademicTerm with the specified ID, name, start date, and end date. + * + * @param id the unique identifier for the term + * @param name the name of the term (e.g., "Fall 2024") + * @param startDate the start date of the term + * @param endDate the end date of the term + */ + public AcademicTerm(int id, String name, LocalDate startDate, LocalDate endDate) { + this.id = id; + this.name = name; + this.startDate = startDate; + this.endDate = endDate; + } + + /** + * Creates a new AcademicTerm with no initial values. + * Useful for frameworks that use reflection. + */ + public AcademicTerm() { + } + + /** + * Returns the unique ID of this academic term. + * + * @return the term ID + */ + public int getId() { + return id; + } + + /** + * Sets the unique ID of this academic term. + * + * @param id the term ID + */ + public void setId(int id) { + this.id = id; + } + + /** + * Returns the name of this academic term. + * + * @return the term name + */ + public String getName() { + return name; + } + + /** + * Sets the name of this academic term. + * + * @param name the term name + */ + public void setName(String name) { + this.name = name; + } + + /** + * Returns the start date of this academic term. + * + * @return the start date + */ + public LocalDate getStartDate() { + return startDate; + } + + /** + * Sets the start date of this academic term. + * + * @param startDate the start date + */ + public void setStartDate(LocalDate startDate) { + this.startDate = startDate; + } + + /** + * Returns the end date of this academic term. + * + * @return the end date + */ + public LocalDate getEndDate() { + return endDate; + } + + /** + * Sets the end date of this academic term. + * + * @param endDate the end date + */ + public void setEndDate(LocalDate endDate) { + this.endDate = endDate; + } + + @Override + public String toString() { + return "AcademicTerm{" + + "id=" + id + + ", name='" + name + '\'' + + ", startDate=" + startDate + + ", endDate=" + endDate + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + AcademicTerm that = (AcademicTerm) o; + + if (id != that.id) return false; + if (name != null ? !name.equals(that.name) : that.name != null) return false; + if (startDate != null ? !startDate.equals(that.startDate) : that.startDate != null) return false; + return endDate != null ? endDate.equals(that.endDate) : that.endDate == null; + } + + @Override + public int hashCode() { + int result = id; + result = 31 * result + (name != null ? name.hashCode() : 0); + result = 31 * result + (startDate != null ? startDate.hashCode() : 0); + result = 31 * result + (endDate != null ? endDate.hashCode() : 0); + return result; + } +} + diff --git a/examples/src/main/java/edu/pdx/cs/joy/jdbc/AcademicTermDAO.java b/examples/src/main/java/edu/pdx/cs/joy/jdbc/AcademicTermDAO.java new file mode 100644 index 000000000..6b041ad01 --- /dev/null +++ b/examples/src/main/java/edu/pdx/cs/joy/jdbc/AcademicTermDAO.java @@ -0,0 +1,63 @@ +package edu.pdx.cs.joy.jdbc; + +import java.sql.SQLException; +import java.util.List; + +/** + * Data Access Object interface for managing AcademicTerm entities in the database. + */ +public interface AcademicTermDAO { + + /** + * Saves an academic term to the database. + * The term's ID will be automatically generated by the database and set on the object. + * + * @param term the academic term to save + * @throws SQLException if a database error occurs + */ + void save(AcademicTerm term) throws SQLException; + + /** + * Finds an academic term by its ID. + * + * @param id the ID to search for + * @return the academic term with the given ID, or null if not found + * @throws SQLException if a database error occurs + */ + AcademicTerm findById(int id) throws SQLException; + + /** + * Finds an academic term by its name. + * + * @param name the name to search for + * @return the academic term with the given name, or null if not found + * @throws SQLException if a database error occurs + */ + AcademicTerm findByName(String name) throws SQLException; + + /** + * Finds all academic terms in the database. + * + * @return a list of all academic terms + * @throws SQLException if a database error occurs + */ + List findAll() throws SQLException; + + /** + * Updates an existing academic term in the database. + * Uses the term's ID to identify which record to update. + * + * @param term the academic term to update + * @throws SQLException if a database error occurs + */ + void update(AcademicTerm term) throws SQLException; + + /** + * Deletes an academic term from the database by ID. + * + * @param id the ID of the academic term to delete + * @throws SQLException if a database error occurs + */ + void delete(int id) throws SQLException; +} + diff --git a/examples/src/main/java/edu/pdx/cs/joy/jdbc/AcademicTermDAOImpl.java b/examples/src/main/java/edu/pdx/cs/joy/jdbc/AcademicTermDAOImpl.java new file mode 100644 index 000000000..eccd56ea4 --- /dev/null +++ b/examples/src/main/java/edu/pdx/cs/joy/jdbc/AcademicTermDAOImpl.java @@ -0,0 +1,216 @@ +package edu.pdx.cs.joy.jdbc; + +import java.sql.*; +import java.util.ArrayList; +import java.util.List; + +/** + * Data Access Object implementation for managing AcademicTerm entities in the database. + * Demonstrates JDBC operations with date fields: CREATE, READ, UPDATE, DELETE. + */ +public class AcademicTermDAOImpl implements AcademicTermDAO { + + private final Connection connection; + + /** + * Creates a new AcademicTermDAOImpl with the specified database connection. + * + * @param connection the database connection to use + */ + public AcademicTermDAOImpl(Connection connection) { + this.connection = connection; + } + + /** + * Drops the academic_terms table from the database if it exists. + * + * @param connection the database connection to use + * @throws SQLException if a database error occurs + */ + public static void dropTable(Connection connection) throws SQLException { + try (Statement statement = connection.createStatement()) { + statement.execute("DROP TABLE IF EXISTS academic_terms"); + } + } + + /** + * Creates the academic_terms table in the database if it does not already exist. + * + * @param connection the database connection to use + * @throws SQLException if a database error occurs + */ + public static void createTable(Connection connection) throws SQLException { + try (Statement statement = connection.createStatement()) { + statement.execute( + "CREATE TABLE IF NOT EXISTS academic_terms (" + + " id IDENTITY PRIMARY KEY," + + " name VARCHAR(255) NOT NULL," + + " start_date DATE NOT NULL," + + " end_date DATE NOT NULL" + + ")" + ); + } + } + + /** + * Saves an academic term to the database. + * The term's ID will be automatically generated by the database and set on the object. + * + * @param term the academic term to save + * @throws SQLException if a database error occurs + */ + @Override + public void save(AcademicTerm term) throws SQLException { + String sql = "INSERT INTO academic_terms (name, start_date, end_date) VALUES (?, ?, ?)"; + + try (PreparedStatement statement = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { + statement.setString(1, term.getName()); + statement.setDate(2, Date.valueOf(term.getStartDate())); + statement.setDate(3, Date.valueOf(term.getEndDate())); + statement.executeUpdate(); + + // Retrieve the auto-generated ID and set it on the term object + try (ResultSet generatedKeys = statement.getGeneratedKeys()) { + if (generatedKeys.next()) { + int generatedId = generatedKeys.getInt(1); + term.setId(generatedId); + } else { + throw new SQLException("Creating academic term failed, no ID obtained."); + } + } + } + } + + /** + * Finds an academic term by its ID. + * + * @param id the ID to search for + * @return the academic term with the given ID, or null if not found + * @throws SQLException if a database error occurs + */ + @Override + public AcademicTerm findById(int id) throws SQLException { + String sql = "SELECT id, name, start_date, end_date FROM academic_terms WHERE id = ?"; + + try (PreparedStatement statement = connection.prepareStatement(sql)) { + statement.setInt(1, id); + + try (ResultSet resultSet = statement.executeQuery()) { + if (resultSet.next()) { + return extractAcademicTermFromResultSet(resultSet); + } + } + } + + return null; + } + + /** + * Finds an academic term by its name. + * + * @param name the name to search for + * @return the academic term with the given name, or null if not found + * @throws SQLException if a database error occurs + */ + @Override + public AcademicTerm findByName(String name) throws SQLException { + String sql = "SELECT id, name, start_date, end_date FROM academic_terms WHERE name = ?"; + + try (PreparedStatement statement = connection.prepareStatement(sql)) { + statement.setString(1, name); + + try (ResultSet resultSet = statement.executeQuery()) { + if (resultSet.next()) { + return extractAcademicTermFromResultSet(resultSet); + } + } + } + + return null; + } + + /** + * Finds all academic terms in the database. + * + * @return a list of all academic terms + * @throws SQLException if a database error occurs + */ + @Override + public List findAll() throws SQLException { + List terms = new ArrayList<>(); + String sql = "SELECT id, name, start_date, end_date FROM academic_terms ORDER BY start_date"; + + try (Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery(sql)) { + while (resultSet.next()) { + terms.add(extractAcademicTermFromResultSet(resultSet)); + } + } + + return terms; + } + + /** + * Updates an existing academic term in the database. + * Uses the term's ID to identify which record to update. + * + * @param term the academic term to update + * @throws SQLException if a database error occurs + */ + @Override + public void update(AcademicTerm term) throws SQLException { + String sql = "UPDATE academic_terms SET name = ?, start_date = ?, end_date = ? WHERE id = ?"; + + try (PreparedStatement statement = connection.prepareStatement(sql)) { + statement.setString(1, term.getName()); + statement.setDate(2, Date.valueOf(term.getStartDate())); + statement.setDate(3, Date.valueOf(term.getEndDate())); + statement.setInt(4, term.getId()); + + int rowsAffected = statement.executeUpdate(); + if (rowsAffected == 0) { + throw new SQLException("Update failed, no academic term found with ID: " + term.getId()); + } + } + } + + /** + * Deletes an academic term from the database by ID. + * + * @param id the ID of the academic term to delete + * @throws SQLException if a database error occurs + */ + @Override + public void delete(int id) throws SQLException { + String sql = "DELETE FROM academic_terms WHERE id = ?"; + + try (PreparedStatement statement = connection.prepareStatement(sql)) { + statement.setInt(1, id); + int rowsAffected = statement.executeUpdate(); + + if (rowsAffected == 0) { + throw new SQLException("Delete failed, no academic term found with ID: " + id); + } + } + } + + /** + * Extracts an AcademicTerm object from the current row of a ResultSet. + * + * @param resultSet the result set positioned at an academic term row + * @return an AcademicTerm object with data from the result set + * @throws SQLException if a database error occurs + */ + private AcademicTerm extractAcademicTermFromResultSet(ResultSet resultSet) throws SQLException { + int id = resultSet.getInt("id"); + String name = resultSet.getString("name"); + Date startDate = resultSet.getDate("start_date"); + Date endDate = resultSet.getDate("end_date"); + + AcademicTerm term = new AcademicTerm(name, startDate.toLocalDate(), endDate.toLocalDate()); + term.setId(id); + return term; + } +} + + diff --git a/examples/src/main/java/edu/pdx/cs/joy/jdbc/Course.java b/examples/src/main/java/edu/pdx/cs/joy/jdbc/Course.java new file mode 100644 index 000000000..fc673450c --- /dev/null +++ b/examples/src/main/java/edu/pdx/cs/joy/jdbc/Course.java @@ -0,0 +1,136 @@ +package edu.pdx.cs.joy.jdbc; + +/** + * Represents a course in a college course catalog. + * Each course has a unique ID, a title, is associated with a department, and has a number of credits. + */ +public class Course { + private int id; + private String title; + private int departmentId; + private int credits; + + /** + * Creates a new Course with the specified title, department ID, and credits. + * + * @param title the title of the course + * @param departmentId the numeric ID of the department offering this course + * @param credits the number of credits for this course + */ + public Course(String title, int departmentId, int credits) { + this.title = title; + this.departmentId = departmentId; + this.credits = credits; + } + + /** + * Creates a new Course with no initial values. + * Useful for frameworks that use reflection. + */ + public Course() { + } + + /** + * Returns the unique ID of this course. + * + * @return the course ID + */ + public int getId() { + return id; + } + + /** + * Sets the unique ID of this course. + * + * @param id the course ID + */ + public void setId(int id) { + this.id = id; + } + + /** + * Returns the title of this course. + * + * @return the course title + */ + public String getTitle() { + return title; + } + + /** + * Sets the title of this course. + * + * @param title the course title + */ + public void setTitle(String title) { + this.title = title; + } + + /** + * Returns the department ID for this course. + * + * @return the department ID + */ + public int getDepartmentId() { + return departmentId; + } + + /** + * Sets the department ID for this course. + * + * @param departmentId the department ID + */ + public void setDepartmentId(int departmentId) { + this.departmentId = departmentId; + } + + /** + * Returns the number of credits for this course. + * + * @return the number of credits + */ + public int getCredits() { + return credits; + } + + /** + * Sets the number of credits for this course. + * + * @param credits the number of credits + */ + public void setCredits(int credits) { + this.credits = credits; + } + + @Override + public String toString() { + return "Course{" + + "id=" + id + + ", title='" + title + '\'' + + ", departmentId=" + departmentId + + ", credits=" + credits + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Course course = (Course) o; + + if (id != course.id) return false; + if (departmentId != course.departmentId) return false; + if (credits != course.credits) return false; + return title != null ? title.equals(course.title) : course.title == null; + } + + @Override + public int hashCode() { + int result = id; + result = 31 * result + (title != null ? title.hashCode() : 0); + result = 31 * result + departmentId; + result = 31 * result + credits; + return result; + } +} diff --git a/examples/src/main/java/edu/pdx/cs/joy/jdbc/CourseDAO.java b/examples/src/main/java/edu/pdx/cs/joy/jdbc/CourseDAO.java new file mode 100644 index 000000000..32d05bf60 --- /dev/null +++ b/examples/src/main/java/edu/pdx/cs/joy/jdbc/CourseDAO.java @@ -0,0 +1,47 @@ +package edu.pdx.cs.joy.jdbc; + +import java.sql.SQLException; +import java.util.List; + +/** + * Data Access Object interface for managing Course entities in the database. + */ +public interface CourseDAO { + + /** + * Saves a course to the database. + * The course's ID will be automatically generated by the database and set on the object. + * + * @param course the course to save + * @throws SQLException if a database error occurs + */ + void save(Course course) throws SQLException; + + /** + * Finds a course by its title. + * + * @param title the title to search for + * @return the course with the given title, or null if not found + * @throws SQLException if a database error occurs + */ + Course findByTitle(String title) throws SQLException; + + /** + * Finds all courses associated with a specific department. + * + * @param departmentId the department ID to search for + * @return a list of courses in the department + * @throws SQLException if a database error occurs + */ + List findByDepartmentId(int departmentId) throws SQLException; + + /** + * Updates an existing course in the database. + * Uses the course's ID to identify which record to update. + * + * @param course the course to update + * @throws SQLException if a database error occurs + */ + void update(Course course) throws SQLException; +} + diff --git a/examples/src/main/java/edu/pdx/cs/joy/jdbc/CourseDAOImpl.java b/examples/src/main/java/edu/pdx/cs/joy/jdbc/CourseDAOImpl.java new file mode 100644 index 000000000..e93997426 --- /dev/null +++ b/examples/src/main/java/edu/pdx/cs/joy/jdbc/CourseDAOImpl.java @@ -0,0 +1,177 @@ +package edu.pdx.cs.joy.jdbc; + +import java.sql.*; +import java.util.ArrayList; +import java.util.List; + +/** + * Data Access Object implementation for managing Course entities in the database. + * Demonstrates basic JDBC operations: CREATE, READ, UPDATE. + */ +public class CourseDAOImpl implements CourseDAO { + + private final Connection connection; + + /** + * Creates a new CourseDAOImpl with the specified database connection. + * + * @param connection the database connection to use + */ + public CourseDAOImpl(Connection connection) { + this.connection = connection; + } + + /** + * Drops the courses table from the database if it exists. + * + * @param connection the database connection to use + * @throws SQLException if a database error occurs + */ + public static void dropTable(Connection connection) throws SQLException { + try (Statement statement = connection.createStatement()) { + statement.execute("DROP TABLE IF EXISTS courses"); + } + } + + /** + * Creates the courses table in the database. + * + * @param connection the database connection to use + * @throws SQLException if a database error occurs + */ + public static void createTable(Connection connection) throws SQLException { + try (Statement statement = connection.createStatement()) { + statement.execute( + "CREATE TABLE courses (" + + " id IDENTITY PRIMARY KEY," + + " title VARCHAR(255) NOT NULL," + + " department_id INTEGER NOT NULL," + + " credits INTEGER NOT NULL," + + " FOREIGN KEY (department_id) REFERENCES departments(id)" + + ")" + ); + } + } + + /** + * Saves a course to the database. + * The course's ID will be automatically generated by the database and set on the object. + * + * @param course the course to save + * @throws SQLException if a database error occurs + */ + @Override + public void save(Course course) throws SQLException { + String sql = "INSERT INTO courses (title, department_id, credits) VALUES (?, ?, ?)"; + + try (PreparedStatement statement = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { + statement.setString(1, course.getTitle()); + statement.setInt(2, course.getDepartmentId()); + statement.setInt(3, course.getCredits()); + statement.executeUpdate(); + + // Retrieve the auto-generated ID and set it on the course object + try (ResultSet generatedKeys = statement.getGeneratedKeys()) { + if (generatedKeys.next()) { + int generatedId = generatedKeys.getInt(1); + course.setId(generatedId); + } else { + throw new SQLException("Creating course failed, no ID obtained."); + } + } + } + } + + /** + * Finds a course by its title. + * + * @param title the title to search for + * @return the course with the given title, or null if not found + * @throws SQLException if a database error occurs + */ + @Override + public Course findByTitle(String title) throws SQLException { + String sql = "SELECT id, title, department_id, credits FROM courses WHERE title = ?"; + + try (PreparedStatement statement = connection.prepareStatement(sql)) { + statement.setString(1, title); + + try (ResultSet resultSet = statement.executeQuery()) { + if (resultSet.next()) { + return extractCourseFromResultSet(resultSet); + } + } + } + + return null; + } + + /** + * Finds all courses associated with a specific department. + * + * @param departmentId the department ID to search for + * @return a list of courses in the department + * @throws SQLException if a database error occurs + */ + @Override + public List findByDepartmentId(int departmentId) throws SQLException { + List courses = new ArrayList<>(); + String sql = "SELECT id, title, department_id, credits FROM courses WHERE department_id = ?"; + + try (PreparedStatement statement = connection.prepareStatement(sql)) { + statement.setInt(1, departmentId); + + try (ResultSet resultSet = statement.executeQuery()) { + while (resultSet.next()) { + courses.add(extractCourseFromResultSet(resultSet)); + } + } + } + + return courses; + } + + /** + * Extracts a Course object from the current row of a ResultSet. + * + * @param resultSet the result set positioned at a course row + * @return a Course object with data from the result set + * @throws SQLException if a database error occurs + */ + private Course extractCourseFromResultSet(ResultSet resultSet) throws SQLException { + int id = resultSet.getInt("id"); + String title = resultSet.getString("title"); + int departmentId = resultSet.getInt("department_id"); + int credits = resultSet.getInt("credits"); + + Course course = new Course(title, departmentId, credits); + course.setId(id); + return course; + } + + /** + * Updates an existing course in the database. + * Uses the course's ID to identify which record to update. + * + * @param course the course to update + * @throws SQLException if a database error occurs + */ + @Override + public void update(Course course) throws SQLException { + String sql = "UPDATE courses SET title = ?, department_id = ?, credits = ? WHERE id = ?"; + + try (PreparedStatement statement = connection.prepareStatement(sql)) { + statement.setString(1, course.getTitle()); + statement.setInt(2, course.getDepartmentId()); + statement.setInt(3, course.getCredits()); + statement.setInt(4, course.getId()); + + int rowsAffected = statement.executeUpdate(); + if (rowsAffected == 0) { + throw new SQLException("Update failed, no course found with ID: " + course.getId()); + } + } + } +} + + diff --git a/examples/src/main/java/edu/pdx/cs/joy/jdbc/Department.java b/examples/src/main/java/edu/pdx/cs/joy/jdbc/Department.java new file mode 100644 index 000000000..4ccdefd59 --- /dev/null +++ b/examples/src/main/java/edu/pdx/cs/joy/jdbc/Department.java @@ -0,0 +1,101 @@ +package edu.pdx.cs.joy.jdbc; + +/** + * Represents a department in a college course catalog. + * Each department has a unique ID and a name. + */ +public class Department { + private int id; + private String name; + + /** + * Creates a new Department with the specified ID and name. + * + * @param id the unique identifier for the department + * @param name the name of the department + */ + public Department(int id, String name) { + this.id = id; + this.name = name; + } + + /** + * Creates a new Department with the specified name. + * The ID will be auto-generated when the department is saved to the database. + * + * @param name the name of the department + */ + public Department(String name) { + this.name = name; + } + + /** + * Creates a new Department with no initial values. + * Useful for frameworks that use reflection. + */ + public Department() { + } + + /** + * Returns the ID of this department. + * + * @return the department ID + */ + public int getId() { + return id; + } + + /** + * Sets the ID of this department. + * + * @param id the department ID + */ + public void setId(int id) { + this.id = id; + } + + /** + * Returns the name of this department. + * + * @return the department name + */ + public String getName() { + return name; + } + + /** + * Sets the name of this department. + * + * @param name the department name + */ + public void setName(String name) { + this.name = name; + } + + @Override + public String toString() { + return "Department{" + + "id=" + id + + ", name='" + name + '\'' + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Department that = (Department) o; + + if (id != that.id) return false; + return name != null ? name.equals(that.name) : that.name == null; + } + + @Override + public int hashCode() { + int result = id; + result = 31 * result + (name != null ? name.hashCode() : 0); + return result; + } +} + diff --git a/examples/src/main/java/edu/pdx/cs/joy/jdbc/DepartmentDAO.java b/examples/src/main/java/edu/pdx/cs/joy/jdbc/DepartmentDAO.java new file mode 100644 index 000000000..bcdc18f9e --- /dev/null +++ b/examples/src/main/java/edu/pdx/cs/joy/jdbc/DepartmentDAO.java @@ -0,0 +1,62 @@ +package edu.pdx.cs.joy.jdbc; + +import java.sql.SQLException; +import java.util.List; + +/** + * Data Access Object interface for managing Department entities in the database. + */ +public interface DepartmentDAO { + + /** + * Saves a department to the database. + * The department's ID will be automatically generated by the database. + * + * @param department the department to save + * @throws SQLException if a database error occurs + */ + void save(Department department) throws SQLException; + + /** + * Finds a department by its ID. + * + * @param id the ID to search for + * @return the department with the given ID, or null if not found + * @throws SQLException if a database error occurs + */ + Department findById(int id) throws SQLException; + + /** + * Finds a department by its name. + * + * @param name the name to search for + * @return the department with the given name, or null if not found + * @throws SQLException if a database error occurs + */ + Department findByName(String name) throws SQLException; + + /** + * Finds all departments in the database. + * + * @return a list of all departments + * @throws SQLException if a database error occurs + */ + List findAll() throws SQLException; + + /** + * Updates an existing department in the database. + * + * @param department the department to update + * @throws SQLException if a database error occurs + */ + void update(Department department) throws SQLException; + + /** + * Deletes a department from the database by ID. + * + * @param id the ID of the department to delete + * @throws SQLException if a database error occurs + */ + void delete(int id) throws SQLException; +} + diff --git a/examples/src/main/java/edu/pdx/cs/joy/jdbc/DepartmentDAOImpl.java b/examples/src/main/java/edu/pdx/cs/joy/jdbc/DepartmentDAOImpl.java new file mode 100644 index 000000000..3d233b255 --- /dev/null +++ b/examples/src/main/java/edu/pdx/cs/joy/jdbc/DepartmentDAOImpl.java @@ -0,0 +1,204 @@ +package edu.pdx.cs.joy.jdbc; + +import java.sql.*; +import java.util.ArrayList; +import java.util.List; + +/** + * Data Access Object implementation for managing Department entities in the database. + * Demonstrates JDBC operations with auto-generated keys: CREATE, READ, UPDATE, DELETE. + */ +public class DepartmentDAOImpl implements DepartmentDAO { + + private final Connection connection; + + /** + * Creates a new DepartmentDAOImpl with the specified database connection. + * + * @param connection the database connection to use + */ + public DepartmentDAOImpl(Connection connection) { + this.connection = connection; + } + + /** + * Drops the departments table from the database if it exists. + * + * @param connection the database connection to use + * @throws SQLException if a database error occurs + */ + public static void dropTable(Connection connection) throws SQLException { + try (Statement statement = connection.createStatement()) { + statement.execute("DROP TABLE IF EXISTS departments"); + } + } + + /** + * Creates the departments table in the database if it does not already exist. + * + * @param connection the database connection to use + * @throws SQLException if a database error occurs + */ + public static void createTable(Connection connection) throws SQLException { + try (Statement statement = connection.createStatement()) { + statement.execute( + "CREATE TABLE IF NOT EXISTS departments (" + + " id IDENTITY PRIMARY KEY," + + " name VARCHAR(255) NOT NULL" + + ")" + ); + } + } + + /** + * Saves a department to the database. + * The department's ID will be automatically generated by the database. + * + * @param department the department to save + * @throws SQLException if a database error occurs + */ + @Override + public void save(Department department) throws SQLException { + String sql = "INSERT INTO departments (name) VALUES (?)"; + + try (PreparedStatement statement = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { + statement.setString(1, department.getName()); + statement.executeUpdate(); + + // Retrieve the auto-generated ID and set it on the department object + try (ResultSet generatedKeys = statement.getGeneratedKeys()) { + if (generatedKeys.next()) { + int generatedId = generatedKeys.getInt(1); + department.setId(generatedId); + } else { + throw new SQLException("Creating department failed, no ID obtained."); + } + } + } + } + + /** + * Finds a department by its ID. + * + * @param id the ID to search for + * @return the department with the given ID, or null if not found + * @throws SQLException if a database error occurs + */ + @Override + public Department findById(int id) throws SQLException { + String sql = "SELECT id, name FROM departments WHERE id = ?"; + + try (PreparedStatement statement = connection.prepareStatement(sql)) { + statement.setInt(1, id); + + try (ResultSet resultSet = statement.executeQuery()) { + if (resultSet.next()) { + return extractDepartmentFromResultSet(resultSet); + } + } + } + + return null; + } + + /** + * Finds a department by its name. + * + * @param name the name to search for + * @return the department with the given name, or null if not found + * @throws SQLException if a database error occurs + */ + @Override + public Department findByName(String name) throws SQLException { + String sql = "SELECT id, name FROM departments WHERE name = ?"; + + try (PreparedStatement statement = connection.prepareStatement(sql)) { + statement.setString(1, name); + + try (ResultSet resultSet = statement.executeQuery()) { + if (resultSet.next()) { + return extractDepartmentFromResultSet(resultSet); + } + } + } + + return null; + } + + /** + * Finds all departments in the database. + * + * @return a list of all departments + * @throws SQLException if a database error occurs + */ + @Override + public List findAll() throws SQLException { + List departments = new ArrayList<>(); + String sql = "SELECT id, name FROM departments ORDER BY id"; + + try (Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery(sql)) { + while (resultSet.next()) { + departments.add(extractDepartmentFromResultSet(resultSet)); + } + } + + return departments; + } + + /** + * Extracts a Department object from the current row of a ResultSet. + * + * @param resultSet the result set positioned at a department row + * @return a Department object with data from the result set + * @throws SQLException if a database error occurs + */ + private Department extractDepartmentFromResultSet(ResultSet resultSet) throws SQLException { + int id = resultSet.getInt("id"); + String name = resultSet.getString("name"); + return new Department(id, name); + } + + /** + * Updates an existing department in the database. + * + * @param department the department to update + * @throws SQLException if a database error occurs + */ + @Override + public void update(Department department) throws SQLException { + String sql = "UPDATE departments SET name = ? WHERE id = ?"; + + try (PreparedStatement statement = connection.prepareStatement(sql)) { + statement.setString(1, department.getName()); + statement.setInt(2, department.getId()); + int rowsAffected = statement.executeUpdate(); + + if (rowsAffected == 0) { + throw new SQLException("Update failed, no department found with ID: " + department.getId()); + } + } + } + + /** + * Deletes a department from the database by ID. + * + * @param id the ID of the department to delete + * @throws SQLException if a database error occurs + */ + @Override + public void delete(int id) throws SQLException { + String sql = "DELETE FROM departments WHERE id = ?"; + + try (PreparedStatement statement = connection.prepareStatement(sql)) { + statement.setInt(1, id); + int rowsAffected = statement.executeUpdate(); + + if (rowsAffected == 0) { + throw new SQLException("Delete failed, no department found with ID: " + id); + } + } + } +} + + diff --git a/examples/src/main/java/edu/pdx/cs/joy/jdbc/ExecuteH2DatabaseStatement.java b/examples/src/main/java/edu/pdx/cs/joy/jdbc/ExecuteH2DatabaseStatement.java new file mode 100644 index 000000000..6bb4c3583 --- /dev/null +++ b/examples/src/main/java/edu/pdx/cs/joy/jdbc/ExecuteH2DatabaseStatement.java @@ -0,0 +1,168 @@ +package edu.pdx.cs.joy.jdbc; + +import java.io.File; +import java.sql.*; + +/** + * A command-line program that executes SQL statements against an H2 database. + * Supports all CRUD operations (INSERT, SELECT, UPDATE, DELETE) and displays + * results in a human-readable format. + */ +public class ExecuteH2DatabaseStatement { + + /** + * Main method that takes a database file path and SQL statement as arguments. + * + * @param args command line arguments: args[0] = database file path, args[1] = SQL statement + * @throws SQLException if a database error occurs + */ + public static void main(String[] args) throws SQLException { + if (args.length < 2) { + System.err.println("Missing required arguments"); + System.err.println("Usage: java ExecuteH2DatabaseStatement "); + System.err.println(); + System.err.println("Examples:"); + System.err.println(" SELECT: java ExecuteH2DatabaseStatement mydb.db \"SELECT * FROM users\""); + System.err.println(" INSERT: java ExecuteH2DatabaseStatement mydb.db \"INSERT INTO users (name) VALUES ('John')\""); + System.err.println(" UPDATE: java ExecuteH2DatabaseStatement mydb.db \"UPDATE users SET name='Jane' WHERE id=1\""); + System.err.println(" DELETE: java ExecuteH2DatabaseStatement mydb.db \"DELETE FROM users WHERE id=1\""); + return; + } + + String dbFilePath = args[0]; + String sqlStatement = args[1]; + + File dbFile = new File(dbFilePath); + System.out.println("Connecting to H2 database: " + dbFile.getAbsolutePath()); + System.out.println("Executing SQL: " + sqlStatement); + System.out.println(); + + try (Connection connection = H2DatabaseHelper.createFileBasedConnection(dbFile)) { + executeStatement(connection, sqlStatement); + } + } + + /** + * Executes a SQL statement and displays the results. + * + * @param connection the database connection + * @param sql the SQL statement to execute + * @throws SQLException if a database error occurs + */ + private static void executeStatement(Connection connection, String sql) throws SQLException { + try (Statement statement = connection.createStatement()) { + // Determine if this is a query (SELECT) or update (INSERT/UPDATE/DELETE) + boolean isQuery = statement.execute(sql); + + if (isQuery) { + // Handle SELECT queries + try (ResultSet resultSet = statement.getResultSet()) { + displayResultSet(resultSet); + } + } else { + // Handle INSERT, UPDATE, DELETE + int rowsAffected = statement.getUpdateCount(); + System.out.println("Statement executed successfully"); + System.out.println("Rows affected: " + rowsAffected); + } + } + } + + /** + * Displays a ResultSet in a formatted table. + * + * @param resultSet the result set to display + * @throws SQLException if a database error occurs + */ + private static void displayResultSet(ResultSet resultSet) throws SQLException { + ResultSetMetaData metaData = resultSet.getMetaData(); + int columnCount = metaData.getColumnCount(); + + // Calculate column widths + int[] columnWidths = new int[columnCount]; + String[] columnNames = new String[columnCount]; + + for (int i = 1; i <= columnCount; i++) { + columnNames[i - 1] = metaData.getColumnLabel(i); + columnWidths[i - 1] = Math.max(columnNames[i - 1].length(), 10); + } + + // Collect all rows to calculate proper column widths + java.util.List rows = new java.util.ArrayList<>(); + while (resultSet.next()) { + String[] row = new String[columnCount]; + for (int i = 1; i <= columnCount; i++) { + Object value = resultSet.getObject(i); + row[i - 1] = value != null ? value.toString() : "NULL"; + columnWidths[i - 1] = Math.max(columnWidths[i - 1], row[i - 1].length()); + } + rows.add(row); + } + + // Print header + printSeparator(columnWidths); + printRow(columnNames, columnWidths); + printSeparator(columnWidths); + + // Print data rows + if (rows.isEmpty()) { + System.out.println("No rows returned"); + } else { + for (String[] row : rows) { + printRow(row, columnWidths); + } + printSeparator(columnWidths); + System.out.println(rows.size() + " row(s) returned"); + } + } + + /** + * Prints a separator line. + * + * @param columnWidths array of column widths + */ + private static void printSeparator(int[] columnWidths) { + System.out.print("+"); + for (int width : columnWidths) { + for (int i = 0; i < width + 2; i++) { + System.out.print("-"); + } + System.out.print("+"); + } + System.out.println(); + } + + /** + * Prints a data row. + * + * @param values array of values to print + * @param columnWidths array of column widths + */ + private static void printRow(String[] values, int[] columnWidths) { + System.out.print("|"); + for (int i = 0; i < values.length; i++) { + System.out.print(" "); + System.out.print(padRight(values[i], columnWidths[i])); + System.out.print(" |"); + } + System.out.println(); + } + + /** + * Pads a string to the right with spaces. + * + * @param str the string to pad + * @param length the desired length + * @return the padded string + */ + private static String padRight(String str, int length) { + if (str.length() >= length) { + return str; + } + StringBuilder sb = new StringBuilder(str); + while (sb.length() < length) { + sb.append(' '); + } + return sb.toString(); + } +} diff --git a/examples/src/main/java/edu/pdx/cs/joy/jdbc/H2DatabaseHelper.java b/examples/src/main/java/edu/pdx/cs/joy/jdbc/H2DatabaseHelper.java new file mode 100644 index 000000000..e09501877 --- /dev/null +++ b/examples/src/main/java/edu/pdx/cs/joy/jdbc/H2DatabaseHelper.java @@ -0,0 +1,38 @@ +package edu.pdx.cs.joy.jdbc; + +import java.io.File; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; + +/** + * Helper class for creating connections to H2 databases. + * Provides factory methods for both in-memory and file-based H2 databases. + */ +public class H2DatabaseHelper { + + /** + * Creates a connection to an in-memory H2 database. + * The database will persist as long as at least one connection remains open + * due to the DB_CLOSE_DELAY=-1 parameter. + * + * @param databaseName the name of the in-memory database + * @return a connection to the in-memory H2 database + * @throws SQLException if a database error occurs + */ + public static Connection createInMemoryConnection(String databaseName) throws SQLException { + return DriverManager.getConnection("jdbc:h2:mem:" + databaseName + ";DB_CLOSE_DELAY=-1"); + } + + /** + * Creates a connection to a file-based H2 database. + * The database will be persisted to a file at the specified path. + * + * @param databaseFilesDirectory the database file (without the .mv.db extension) + * @return a connection to the file-based H2 database + * @throws SQLException if a database error occurs + */ + public static Connection createFileBasedConnection(File databaseFilesDirectory) throws SQLException { + return DriverManager.getConnection("jdbc:h2:" + databaseFilesDirectory.getAbsolutePath()); + } +} diff --git a/examples/src/main/java/edu/pdx/cs/joy/jdbc/ManageDepartments.java b/examples/src/main/java/edu/pdx/cs/joy/jdbc/ManageDepartments.java new file mode 100644 index 000000000..ad66e748f --- /dev/null +++ b/examples/src/main/java/edu/pdx/cs/joy/jdbc/ManageDepartments.java @@ -0,0 +1,157 @@ +package edu.pdx.cs.joy.jdbc; + +import java.io.File; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.List; + +/** + * A command-line program that demonstrates CRUD operations on Department objects + * using the DepartmentDAO class with an H2 database. + */ +public class ManageDepartments { + + /** + * Main method that performs CRUD operations on departments in an H2 database file. + * + * @param args command line arguments where args[0] is the path to the database file + * and args[1] is the command (create, retrieve, update, delete, or list) + * @throws SQLException if a database error occurs + */ + public static void main(String[] args) throws SQLException { + if (args.length < 2) { + printUsage(); + return; + } + + String dbFilePath = args[0]; + String command = args[1].toLowerCase(); + + File dbFile = new File(dbFilePath); + + try (Connection connection = H2DatabaseHelper.createFileBasedConnection(dbFile)) { + // Create the departments table if it doesn't exist + DepartmentDAOImpl.createTable(connection); + + // Create a new DepartmentDAO + DepartmentDAO departmentDAO = new DepartmentDAOImpl(connection); + + switch (command) { + case "create": + handleCreate(args, departmentDAO); + break; + case "retrieve": + handleRetrieve(args, departmentDAO); + break; + case "update": + handleUpdate(args, departmentDAO); + break; + case "delete": + handleDelete(args, departmentDAO); + break; + case "list": + handleList(departmentDAO); + break; + default: + System.err.println("Unknown command: " + command); + printUsage(); + } + } + } + + private static void printUsage() { + System.err.println("Usage: java ManageDepartments [args...]"); + System.err.println(); + System.err.println("Commands:"); + System.err.println(" create - Create a new department with the given name"); + System.err.println(" retrieve - Retrieve a department by ID"); + System.err.println(" update - Update the name of a department"); + System.err.println(" delete - Delete a department by ID"); + System.err.println(" list - List all departments"); + } + + private static void handleCreate(String[] args, DepartmentDAO departmentDAO) throws SQLException { + if (args.length < 3) { + System.err.println("Missing department name for create command"); + System.err.println("Usage: java ManageDepartments create "); + return; + } + + String departmentName = args[2]; + Department department = new Department(departmentName); + departmentDAO.save(department); + + System.out.println("Successfully created department:"); + System.out.println(department); + System.out.println("Auto-generated ID: " + department.getId()); + } + + private static void handleRetrieve(String[] args, DepartmentDAO departmentDAO) throws SQLException { + if (args.length < 3) { + System.err.println("Missing department ID for retrieve command"); + System.err.println("Usage: java ManageDepartments retrieve "); + return; + } + + int id = Integer.parseInt(args[2]); + Department department = departmentDAO.findById(id); + + if (department == null) { + System.out.println("No department found with ID: " + id); + } else { + System.out.println("Found department:"); + System.out.println(department); + } + } + + private static void handleUpdate(String[] args, DepartmentDAO departmentDAO) throws SQLException { + if (args.length < 4) { + System.err.println("Missing arguments for update command"); + System.err.println("Usage: java ManageDepartments update "); + return; + } + + int id = Integer.parseInt(args[2]); + String newName = args[3]; + + Department department = departmentDAO.findById(id); + if (department == null) { + System.out.println("No department found with ID: " + id); + return; + } + + department.setName(newName); + departmentDAO.update(department); + + System.out.println("Successfully updated department:"); + System.out.println(department); + } + + private static void handleDelete(String[] args, DepartmentDAO departmentDAO) throws SQLException { + if (args.length < 3) { + System.err.println("Missing department ID for delete command"); + System.err.println("Usage: java ManageDepartments delete "); + return; + } + + int id = Integer.parseInt(args[2]); + Department department = departmentDAO.findById(id); + + if (department == null) { + System.out.println("No department found with ID: " + id); + return; + } + + departmentDAO.delete(id); + System.out.println("Successfully deleted department:"); + System.out.println(department); + } + + private static void handleList(DepartmentDAO departmentDAO) throws SQLException { + List allDepartments = departmentDAO.findAll(); + System.out.println("Found " + allDepartments.size() + " department(s)"); + for (Department dept : allDepartments) { + System.out.println(" " + dept); + } + } +} diff --git a/examples/src/main/java/edu/pdx/cs/joy/jdbc/PrintH2DatabaseSchema.java b/examples/src/main/java/edu/pdx/cs/joy/jdbc/PrintH2DatabaseSchema.java new file mode 100644 index 000000000..9d55b38be --- /dev/null +++ b/examples/src/main/java/edu/pdx/cs/joy/jdbc/PrintH2DatabaseSchema.java @@ -0,0 +1,217 @@ +package edu.pdx.cs.joy.jdbc; + +import java.io.File; +import java.sql.*; +import java.util.Set; + +/** + * A command-line program that uses the JDBC DatabaseMetaData API to print + * information about the tables in an H2 database file. + */ +public class PrintH2DatabaseSchema { + + private static final Set uninterestingTablePrefixes = Set.of( + "CONSTANTS", "ENUM_VALUES", "INDEXES", "INDEX_COLUMNS", "INFORMATION_SCHEMA_CATALOG_NAME", + "IN_DOUBT", "LOCKS", "QUERY_STATISTICS", "RIGHTS", "ROLES", "SESSIONS", "SESSION_STATE", + "SETTINGS", "SYNONYMS", "USERS" + ); + + /** + * Prints information about all tables in the database. + * + * @param connection the database connection + * @throws SQLException if a database error occurs + */ + private static void printDatabaseSchema(Connection connection) throws SQLException { + DatabaseMetaData metaData = connection.getMetaData(); + + System.out.println("=== Database Information ==="); + System.out.println("Database Product: " + metaData.getDatabaseProductName()); + System.out.println("Database Version: " + metaData.getDatabaseProductVersion()); + System.out.println("Driver Name: " + metaData.getDriverName()); + System.out.println("Driver Version: " + metaData.getDriverVersion()); + System.out.println(); + + // Get all tables + System.out.println("=== Tables ==="); + try (ResultSet tables = metaData.getTables(null, null, "%", new String[]{"TABLE"})) { + boolean foundTables = false; + + while (tables.next()) { + foundTables = true; + String tableName = tables.getString("TABLE_NAME"); + + if (tableNameIsNotInteresting(tableName)) { + continue; // Skip system or uninteresting tables + } + + String tableType = tables.getString("TABLE_TYPE"); + String remarks = tables.getString("REMARKS"); + + System.out.println("\nTable: " + tableName); + System.out.println(" Type: " + tableType); + if (remarks != null && !remarks.isEmpty()) { + System.out.println(" Remarks: " + remarks); + } + + // Print columns for this table + printTableColumns(metaData, tableName); + + // Print primary keys + printPrimaryKeys(metaData, tableName); + + // Print foreign keys + printForeignKeys(metaData, tableName); + + // Print indexes + printIndexes(metaData, tableName); + } + + if (!foundTables) { + System.out.println("No tables found in the database."); + } + } + } + + private static boolean tableNameIsNotInteresting(String tableName) { + return uninterestingTablePrefixes.stream().anyMatch(tableName::startsWith); + } + + /** + * Prints information about columns in a table. + */ + private static void printTableColumns(DatabaseMetaData metaData, String tableName) throws SQLException { + System.out.println(" Columns:"); + try (ResultSet columns = metaData.getColumns(null, null, tableName, "%")) { + while (columns.next()) { + String columnName = columns.getString("COLUMN_NAME"); + String columnType = columns.getString("TYPE_NAME"); + int columnSize = columns.getInt("COLUMN_SIZE"); + String nullable = columns.getString("IS_NULLABLE"); + String defaultValue = columns.getString("COLUMN_DEF"); + + System.out.print(" - " + columnName + " " + columnType); + if (columnSize > 0) { + System.out.print("(" + columnSize + ")"); + } + System.out.print(" [" + (nullable.equals("YES") ? "NULL" : "NOT NULL") + "]"); + if (defaultValue != null) { + System.out.print(" DEFAULT " + defaultValue); + } + System.out.println(); + } + } + } + + /** + * Prints information about primary keys in a table. + */ + private static void printPrimaryKeys(DatabaseMetaData metaData, String tableName) throws SQLException { + System.out.println(" Primary Keys:"); + try (ResultSet primaryKeys = metaData.getPrimaryKeys(null, null, tableName)) { + boolean foundKeys = false; + while (primaryKeys.next()) { + foundKeys = true; + String columnName = primaryKeys.getString("COLUMN_NAME"); + String pkName = primaryKeys.getString("PK_NAME"); + int keySeq = primaryKeys.getInt("KEY_SEQ"); + + System.out.println(" - " + columnName + " (Key: " + pkName + ", Sequence: " + keySeq + ")"); + } + if (!foundKeys) { + System.out.println(" None"); + } + } + } + + /** + * Prints information about foreign keys in a table. + */ + private static void printForeignKeys(DatabaseMetaData metaData, String tableName) throws SQLException { + System.out.println(" Foreign Keys:"); + try (ResultSet foreignKeys = metaData.getImportedKeys(null, null, tableName)) { + boolean foundKeys = false; + while (foreignKeys.next()) { + foundKeys = true; + String fkColumnName = foreignKeys.getString("FKCOLUMN_NAME"); + String pkTableName = foreignKeys.getString("PKTABLE_NAME"); + String pkColumnName = foreignKeys.getString("PKCOLUMN_NAME"); + String fkName = foreignKeys.getString("FK_NAME"); + + System.out.println(" - " + fkColumnName + " -> " + pkTableName + "(" + pkColumnName + ")" + + (fkName != null ? " [" + fkName + "]" : "")); + } + if (!foundKeys) { + System.out.println(" None"); + } + } + } + + /** + * Prints information about indexes in a table. + */ + private static void printIndexes(DatabaseMetaData metaData, String tableName) throws SQLException { + System.out.println(" Indexes:"); + try (ResultSet indexes = metaData.getIndexInfo(null, null, tableName, false, false)) { + boolean foundIndexes = false; + String lastIndexName = null; + StringBuilder indexColumns = new StringBuilder(); + + while (indexes.next()) { + String indexName = indexes.getString("INDEX_NAME"); + String columnName = indexes.getString("COLUMN_NAME"); + boolean nonUnique = indexes.getBoolean("NON_UNIQUE"); + + if (indexName == null) { + continue; // Skip table statistics + } + + if (lastIndexName != null && !lastIndexName.equals(indexName)) { + // Print the previous index + System.out.println(" - " + lastIndexName + " (" + indexColumns + ")"); + indexColumns.setLength(0); + } + + if (indexColumns.length() > 0) { + indexColumns.append(", "); + } + indexColumns.append(columnName); + lastIndexName = indexName; + foundIndexes = true; + } + + // Print the last index + if (lastIndexName != null) { + System.out.println(" - " + lastIndexName + " (" + indexColumns + ")"); + } + + if (!foundIndexes) { + System.out.println(" None"); + } + } + } + + /** + * Main method that takes a database file path and prints the schema. + * + * @param args command line arguments where args[0] is the path to the H2 database file + * @throws SQLException if a database error occurs + */ + public static void main(String[] args) throws SQLException { + if (args.length < 1) { + System.err.println("Missing database file path argument"); + System.err.println("Usage: java PrintH2DatabaseSchema "); + return; + } + + String dbFilePath = args[0]; + File dbFile = new File(dbFilePath); + + System.out.println("Reading schema from H2 database: " + dbFile.getAbsolutePath()); + System.out.println(); + + try (Connection connection = H2DatabaseHelper.createFileBasedConnection(dbFile)) { + printDatabaseSchema(connection); + } + } +} diff --git a/examples/src/main/java/edu/pdx/cs/joy/jdbc/SQLInjectionExample.java b/examples/src/main/java/edu/pdx/cs/joy/jdbc/SQLInjectionExample.java new file mode 100644 index 000000000..89e3be4c7 --- /dev/null +++ b/examples/src/main/java/edu/pdx/cs/joy/jdbc/SQLInjectionExample.java @@ -0,0 +1,214 @@ +package edu.pdx.cs.joy.jdbc; + +import java.math.BigDecimal; +import java.sql.*; + +/** + * Demonstrates the security vulnerability of using Statement versus PreparedStatement + * for database queries. This example shows how SQL injection attacks work and how + * PreparedStatement protects against them. + */ +public class SQLInjectionExample { + + /** + * Simple Employee class to hold employee data. + */ + static class Employee { + private final String name; + private final String email; + private final BigDecimal salary; + private final String password; + + public Employee(String name, String email, BigDecimal salary, String password) { + this.name = name; + this.email = email; + this.salary = salary; + this.password = password; + } + + public String getName() { + return name; + } + + public String getEmail() { + return email; + } + + public BigDecimal getSalary() { + return salary; + } + + public String getPassword() { + return password; + } + + @Override + public String toString() { + return "Employee{" + + "name='" + name + '\'' + + ", email='" + email + '\'' + + ", salary=" + salary + + ", password='" + password + '\'' + + '}'; + } + } + + /** + * Creates the employees table in the database. + */ + private static void createTable(Connection connection) throws SQLException { + try (Statement statement = connection.createStatement()) { + statement.execute( + "CREATE TABLE employees (" + + " id IDENTITY PRIMARY KEY," + + " name VARCHAR(255) NOT NULL," + + " email VARCHAR(255) NOT NULL," + + " salary DECIMAL(10, 2) NOT NULL," + + " password VARCHAR(255) NOT NULL" + + ")" + ); + } + } + + /** + * Inserts an employee into the database. + */ + private static void insertEmployee(Connection connection, Employee employee) throws SQLException { + String sql = "INSERT INTO employees (name, email, salary, password) VALUES (?, ?, ?, ?)"; + try (PreparedStatement statement = connection.prepareStatement(sql)) { + statement.setString(1, employee.getName()); + statement.setString(2, employee.getEmail()); + statement.setBigDecimal(3, employee.getSalary()); + statement.setString(4, employee.getPassword()); + statement.executeUpdate(); + } + } + + /** + * VULNERABLE: Uses Statement with string concatenation, allowing SQL injection. + * + * This method is intentionally vulnerable to demonstrate the security risk. + * A malicious user can use the username "Dave --'" to comment out the password check, + * gaining unauthorized access to the data. + */ + private static Employee getEmployeeDataWithStatement(Connection connection, String name, String password) throws SQLException { + // SECURITY VULNERABILITY: Building SQL with string concatenation + String sql = "SELECT name, email, salary, password FROM employees WHERE name = '" + name + "' AND password = '" + password + "'"; + + System.out.println("\nExecuting SQL with Statement:"); + System.out.println(" SQL: " + sql); + + try (Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery(sql)) { + + if (resultSet.next()) { + return createEmployee(resultSet); + } + } + + return null; + } + + /** + * SECURE: Uses PreparedStatement with parameter binding, preventing SQL injection. + * + * This method properly uses PreparedStatement, which treats user input as data + * rather than SQL code, preventing SQL injection attacks. + */ + private static Employee getEmployeeDataWithPreparedStatement(Connection connection, String name, String password) throws SQLException { + String sql = "SELECT name, email, salary, password FROM employees WHERE name = ? AND password = ?"; + + System.out.println("\nExecuting SQL with PreparedStatement:"); + System.out.println(" SQL: " + sql); + System.out.println(" Parameter 1 (name): " + name); + System.out.println(" Parameter 2 (password): " + password); + + try (PreparedStatement statement = connection.prepareStatement(sql)) { + statement.setString(1, name); + statement.setString(2, password); + + try (ResultSet resultSet = statement.executeQuery()) { + if (resultSet.next()) { + return createEmployee(resultSet); + } + } + } + + return null; + } + + private static Employee createEmployee(ResultSet resultSet) throws SQLException { + return new Employee( + resultSet.getString("name"), + resultSet.getString("email"), + resultSet.getBigDecimal("salary"), + resultSet.getString("password") + ); + } + + public static void main(String[] args) throws SQLException { + System.out.println("=== SQL Injection Demonstration ===\n"); + + // 1) Create an in-memory H2 database + Connection connection = H2DatabaseHelper.createInMemoryConnection("sqlInjectionDemo"); + + // Create the employees table + createTable(connection); + + // 2) Persist four employees with unique data + insertEmployee(connection, new Employee("Dave", "dave@example.com", new BigDecimal("85000.00"), "securePass123")); + insertEmployee(connection, new Employee("Alice", "alice@example.com", new BigDecimal("90000.00"), "aliceSecret")); + insertEmployee(connection, new Employee("Bob", "bob@example.com", new BigDecimal("78000.00"), "bobPassword")); + insertEmployee(connection, new Employee("Carol", "carol@example.com", new BigDecimal("92000.00"), "carolPass456")); + + System.out.println("Created employees table and inserted 4 employees.\n"); + + // Malicious input: Using SQL injection to bypass password check + String maliciousUsername = "Dave --'"; + String incorrectPassword = "wrongPassword"; + + System.out.println("Attempting to access Dave's data with:"); + System.out.println(" Username: \"" + maliciousUsername + "\""); + System.out.println(" Password: \"" + incorrectPassword + "\" (incorrect)"); + + // 3) Demonstrate SQL injection vulnerability with Statement + System.out.println("\n--- Using Statement (VULNERABLE) ---"); + try { + Employee employee = getEmployeeDataWithStatement(connection, maliciousUsername, incorrectPassword); + if (employee != null) { + System.out.println("\n️SQL INJECTION SUCCESSFUL! Unauthorized access granted:"); + System.out.println(" Name: " + employee.getName()); + System.out.println(" Email: " + employee.getEmail()); + System.out.println(" Salary: $" + String.format("%.2f", employee.getSalary())); + System.out.println(" Password: " + employee.getPassword()); + System.out.println("\nThe SQL comment '--' caused the password check to be ignored!"); + } else { + System.out.println("\nAccess denied (unexpected)"); + } + } catch (SQLException e) { + System.out.println("\nError: " + e.getMessage()); + } + + // 4) Demonstrate protection with PreparedStatement + System.out.println("\n--- Using PreparedStatement (SECURE) ---"); + try { + Employee employee = getEmployeeDataWithPreparedStatement(connection, maliciousUsername, incorrectPassword); + if (employee != null) { + System.out.println("\nAccess granted (unexpected):"); + System.out.println(" " + employee); + } else { + System.out.println("\nAccess denied - No employee found with that name and password combination."); + System.out.println("PreparedStatement treated 'Dave --'' as a literal username, not SQL code."); + } + } catch (SQLException e) { + System.out.println("\nError: " + e.getMessage()); + } + + // Clean up + connection.close(); + + System.out.println("\n=== Summary ==="); + System.out.println("Statement with string concatenation: VULNERABLE to SQL injection"); + System.out.println("PreparedStatement with parameters: SECURE against SQL injection"); + } +} diff --git a/examples/src/test/java/edu/pdx/cs/joy/jdbc/AcademicTermDAOTest.java b/examples/src/test/java/edu/pdx/cs/joy/jdbc/AcademicTermDAOTest.java new file mode 100644 index 000000000..a5a4b1956 --- /dev/null +++ b/examples/src/test/java/edu/pdx/cs/joy/jdbc/AcademicTermDAOTest.java @@ -0,0 +1,187 @@ +package edu.pdx.cs.joy.jdbc; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.sql.Connection; +import java.sql.SQLException; +import java.time.LocalDate; +import java.util.List; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; + +public class AcademicTermDAOTest { + + private Connection connection; + private AcademicTermDAO termDAO; + + @BeforeEach + public void setUp() throws SQLException { + // Create an in-memory H2 database + connection = H2DatabaseHelper.createInMemoryConnection("test"); + + // Drop and create the academic_terms table + AcademicTermDAOImpl.dropTable(connection); + AcademicTermDAOImpl.createTable(connection); + + // Initialize the DAO with the connection + termDAO = new AcademicTermDAOImpl(connection); + } + + @AfterEach + public void tearDown() throws SQLException { + if (connection != null && !connection.isClosed()) { + AcademicTermDAOImpl.dropTable(connection); + connection.close(); + } + } + + @Test + public void testPersistAndFetchAcademicTerm() throws SQLException { + // Create an academic term + String termName = "Fall 2024"; + LocalDate startDate = LocalDate.of(2024, 9, 1); + LocalDate endDate = LocalDate.of(2024, 12, 15); + AcademicTerm term = new AcademicTerm(termName, startDate, endDate); + + // Persist the term + termDAO.save(term); + + // Verify that an ID was auto-generated + int generatedId = term.getId(); + assertThat(generatedId, is(greaterThan(0))); + + // Fetch the term by ID + AcademicTerm fetchedTerm = termDAO.findById(generatedId); + + // Validate the fetched term using Hamcrest assertions + assertThat(fetchedTerm, is(notNullValue())); + assertThat(fetchedTerm.getId(), is(equalTo(generatedId))); + assertThat(fetchedTerm.getName(), is(equalTo(termName))); + assertThat(fetchedTerm.getStartDate(), is(equalTo(startDate))); + assertThat(fetchedTerm.getEndDate(), is(equalTo(endDate))); + } + + @Test + public void testFindByName() throws SQLException { + // Create and persist an academic term + String termName = "Spring 2025"; + LocalDate startDate = LocalDate.of(2025, 1, 15); + LocalDate endDate = LocalDate.of(2025, 5, 30); + AcademicTerm term = new AcademicTerm(termName, startDate, endDate); + termDAO.save(term); + + // Fetch the term by name + AcademicTerm fetchedTerm = termDAO.findByName(termName); + + // Validate the fetched term + assertThat(fetchedTerm, is(notNullValue())); + assertThat(fetchedTerm.getName(), is(equalTo(termName))); + assertThat(fetchedTerm.getStartDate(), is(equalTo(startDate))); + assertThat(fetchedTerm.getEndDate(), is(equalTo(endDate))); + } + + @Test + public void testFindNonExistentTerm() throws SQLException { + // Try to fetch a term that doesn't exist + AcademicTerm fetchedTerm = termDAO.findById(999); + assertThat(fetchedTerm, is(nullValue())); + + AcademicTerm fetchedByName = termDAO.findByName("Nonexistent Term"); + assertThat(fetchedByName, is(nullValue())); + } + + @Test + public void testFindAllTerms() throws SQLException { + // Create and persist multiple academic terms + AcademicTerm fall2024 = new AcademicTerm("Fall 2024", + LocalDate.of(2024, 9, 1), LocalDate.of(2024, 12, 15)); + AcademicTerm spring2025 = new AcademicTerm("Spring 2025", + LocalDate.of(2025, 1, 15), LocalDate.of(2025, 5, 30)); + AcademicTerm summer2025 = new AcademicTerm("Summer 2025", + LocalDate.of(2025, 6, 15), LocalDate.of(2025, 8, 30)); + + termDAO.save(fall2024); + termDAO.save(spring2025); + termDAO.save(summer2025); + + // Fetch all terms + List allTerms = termDAO.findAll(); + + // Validate using Hamcrest matchers + assertThat(allTerms, hasSize(3)); + assertThat(allTerms, hasItem(hasProperty("name", is("Fall 2024")))); + assertThat(allTerms, hasItem(hasProperty("name", is("Spring 2025")))); + assertThat(allTerms, hasItem(hasProperty("name", is("Summer 2025")))); + + // Verify terms are ordered by start date + assertThat(allTerms.get(0).getName(), is(equalTo("Fall 2024"))); + assertThat(allTerms.get(1).getName(), is(equalTo("Spring 2025"))); + assertThat(allTerms.get(2).getName(), is(equalTo("Summer 2025"))); + } + + @Test + public void testUpdateAcademicTerm() throws SQLException { + // Create and persist an academic term + AcademicTerm term = new AcademicTerm("Fall 2024", + LocalDate.of(2024, 9, 1), LocalDate.of(2024, 12, 15)); + termDAO.save(term); + int termId = term.getId(); + + // Update the term + term.setName("Fall 2024 (Extended)"); + term.setEndDate(LocalDate.of(2024, 12, 20)); + termDAO.update(term); + + // Fetch the updated term + AcademicTerm updatedTerm = termDAO.findById(termId); + + // Validate the update + assertThat(updatedTerm, is(notNullValue())); + assertThat(updatedTerm.getId(), is(equalTo(termId))); + assertThat(updatedTerm.getName(), is(equalTo("Fall 2024 (Extended)"))); + assertThat(updatedTerm.getEndDate(), is(equalTo(LocalDate.of(2024, 12, 20)))); + } + + @Test + public void testDeleteAcademicTerm() throws SQLException { + // Create and persist an academic term + AcademicTerm term = new AcademicTerm("Fall 2024", + LocalDate.of(2024, 9, 1), LocalDate.of(2024, 12, 15)); + termDAO.save(term); + int termId = term.getId(); + + // Verify the term exists + assertThat(termDAO.findById(termId), is(notNullValue())); + + // Delete the term + termDAO.delete(termId); + + // Verify the term no longer exists + assertThat(termDAO.findById(termId), is(nullValue())); + } + + @Test + public void testDatesArePersisted() throws SQLException { + // Create terms with different dates + AcademicTerm term1 = new AcademicTerm("Term 1", + LocalDate.of(2024, 1, 1), LocalDate.of(2024, 3, 31)); + AcademicTerm term2 = new AcademicTerm("Term 2", + LocalDate.of(2024, 6, 1), LocalDate.of(2024, 8, 31)); + + termDAO.save(term1); + termDAO.save(term2); + + // Fetch and verify dates + AcademicTerm fetched1 = termDAO.findByName("Term 1"); + AcademicTerm fetched2 = termDAO.findByName("Term 2"); + + assertThat(fetched1.getStartDate(), is(equalTo(LocalDate.of(2024, 1, 1)))); + assertThat(fetched1.getEndDate(), is(equalTo(LocalDate.of(2024, 3, 31)))); + assertThat(fetched2.getStartDate(), is(equalTo(LocalDate.of(2024, 6, 1)))); + assertThat(fetched2.getEndDate(), is(equalTo(LocalDate.of(2024, 8, 31)))); + } +} + diff --git a/examples/src/test/java/edu/pdx/cs/joy/jdbc/CourseDAOTest.java b/examples/src/test/java/edu/pdx/cs/joy/jdbc/CourseDAOTest.java new file mode 100644 index 000000000..23c4a75a6 --- /dev/null +++ b/examples/src/test/java/edu/pdx/cs/joy/jdbc/CourseDAOTest.java @@ -0,0 +1,202 @@ +package edu.pdx.cs.joy.jdbc; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.List; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class CourseDAOTest { + + private Connection connection; + private CourseDAO courseDAO; + private DepartmentDAO departmentDAO; + + @BeforeEach + public void setUp() throws SQLException { + // Create an in-memory H2 database + connection = H2DatabaseHelper.createInMemoryConnection("test"); + + // Drop tables if they exist from a previous test, then create them + // Note: Must drop courses first due to foreign key constraint + CourseDAOImpl.dropTable(connection); + DepartmentDAOImpl.dropTable(connection); + + // Create departments table first, then courses (due to foreign key) + DepartmentDAOImpl.createTable(connection); + CourseDAOImpl.createTable(connection); + + // Initialize the DAOs with the connection + courseDAO = new CourseDAOImpl(connection); + departmentDAO = new DepartmentDAOImpl(connection); + } + + @AfterEach + public void tearDown() throws SQLException { + if (connection != null && !connection.isClosed()) { + // Drop tables and close the connection + // Note: Must drop courses first due to foreign key constraint + CourseDAOImpl.dropTable(connection); + DepartmentDAOImpl.dropTable(connection); + connection.close(); + } + } + + @Test + public void testPersistAndFetchCourse() throws SQLException { + // Create and persist a department first (required for foreign key) + Department department = new Department("Computer Science"); + departmentDAO.save(department); + + // Get the auto-generated department ID + int csDepartmentId = department.getId(); + + // Create a course + String javaCourseName = "Introduction to Java"; + int credits = 4; + Course course = new Course(javaCourseName, csDepartmentId, credits); + + // Persist the course + courseDAO.save(course); + + // Verify that an ID was auto-generated + int generatedId = course.getId(); + assertThat(generatedId, is(greaterThan(0))); + + // Fetch the course by title + Course fetchedCourse = courseDAO.findByTitle(javaCourseName); + + // Validate the fetched course using Hamcrest assertions + assertThat(fetchedCourse, is(notNullValue())); + assertThat(fetchedCourse.getId(), is(equalTo(generatedId))); + assertThat(fetchedCourse.getTitle(), is(equalTo(javaCourseName))); + assertThat(fetchedCourse.getDepartmentId(), is(equalTo(csDepartmentId))); + assertThat(fetchedCourse.getCredits(), is(equalTo(credits))); + } + + @Test + public void testFetchNonExistentCourse() throws SQLException { + // Try to fetch a course that doesn't exist + Course fetchedCourse = courseDAO.findByTitle("Nonexistent Course"); + + // Validate that null is returned + assertThat(fetchedCourse, is(nullValue())); + } + + @Test + public void testPersistMultipleCourses() throws SQLException { + // Create and persist departments first (required for foreign key) + Department csDepartment = new Department("Computer Science"); + Department mathDepartment = new Department("Mathematics"); + + departmentDAO.save(csDepartment); + departmentDAO.save(mathDepartment); + + // Get the auto-generated department IDs + int csDepartmentId = csDepartment.getId(); + int mathDepartmentId = mathDepartment.getId(); + + // Create multiple courses + String dataStructuresName = "Data Structures"; + String algorithmsName = "Algorithms"; + String calculusName = "Calculus"; + + Course course1 = new Course(dataStructuresName, csDepartmentId, 4); + Course course2 = new Course(algorithmsName, csDepartmentId, 3); + Course course3 = new Course(calculusName, mathDepartmentId, 4); + + // Persist all courses + courseDAO.save(course1); + courseDAO.save(course2); + courseDAO.save(course3); + + // Fetch courses by department + List coursesByDept102 = courseDAO.findByDepartmentId(csDepartmentId); + List coursesByDept103 = courseDAO.findByDepartmentId(mathDepartmentId); + + // Validate using Hamcrest matchers + assertThat(coursesByDept102, hasSize(2)); + assertThat(coursesByDept102, hasItem(hasProperty("title", is(dataStructuresName)))); + assertThat(coursesByDept102, hasItem(hasProperty("title", is(algorithmsName)))); + + assertThat(coursesByDept103, hasSize(1)); + assertThat(coursesByDept103, hasItem(hasProperty("title", is(calculusName)))); + } + + @Test + public void testForeignKeyConstraintPreventsInvalidDepartmentId() { + // Try to create a course with a non-existent department ID + Course course = new Course("Database Systems", 999, 3); + + // Attempting to save should throw an SQLException due to foreign key constraint + SQLException exception = assertThrows(SQLException.class, () -> { + courseDAO.save(course); + }); + assertThat(exception.getMessage(), containsString("Referential integrity")); + } + + @Test + public void testCreditsArePersisted() throws SQLException { + // Create and persist a department first (required for foreign key) + Department department = new Department("Mathematics"); + departmentDAO.save(department); + int deptId = department.getId(); + + // Create courses with different credit values + Course threeCredits = new Course("Statistics", deptId, 3); + Course fourCredits = new Course("Linear Algebra", deptId, 4); + Course fiveCredits = new Course("Abstract Algebra", deptId, 5); + + // Persist all courses + courseDAO.save(threeCredits); + courseDAO.save(fourCredits); + courseDAO.save(fiveCredits); + + // Fetch the courses and verify credits + Course fetchedThree = courseDAO.findByTitle("Statistics"); + Course fetchedFour = courseDAO.findByTitle("Linear Algebra"); + Course fetchedFive = courseDAO.findByTitle("Abstract Algebra"); + + assertThat(fetchedThree.getCredits(), is(equalTo(3))); + assertThat(fetchedFour.getCredits(), is(equalTo(4))); + assertThat(fetchedFive.getCredits(), is(equalTo(5))); + } + + @Test + public void testUpdateCourse() throws SQLException { + // Create and persist a department first (required for foreign key) + Department department = new Department("Computer Science"); + departmentDAO.save(department); + int deptId = department.getId(); + + // Create and persist a course + Course course = new Course("Database Systems", deptId, 3); + courseDAO.save(course); + + int courseId = course.getId(); + assertThat(courseId, is(greaterThan(0))); + + // Update the course + course.setTitle("Advanced Database Systems"); + course.setCredits(4); + courseDAO.update(course); + + // Fetch the course and verify it was updated + Course updatedCourse = courseDAO.findByTitle("Advanced Database Systems"); + assertThat(updatedCourse, is(notNullValue())); + assertThat(updatedCourse.getId(), is(equalTo(courseId))); + assertThat(updatedCourse.getTitle(), is(equalTo("Advanced Database Systems"))); + assertThat(updatedCourse.getCredits(), is(equalTo(4))); + + // Verify the old title doesn't exist anymore + Course oldCourse = courseDAO.findByTitle("Database Systems"); + assertThat(oldCourse, is(nullValue())); + } + +} diff --git a/examples/src/test/java/edu/pdx/cs/joy/jdbc/DepartmentDAOTest.java b/examples/src/test/java/edu/pdx/cs/joy/jdbc/DepartmentDAOTest.java new file mode 100644 index 000000000..abd3b6ad9 --- /dev/null +++ b/examples/src/test/java/edu/pdx/cs/joy/jdbc/DepartmentDAOTest.java @@ -0,0 +1,140 @@ +package edu.pdx.cs.joy.jdbc; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.List; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; + +public class DepartmentDAOTest { + + private Connection connection; + private DepartmentDAO departmentDAO; + + @BeforeEach + public void setUp() throws SQLException { + // Create an in-memory H2 database + connection = H2DatabaseHelper.createInMemoryConnection("test"); + + // Drop the table if it exists from a previous test, then create it + DepartmentDAOImpl.dropTable(connection); + DepartmentDAOImpl.createTable(connection); + + // Initialize the DAO with the connection + departmentDAO = new DepartmentDAOImpl(connection); + } + + @AfterEach + public void tearDown() throws SQLException { + if (connection != null && !connection.isClosed()) { + // Drop the table and close the connection + DepartmentDAOImpl.dropTable(connection); + connection.close(); + } + } + + @Test + public void testPersistAndFetchDepartmentById() throws SQLException { + // Create a department (ID will be auto-generated) + Department department = new Department("Computer Science"); + + // Persist the department + departmentDAO.save(department); + + // Verify that an ID was auto-generated + assertThat(department.getId(), is(greaterThan(0))); + + // Fetch the department by the auto-generated ID + Department fetchedDepartment = departmentDAO.findById(department.getId()); + + // Validate the fetched department using Hamcrest assertions + assertThat(fetchedDepartment, is(notNullValue())); + assertThat(fetchedDepartment.getId(), is(equalTo(department.getId()))); + assertThat(fetchedDepartment.getName(), is(equalTo("Computer Science"))); + } + + @Test + public void testFindDepartmentByName() throws SQLException { + // Create and persist a department (ID will be auto-generated) + Department department = new Department("Mathematics"); + departmentDAO.save(department); + + // Fetch the department by name + Department fetchedDepartment = departmentDAO.findByName("Mathematics"); + + // Validate the fetched department + assertThat(fetchedDepartment, is(notNullValue())); + assertThat(fetchedDepartment.getId(), is(equalTo(department.getId()))); + assertThat(fetchedDepartment.getName(), is(equalTo("Mathematics"))); + } + + @Test + public void testFetchNonExistentDepartmentById() throws SQLException { + // Try to fetch a department that doesn't exist + Department fetchedDepartment = departmentDAO.findById(999); + + // Validate that null is returned + assertThat(fetchedDepartment, is(nullValue())); + } + + @Test + public void testFetchNonExistentDepartmentByName() throws SQLException { + // Try to fetch a department that doesn't exist + Department fetchedDepartment = departmentDAO.findByName("Nonexistent Department"); + + // Validate that null is returned + assertThat(fetchedDepartment, is(nullValue())); + } + + @Test + public void testFindAllDepartments() throws SQLException { + // Create multiple departments (IDs will be auto-generated) + Department dept1 = new Department("Computer Science"); + Department dept2 = new Department("Mathematics"); + Department dept3 = new Department("Physics"); + + // Persist all departments + departmentDAO.save(dept1); + departmentDAO.save(dept2); + departmentDAO.save(dept3); + + // Fetch all departments + List allDepartments = departmentDAO.findAll(); + + // Validate using Hamcrest matchers + assertThat(allDepartments, hasSize(3)); + assertThat(allDepartments, hasItem(hasProperty("name", is("Computer Science")))); + assertThat(allDepartments, hasItem(hasProperty("name", is("Mathematics")))); + assertThat(allDepartments, hasItem(hasProperty("name", is("Physics")))); + assertThat(allDepartments, hasItem(hasProperty("id", is(dept1.getId())))); + assertThat(allDepartments, hasItem(hasProperty("id", is(dept2.getId())))); + assertThat(allDepartments, hasItem(hasProperty("id", is(dept3.getId())))); + } + + @Test + public void testFindAllReturnsEmptyListWhenNoDepartments() throws SQLException { + // Fetch all departments from empty table + List allDepartments = departmentDAO.findAll(); + + // Validate that an empty list is returned + assertThat(allDepartments, is(empty())); + } + + @Test + public void testDepartmentEquality() throws SQLException { + // Create and persist a department (ID will be auto-generated) + Department original = new Department("Engineering"); + departmentDAO.save(original); + + // Fetch the department by its auto-generated ID + Department fetched = departmentDAO.findById(original.getId()); + + // Validate that the objects are equal + assertThat(fetched, is(equalTo(original))); + } +} diff --git a/family/pom.xml b/family/pom.xml index 9590f9ba5..863fb3823 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,17 @@ io.github.davidwhitlock.joy projects - 3.0.3 + 3.0.4-SNAPSHOT + + + io.github.davidwhitlock.joy + examples + 1.4.0-SNAPSHOT + + + com.h2database + h2 + ${h2.version} diff --git a/family/src/main/java/edu/pdx/cs/joy/family/FamilyTreeDAO.java b/family/src/main/java/edu/pdx/cs/joy/family/FamilyTreeDAO.java new file mode 100644 index 000000000..0d55f9b82 --- /dev/null +++ b/family/src/main/java/edu/pdx/cs/joy/family/FamilyTreeDAO.java @@ -0,0 +1,49 @@ +package edu.pdx.cs.joy.family; + +import java.sql.Connection; +import java.sql.SQLException; + +/** + * Data Access Object interface for managing FamilyTree entities in the database. + */ +public interface FamilyTreeDAO { + + /** + * Drops all family tree related tables from the database if they exist. + * + * @param connection the database connection to use + * @throws SQLException if a database error occurs + */ + static void dropTables(Connection connection) throws SQLException { + FamilyTreeDAOImpl.dropTables(connection); + } + + /** + * Creates all family tree related tables in the database. + * + * @param connection the database connection to use + * @throws SQLException if a database error occurs + */ + static void createTables(Connection connection) throws SQLException { + FamilyTreeDAOImpl.createTables(connection); + } + + /** + * Saves a complete family tree to the database. + * This includes all persons and marriages in the tree. + * + * @param familyTree the family tree to save + * @throws SQLException if a database error occurs + */ + void save(FamilyTree familyTree) throws SQLException; + + /** + * Loads a complete family tree from the database. + * This includes all persons and marriages, with relationships properly resolved. + * + * @return the family tree loaded from the database + * @throws SQLException if a database error occurs + */ + FamilyTree load() throws SQLException; +} + diff --git a/family/src/main/java/edu/pdx/cs/joy/family/FamilyTreeDAOImpl.java b/family/src/main/java/edu/pdx/cs/joy/family/FamilyTreeDAOImpl.java new file mode 100644 index 000000000..35de309ee --- /dev/null +++ b/family/src/main/java/edu/pdx/cs/joy/family/FamilyTreeDAOImpl.java @@ -0,0 +1,189 @@ +package edu.pdx.cs.joy.family; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Data Access Object implementation for managing FamilyTree entities in the database. + * Coordinates persistence of Person and Marriage objects. + */ +public class FamilyTreeDAOImpl implements FamilyTreeDAO { + + private final Connection connection; + private final PersonDAO personDAO; + + /** + * Creates a new FamilyTreeDAOImpl with the specified database connection. + * + * @param connection the database connection to use + */ + public FamilyTreeDAOImpl(Connection connection) { + this.connection = connection; + this.personDAO = new PersonDAOImpl(connection); + } + + /** + * Drops all family tree related tables from the database if they exist. + * Note: Must drop marriages first due to foreign key constraints. + * + * @param connection the database connection to use + * @throws SQLException if a database error occurs + */ + public static void dropTables(Connection connection) throws SQLException { + MarriageDAO.dropTable(connection); + PersonDAO.dropTable(connection); + } + + /** + * Creates all family tree related tables in the database. + * Note: Must create persons first due to foreign key constraints. + * + * @param connection the database connection to use + * @throws SQLException if a database error occurs + */ + public static void createTables(Connection connection) throws SQLException { + PersonDAO.createTable(connection); + MarriageDAO.createTable(connection); + } + + /** + * Saves a complete family tree to the database. + * This includes all persons and marriages in the tree. + * + * @param familyTree the family tree to save + * @throws SQLException if a database error occurs + */ + @Override + public void save(FamilyTree familyTree) throws SQLException { + // First, save all persons + for (Person person : familyTree.getPeople()) { + personDAO.save(person); + } + + // Build a person cache for marriage persistence + Map personCache = new HashMap<>(); + for (Person person : familyTree.getPeople()) { + personCache.put(person.getId(), person); + } + + // Then, save all marriages + MarriageDAO marriageDAO = new MarriageDAOImpl(connection, personCache); + for (Person person : familyTree.getPeople()) { + for (Marriage marriage : person.getMarriages()) { + // Only save each marriage once (from husband's perspective to avoid duplicates) + if (person.equals(marriage.getHusband())) { + marriageDAO.save(marriage); + } + } + } + } + + /** + * Loads a complete family tree from the database. + * This includes all persons and marriages, with relationships properly resolved. + * + * @return the family tree loaded from the database + * @throws SQLException if a database error occurs + */ + @Override + public FamilyTree load() throws SQLException { + FamilyTree familyTree = new FamilyTree(); + + // First pass: Load all persons and build cache + Map personCache = new HashMap<>(); + Map parentIdsMap = new HashMap<>(); + + String sql = "SELECT id, gender, first_name, middle_name, last_name, " + + "father_id, mother_id, date_of_birth, date_of_death FROM persons ORDER BY id"; + + try (java.sql.Statement statement = connection.createStatement(); + java.sql.ResultSet resultSet = statement.executeQuery(sql)) { + + while (resultSet.next()) { + int id = resultSet.getInt("id"); + String genderStr = resultSet.getString("gender"); + Person.Gender gender = Person.Gender.valueOf(genderStr); + + Person person = new Person(id, gender); + person.setFirstName(resultSet.getString("first_name")); + person.setMiddleName(resultSet.getString("middle_name")); + person.setLastName(resultSet.getString("last_name")); + + // Store parent IDs for later resolution + int fatherId = resultSet.getInt("father_id"); + boolean fatherIsNull = resultSet.wasNull(); + int motherId = resultSet.getInt("mother_id"); + boolean motherIsNull = resultSet.wasNull(); + + if (!fatherIsNull || !motherIsNull) { + parentIdsMap.put(id, new ParentIds( + fatherIsNull ? Person.UNKNOWN : fatherId, + motherIsNull ? Person.UNKNOWN : motherId + )); + } + + java.sql.Timestamp dob = resultSet.getTimestamp("date_of_birth"); + if (dob != null) { + person.setDateOfBirth(new java.util.Date(dob.getTime())); + } + + java.sql.Timestamp dod = resultSet.getTimestamp("date_of_death"); + if (dod != null) { + person.setDateOfDeath(new java.util.Date(dod.getTime())); + } + + familyTree.addPerson(person); + personCache.put(id, person); + } + } + + // Second pass: Resolve parent relationships + for (Map.Entry entry : parentIdsMap.entrySet()) { + Person person = personCache.get(entry.getKey()); + ParentIds parentIds = entry.getValue(); + + if (parentIds.fatherId != Person.UNKNOWN) { + Person father = personCache.get(parentIds.fatherId); + if (father != null) { + person.setFather(father); + } + } + + if (parentIds.motherId != Person.UNKNOWN) { + Person mother = personCache.get(parentIds.motherId); + if (mother != null) { + person.setMother(mother); + } + } + } + + // Third pass: Load all marriages + MarriageDAO marriageDAO = new MarriageDAOImpl(connection, personCache); + List marriages = marriageDAO.findAll(); + + for (Marriage marriage : marriages) { + // Add marriage to both spouses + marriage.getHusband().addMarriage(marriage); + marriage.getWife().addMarriage(marriage); + } + + return familyTree; + } + + /** + * Helper class to store parent IDs during loading. + */ + private static class ParentIds { + final int fatherId; + final int motherId; + + ParentIds(int fatherId, int motherId) { + this.fatherId = fatherId; + this.motherId = motherId; + } + } +} + diff --git a/family/src/main/java/edu/pdx/cs/joy/family/MarriageDAO.java b/family/src/main/java/edu/pdx/cs/joy/family/MarriageDAO.java new file mode 100644 index 000000000..4e6da9816 --- /dev/null +++ b/family/src/main/java/edu/pdx/cs/joy/family/MarriageDAO.java @@ -0,0 +1,66 @@ +package edu.pdx.cs.joy.family; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.List; + +/** + * Data Access Object interface for managing Marriage entities in the database. + */ +public interface MarriageDAO { + + /** + * Drops the marriages table from the database if it exists. + * + * @param connection the database connection to use + * @throws SQLException if a database error occurs + */ + static void dropTable(Connection connection) throws SQLException { + MarriageDAOImpl.dropTable(connection); + } + + /** + * Creates the marriages table in the database. + * + * @param connection the database connection to use + * @throws SQLException if a database error occurs + */ + static void createTable(Connection connection) throws SQLException { + MarriageDAOImpl.createTable(connection); + } + + /** + * Saves a marriage to the database. + * + * @param marriage the marriage to save + * @throws SQLException if a database error occurs + */ + void save(Marriage marriage) throws SQLException; + + /** + * Finds all marriages for a specific person ID. + * + * @param personId the person ID + * @return a list of marriages involving the person + * @throws SQLException if a database error occurs + */ + List findByPersonId(int personId) throws SQLException; + + /** + * Finds all marriages in the database. + * + * @return a list of all marriages + * @throws SQLException if a database error occurs + */ + List findAll() throws SQLException; + + /** + * Deletes a marriage from the database. + * + * @param husbandId the husband's ID + * @param wifeId the wife's ID + * @throws SQLException if a database error occurs + */ + void delete(int husbandId, int wifeId) throws SQLException; +} + diff --git a/family/src/main/java/edu/pdx/cs/joy/family/MarriageDAOImpl.java b/family/src/main/java/edu/pdx/cs/joy/family/MarriageDAOImpl.java new file mode 100644 index 000000000..cd6c9b3f4 --- /dev/null +++ b/family/src/main/java/edu/pdx/cs/joy/family/MarriageDAOImpl.java @@ -0,0 +1,190 @@ +package edu.pdx.cs.joy.family; + +import java.sql.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * Data Access Object implementation for managing Marriage entities in the database. + */ +public class MarriageDAOImpl implements MarriageDAO { + + private final Connection connection; + private final Map personCache; + + /** + * Creates a new MarriageDAOImpl with the specified database connection. + * + * @param connection the database connection to use + * @param personCache a cache of persons to resolve marriage partners + */ + public MarriageDAOImpl(Connection connection, Map personCache) { + this.connection = connection; + this.personCache = personCache; + } + + /** + * Drops the marriages table from the database if it exists. + * + * @param connection the database connection to use + * @throws SQLException if a database error occurs + */ + public static void dropTable(Connection connection) throws SQLException { + try (Statement statement = connection.createStatement()) { + statement.execute("DROP TABLE IF EXISTS marriages"); + } + } + + /** + * Creates the marriages table in the database. + * + * @param connection the database connection to use + * @throws SQLException if a database error occurs + */ + public static void createTable(Connection connection) throws SQLException { + try (Statement statement = connection.createStatement()) { + statement.execute( + "CREATE TABLE IF NOT EXISTS marriages (" + + " husband_id INTEGER NOT NULL," + + " wife_id INTEGER NOT NULL," + + " marriage_date TIMESTAMP," + + " location VARCHAR(255)," + + " PRIMARY KEY (husband_id, wife_id)," + + " FOREIGN KEY (husband_id) REFERENCES persons(id)," + + " FOREIGN KEY (wife_id) REFERENCES persons(id)" + + ")" + ); + } + } + + /** + * Saves a marriage to the database. + * + * @param marriage the marriage to save + * @throws SQLException if a database error occurs + */ + @Override + public void save(Marriage marriage) throws SQLException { + String sql = "INSERT INTO marriages (husband_id, wife_id, marriage_date, location) " + + "VALUES (?, ?, ?, ?)"; + + try (PreparedStatement statement = connection.prepareStatement(sql)) { + statement.setInt(1, marriage.getHusband().getId()); + statement.setInt(2, marriage.getWife().getId()); + + if (marriage.getDate() == null) { + statement.setNull(3, Types.TIMESTAMP); + } else { + statement.setTimestamp(3, new Timestamp(marriage.getDate().getTime())); + } + + statement.setString(4, marriage.getLocation()); + + statement.executeUpdate(); + } + } + + /** + * Finds all marriages for a specific person ID. + * + * @param personId the person ID + * @return a list of marriages involving the person + * @throws SQLException if a database error occurs + */ + @Override + public List findByPersonId(int personId) throws SQLException { + List marriages = new ArrayList<>(); + String sql = "SELECT husband_id, wife_id, marriage_date, location " + + "FROM marriages WHERE husband_id = ? OR wife_id = ?"; + + try (PreparedStatement statement = connection.prepareStatement(sql)) { + statement.setInt(1, personId); + statement.setInt(2, personId); + + try (ResultSet resultSet = statement.executeQuery()) { + while (resultSet.next()) { + marriages.add(extractMarriageFromResultSet(resultSet)); + } + } + } + + return marriages; + } + + /** + * Finds all marriages in the database. + * + * @return a list of all marriages + * @throws SQLException if a database error occurs + */ + @Override + public List findAll() throws SQLException { + List marriages = new ArrayList<>(); + String sql = "SELECT husband_id, wife_id, marriage_date, location FROM marriages"; + + try (Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery(sql)) { + while (resultSet.next()) { + marriages.add(extractMarriageFromResultSet(resultSet)); + } + } + + return marriages; + } + + /** + * Deletes a marriage from the database. + * + * @param husbandId the husband's ID + * @param wifeId the wife's ID + * @throws SQLException if a database error occurs + */ + @Override + public void delete(int husbandId, int wifeId) throws SQLException { + String sql = "DELETE FROM marriages WHERE husband_id = ? AND wife_id = ?"; + + try (PreparedStatement statement = connection.prepareStatement(sql)) { + statement.setInt(1, husbandId); + statement.setInt(2, wifeId); + int rowsAffected = statement.executeUpdate(); + + if (rowsAffected == 0) { + throw new SQLException("Delete failed, no marriage found for husband ID: " + + husbandId + " and wife ID: " + wifeId); + } + } + } + + /** + * Extracts a Marriage object from the current row of a ResultSet. + * + * @param resultSet the result set positioned at a marriage row + * @return a Marriage object with data from the result set + * @throws SQLException if a database error occurs + */ + private Marriage extractMarriageFromResultSet(ResultSet resultSet) throws SQLException { + int husbandId = resultSet.getInt("husband_id"); + int wifeId = resultSet.getInt("wife_id"); + + Person husband = personCache.get(husbandId); + Person wife = personCache.get(wifeId); + + if (husband == null || wife == null) { + throw new SQLException("Cannot create marriage: Person not found in cache. " + + "Husband ID: " + husbandId + ", Wife ID: " + wifeId); + } + + Marriage marriage = new Marriage(husband, wife); + + Timestamp marriageDate = resultSet.getTimestamp("marriage_date"); + if (marriageDate != null) { + marriage.setDate(new java.util.Date(marriageDate.getTime())); + } + + marriage.setLocation(resultSet.getString("location")); + + return marriage; + } +} + diff --git a/family/src/main/java/edu/pdx/cs/joy/family/PersonDAO.java b/family/src/main/java/edu/pdx/cs/joy/family/PersonDAO.java new file mode 100644 index 000000000..53fb8e536 --- /dev/null +++ b/family/src/main/java/edu/pdx/cs/joy/family/PersonDAO.java @@ -0,0 +1,73 @@ +package edu.pdx.cs.joy.family; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.List; + +/** + * Data Access Object interface for managing Person entities in the database. + */ +public interface PersonDAO { + + /** + * Drops the persons table from the database if it exists. + * + * @param connection the database connection to use + * @throws SQLException if a database error occurs + */ + static void dropTable(Connection connection) throws SQLException { + PersonDAOImpl.dropTable(connection); + } + + /** + * Creates the persons table in the database. + * + * @param connection the database connection to use + * @throws SQLException if a database error occurs + */ + static void createTable(Connection connection) throws SQLException { + PersonDAOImpl.createTable(connection); + } + + /** + * Saves a person to the database. + * + * @param person the person to save + * @throws SQLException if a database error occurs + */ + void save(Person person) throws SQLException; + + /** + * Finds a person by their ID. + * + * @param id the ID to search for + * @return the person with the given ID, or null if not found + * @throws SQLException if a database error occurs + */ + Person findById(int id) throws SQLException; + + /** + * Finds all persons in the database. + * + * @return a list of all persons + * @throws SQLException if a database error occurs + */ + List findAll() throws SQLException; + + /** + * Updates an existing person in the database. + * + * @param person the person to update + * @throws SQLException if a database error occurs + */ + void update(Person person) throws SQLException; + + /** + * Deletes a person from the database by ID. + * + * @param id the ID of the person to delete + * @throws SQLException if a database error occurs + */ + void delete(int id) throws SQLException; +} + diff --git a/family/src/main/java/edu/pdx/cs/joy/family/PersonDAOImpl.java b/family/src/main/java/edu/pdx/cs/joy/family/PersonDAOImpl.java new file mode 100644 index 000000000..58e3dcb77 --- /dev/null +++ b/family/src/main/java/edu/pdx/cs/joy/family/PersonDAOImpl.java @@ -0,0 +1,290 @@ +package edu.pdx.cs.joy.family; + +import java.sql.*; +import java.util.ArrayList; +import java.util.List; + +/** + * Data Access Object implementation for managing Person entities in the database. + */ +public class PersonDAOImpl implements PersonDAO { + + private final Connection connection; + + /** + * Creates a new PersonDAOImpl with the specified database connection. + * + * @param connection the database connection to use + */ + public PersonDAOImpl(Connection connection) { + this.connection = connection; + } + + /** + * Drops the persons table from the database if it exists. + * + * @param connection the database connection to use + * @throws SQLException if a database error occurs + */ + public static void dropTable(Connection connection) throws SQLException { + try (Statement statement = connection.createStatement()) { + statement.execute("DROP TABLE IF EXISTS persons"); + } + } + + /** + * Creates the persons table in the database. + * + * @param connection the database connection to use + * @throws SQLException if a database error occurs + */ + public static void createTable(Connection connection) throws SQLException { + try (Statement statement = connection.createStatement()) { + statement.execute( + "CREATE TABLE IF NOT EXISTS persons (" + + " id INTEGER PRIMARY KEY," + + " gender VARCHAR(10) NOT NULL," + + " first_name VARCHAR(255)," + + " middle_name VARCHAR(255)," + + " last_name VARCHAR(255)," + + " father_id INTEGER," + + " mother_id INTEGER," + + " date_of_birth TIMESTAMP," + + " date_of_death TIMESTAMP," + + " FOREIGN KEY (father_id) REFERENCES persons(id)," + + " FOREIGN KEY (mother_id) REFERENCES persons(id)" + + ")" + ); + } + } + + /** + * Saves a person to the database. + * + * @param person the person to save + * @throws SQLException if a database error occurs + */ + @Override + public void save(Person person) throws SQLException { + String sql = "INSERT INTO persons (id, gender, first_name, middle_name, last_name, " + + "father_id, mother_id, date_of_birth, date_of_death) " + + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"; + + try (PreparedStatement statement = connection.prepareStatement(sql)) { + statement.setInt(1, person.getId()); + statement.setString(2, person.getGender().name()); + statement.setString(3, person.getFirstName()); + statement.setString(4, person.getMiddleName()); + statement.setString(5, person.getLastName()); + + if (person.getFatherId() == Person.UNKNOWN) { + statement.setNull(6, Types.INTEGER); + } else { + statement.setInt(6, person.getFatherId()); + } + + if (person.getMotherId() == Person.UNKNOWN) { + statement.setNull(7, Types.INTEGER); + } else { + statement.setInt(7, person.getMotherId()); + } + + if (person.getDateOfBirth() == null) { + statement.setNull(8, Types.TIMESTAMP); + } else { + statement.setTimestamp(8, new Timestamp(person.getDateOfBirth().getTime())); + } + + if (person.getDateOfDeath() == null) { + statement.setNull(9, Types.TIMESTAMP); + } else { + statement.setTimestamp(9, new Timestamp(person.getDateOfDeath().getTime())); + } + + statement.executeUpdate(); + } + } + + /** + * Finds a person by their ID. + * + * @param id the ID to search for + * @return the person with the given ID, or null if not found + * @throws SQLException if a database error occurs + */ + @Override + public Person findById(int id) throws SQLException { + String sql = "SELECT id, gender, first_name, middle_name, last_name, " + + "father_id, mother_id, date_of_birth, date_of_death " + + "FROM persons WHERE id = ?"; + + try (PreparedStatement statement = connection.prepareStatement(sql)) { + statement.setInt(1, id); + + try (ResultSet resultSet = statement.executeQuery()) { + if (resultSet.next()) { + return extractPersonFromResultSet(resultSet); + } + } + } + + return null; + } + + /** + * Finds all persons in the database. + * + * @return a list of all persons + * @throws SQLException if a database error occurs + */ + @Override + public List findAll() throws SQLException { + List persons = new ArrayList<>(); + String sql = "SELECT id, gender, first_name, middle_name, last_name, " + + "father_id, mother_id, date_of_birth, date_of_death " + + "FROM persons ORDER BY id"; + + try (Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery(sql)) { + while (resultSet.next()) { + persons.add(extractPersonFromResultSet(resultSet)); + } + } + + return persons; + } + + /** + * Updates an existing person in the database. + * + * @param person the person to update + * @throws SQLException if a database error occurs + */ + @Override + public void update(Person person) throws SQLException { + String sql = "UPDATE persons SET gender = ?, first_name = ?, middle_name = ?, " + + "last_name = ?, father_id = ?, mother_id = ?, " + + "date_of_birth = ?, date_of_death = ? WHERE id = ?"; + + try (PreparedStatement statement = connection.prepareStatement(sql)) { + statement.setString(1, person.getGender().name()); + statement.setString(2, person.getFirstName()); + statement.setString(3, person.getMiddleName()); + statement.setString(4, person.getLastName()); + + if (person.getFatherId() == Person.UNKNOWN) { + statement.setNull(5, Types.INTEGER); + } else { + statement.setInt(5, person.getFatherId()); + } + + if (person.getMotherId() == Person.UNKNOWN) { + statement.setNull(6, Types.INTEGER); + } else { + statement.setInt(6, person.getMotherId()); + } + + if (person.getDateOfBirth() == null) { + statement.setNull(7, Types.TIMESTAMP); + } else { + statement.setTimestamp(7, new Timestamp(person.getDateOfBirth().getTime())); + } + + if (person.getDateOfDeath() == null) { + statement.setNull(8, Types.TIMESTAMP); + } else { + statement.setTimestamp(8, new Timestamp(person.getDateOfDeath().getTime())); + } + + statement.setInt(9, person.getId()); + + int rowsAffected = statement.executeUpdate(); + if (rowsAffected == 0) { + throw new SQLException("Update failed, no person found with ID: " + person.getId()); + } + } + } + + /** + * Deletes a person from the database by ID. + * + * @param id the ID of the person to delete + * @throws SQLException if a database error occurs + */ + @Override + public void delete(int id) throws SQLException { + String sql = "DELETE FROM persons WHERE id = ?"; + + try (PreparedStatement statement = connection.prepareStatement(sql)) { + statement.setInt(1, id); + int rowsAffected = statement.executeUpdate(); + + if (rowsAffected == 0) { + throw new SQLException("Delete failed, no person found with ID: " + id); + } + } + } + + /** + * Extracts a Person object from the current row of a ResultSet. + * Note: This creates a Person without resolving parent references. + * Parent IDs are stored in the database but parent Person objects + * must be resolved by the caller. + * + * @param resultSet the result set positioned at a person row + * @return a Person object with data from the result set + * @throws SQLException if a database error occurs + */ + private Person extractPersonFromResultSet(ResultSet resultSet) throws SQLException { + int id = resultSet.getInt("id"); + String genderStr = resultSet.getString("gender"); + Person.Gender gender = Person.Gender.valueOf(genderStr); + + Person person = new Person(id, gender); + person.setFirstName(resultSet.getString("first_name")); + person.setMiddleName(resultSet.getString("middle_name")); + person.setLastName(resultSet.getString("last_name")); + + // Note: Parent relationships will be resolved by FamilyTreeDAO + // We cannot call package-protected setFatherId/setMotherId from here + + Timestamp dob = resultSet.getTimestamp("date_of_birth"); + if (dob != null) { + person.setDateOfBirth(new java.util.Date(dob.getTime())); + } + + Timestamp dod = resultSet.getTimestamp("date_of_death"); + if (dod != null) { + person.setDateOfDeath(new java.util.Date(dod.getTime())); + } + + return person; + } + + /** + * Helper method to get father ID from result set. + * Package-protected to allow FamilyTreeDAO to resolve relationships. + * + * @param resultSet the result set + * @return the father ID or Person.UNKNOWN if null + * @throws SQLException if a database error occurs + */ + int getFatherIdFromResultSet(ResultSet resultSet) throws SQLException { + int fatherId = resultSet.getInt("father_id"); + return resultSet.wasNull() ? Person.UNKNOWN : fatherId; + } + + /** + * Helper method to get mother ID from result set. + * Package-protected to allow FamilyTreeDAO to resolve relationships. + * + * @param resultSet the result set + * @return the mother ID or Person.UNKNOWN if null + * @throws SQLException if a database error occurs + */ + int getMotherIdFromResultSet(ResultSet resultSet) throws SQLException { + int motherId = resultSet.getInt("mother_id"); + return resultSet.wasNull() ? Person.UNKNOWN : motherId; + } +} + diff --git a/family/src/test/java/edu/pdx/cs/joy/family/FamilyTreeDAOTest.java b/family/src/test/java/edu/pdx/cs/joy/family/FamilyTreeDAOTest.java new file mode 100644 index 000000000..94218e6ff --- /dev/null +++ b/family/src/test/java/edu/pdx/cs/joy/family/FamilyTreeDAOTest.java @@ -0,0 +1,229 @@ +package edu.pdx.cs.joy.family; + +import edu.pdx.cs.joy.jdbc.H2DatabaseHelper; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.Calendar; +import java.util.Date; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; + +/** + * Unit test for FamilyTree DAO classes. + * Tests persistence of Person, Marriage, and FamilyTree objects to an H2 database. + */ +public class FamilyTreeDAOTest { + + private Connection connection; + private FamilyTreeDAO familyTreeDAO; + + @BeforeEach + public void setUp() throws SQLException { + // Create an in-memory H2 database + connection = H2DatabaseHelper.createInMemoryConnection("familyTreeTest"); + + // Drop and create tables + FamilyTreeDAO.dropTables(connection); + FamilyTreeDAO.createTables(connection); + + // Initialize the DAO + familyTreeDAO = new FamilyTreeDAOImpl(connection); + } + + @AfterEach + public void tearDown() throws SQLException { + if (connection != null && !connection.isClosed()) { + FamilyTreeDAO.dropTables(connection); + connection.close(); + } + } + + @Test + public void testPersistAndLoadSimpleFamilyTree() throws SQLException { + // Create a simple family tree + FamilyTree familyTree = new FamilyTree(); + + Person father = new Person(1, Person.MALE); + father.setFirstName("John"); + father.setLastName("Doe"); + father.setDateOfBirth(createDate(1970, 1, 15)); + + Person mother = new Person(2, Person.FEMALE); + mother.setFirstName("Jane"); + mother.setLastName("Smith"); + mother.setDateOfBirth(createDate(1972, 3, 20)); + + Person child = new Person(3, Person.MALE); + child.setFirstName("Jack"); + child.setLastName("Doe"); + child.setDateOfBirth(createDate(2000, 6, 10)); + child.setFather(father); + child.setMother(mother); + + familyTree.addPerson(father); + familyTree.addPerson(mother); + familyTree.addPerson(child); + + // Add marriage + Marriage marriage = new Marriage(father, mother); + marriage.setDate(createDate(1998, 5, 15)); + marriage.setLocation("Portland, OR"); + father.addMarriage(marriage); + mother.addMarriage(marriage); + + // Save to database + familyTreeDAO.save(familyTree); + + // Load from database + FamilyTree loadedTree = familyTreeDAO.load(); + + // Validate + assertThat(loadedTree.getPeople(), hasSize(3)); + assertThat(loadedTree.containsPerson(1), is(true)); + assertThat(loadedTree.containsPerson(2), is(true)); + assertThat(loadedTree.containsPerson(3), is(true)); + + // Validate father + Person loadedFather = loadedTree.getPerson(1); + assertThat(loadedFather.getFirstName(), is(equalTo("John"))); + assertThat(loadedFather.getLastName(), is(equalTo("Doe"))); + assertThat(loadedFather.getGender(), is(equalTo(Person.MALE))); + assertThat(loadedFather.getMarriages(), hasSize(1)); + + // Validate mother + Person loadedMother = loadedTree.getPerson(2); + assertThat(loadedMother.getFirstName(), is(equalTo("Jane"))); + assertThat(loadedMother.getLastName(), is(equalTo("Smith"))); + assertThat(loadedMother.getGender(), is(equalTo(Person.FEMALE))); + + // Validate child + Person loadedChild = loadedTree.getPerson(3); + assertThat(loadedChild.getFirstName(), is(equalTo("Jack"))); + assertThat(loadedChild.getFather(), is(notNullValue())); + assertThat(loadedChild.getFather().getId(), is(equalTo(1))); + assertThat(loadedChild.getMother(), is(notNullValue())); + assertThat(loadedChild.getMother().getId(), is(equalTo(2))); + + // Validate marriage + Marriage loadedMarriage = loadedFather.getMarriages().iterator().next(); + assertThat(loadedMarriage.getHusband().getId(), is(equalTo(1))); + assertThat(loadedMarriage.getWife().getId(), is(equalTo(2))); + assertThat(loadedMarriage.getLocation(), is(equalTo("Portland, OR"))); + } + + @Test + public void testPersistMultipleGenerations() throws SQLException { + FamilyTree familyTree = new FamilyTree(); + + // Grandparents + Person grandfather = new Person(1, Person.MALE); + grandfather.setFirstName("William"); + grandfather.setLastName("Doe"); + + Person grandmother = new Person(2, Person.FEMALE); + grandmother.setFirstName("Mary"); + grandmother.setLastName("Johnson"); + + // Parents + Person father = new Person(3, Person.MALE); + father.setFirstName("John"); + father.setLastName("Doe"); + father.setFather(grandfather); + father.setMother(grandmother); + + Person mother = new Person(4, Person.FEMALE); + mother.setFirstName("Jane"); + mother.setLastName("Smith"); + + // Child + Person child = new Person(5, Person.FEMALE); + child.setFirstName("Emily"); + child.setLastName("Doe"); + child.setFather(father); + child.setMother(mother); + + familyTree.addPerson(grandfather); + familyTree.addPerson(grandmother); + familyTree.addPerson(father); + familyTree.addPerson(mother); + familyTree.addPerson(child); + + // Save and load + familyTreeDAO.save(familyTree); + FamilyTree loadedTree = familyTreeDAO.load(); + + // Validate multi-generation relationships + assertThat(loadedTree.getPeople(), hasSize(5)); + + Person loadedChild = loadedTree.getPerson(5); + assertThat(loadedChild.getFather(), is(notNullValue())); + assertThat(loadedChild.getFather().getId(), is(equalTo(3))); + + Person loadedFather = loadedChild.getFather(); + assertThat(loadedFather.getFather(), is(notNullValue())); + assertThat(loadedFather.getFather().getId(), is(equalTo(1))); + assertThat(loadedFather.getMother(), is(notNullValue())); + assertThat(loadedFather.getMother().getId(), is(equalTo(2))); + } + + @Test + public void testPersistPersonWithDates() throws SQLException { + FamilyTree familyTree = new FamilyTree(); + + Person person = new Person(1, Person.MALE); + person.setFirstName("George"); + person.setLastName("Washington"); + person.setDateOfBirth(createDate(1732, 2, 22)); + person.setDateOfDeath(createDate(1799, 12, 14)); + + familyTree.addPerson(person); + + familyTreeDAO.save(familyTree); + FamilyTree loadedTree = familyTreeDAO.load(); + + Person loadedPerson = loadedTree.getPerson(1); + assertThat(loadedPerson.getDateOfBirth(), is(notNullValue())); + assertThat(loadedPerson.getDateOfDeath(), is(notNullValue())); + } + + @Test + public void testLoadEmptyFamilyTree() throws SQLException { + FamilyTree loadedTree = familyTreeDAO.load(); + assertThat(loadedTree.getPeople(), is(empty())); + } + + @Test + public void testPersistPersonWithoutParents() throws SQLException { + FamilyTree familyTree = new FamilyTree(); + + Person person = new Person(1, Person.FEMALE); + person.setFirstName("Alice"); + person.setLastName("Unknown"); + + familyTree.addPerson(person); + + familyTreeDAO.save(familyTree); + FamilyTree loadedTree = familyTreeDAO.load(); + + Person loadedPerson = loadedTree.getPerson(1); + assertThat(loadedPerson, is(notNullValue())); + assertThat(loadedPerson.getFather(), is(nullValue())); + assertThat(loadedPerson.getMother(), is(nullValue())); + } + + /** + * Helper method to create a Date object. + */ + private Date createDate(int year, int month, int day) { + Calendar cal = Calendar.getInstance(); + cal.set(year, month - 1, day, 0, 0, 0); + cal.set(Calendar.MILLISECOND, 0); + return cal.getTime(); + } +} + diff --git a/family/src/main/java/edu/pdx/cs/joy/family/davesFamily.xml b/family/src/test/resources/edu/pdx/cs/joy/family/davesFamily.xml similarity index 100% rename from family/src/main/java/edu/pdx/cs/joy/family/davesFamily.xml rename to family/src/test/resources/edu/pdx/cs/joy/family/davesFamily.xml diff --git a/grader/pom.xml b/grader/pom.xml index d82dc0da5..12420c0c8 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 @@ -10,26 +10,41 @@ ${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 jakarta.mail - 2.0.1 + 2.0.2 io.github.davidwhitlock.joy projects - 3.0.3 + 3.0.4-SNAPSHOT com.opencsv opencsv - 5.9 + 5.12.0 ch.qos.logback logback-classic - 1.5.12 + 1.5.23 com.google.inject 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..9c4648a8f --- /dev/null +++ b/grader/src/main/java/edu/pdx/cs/joy/grader/FindUngradedSubmissions.java @@ -0,0 +1,316 @@ +package edu.pdx.cs.joy.grader; + +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.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 { + private final SubmissionDetailsProvider submissionDetailsProvider; + private final TestOutputPathProvider testOutputProvider; + private final TestOutputDetailsProvider testOutputDetailsProvider; + + @VisibleForTesting + FindUngradedSubmissions(SubmissionDetailsProvider submissionDetailsProvider, TestOutputPathProvider testOutputProvider, TestOutputDetailsProvider testOutputDetailsProvider) { + this.submissionDetailsProvider = submissionDetailsProvider; + this.testOutputProvider = testOutputProvider; + this.testOutputDetailsProvider = testOutputDetailsProvider; + } + + public FindUngradedSubmissions() { + this(new SubmissionDetailsProviderFromZipFile(), new TestOutputProviderInParentDirectory(), new TestOutputDetailsProviderFromTestOutputFile()); + } + + @VisibleForTesting + SubmissionAnalysis analyzeSubmission(Path submissionPath) { + SubmissionDetails submission = this.submissionDetailsProvider.getSubmissionDetails(submissionPath); + Path submissionDirectory = submissionPath.getParent(); + 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 (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, reason); + } + + private static boolean submittedAfterTesting(SubmissionDetails submission, TestOutputDetails testOutput) { + LocalDateTime submissionTime = submission.submissionTime(); + LocalDateTime testedSubmissionTime = testOutput.testedSubmissionTime(); + return submissionTime.isAfter(testedSubmissionTime.plusMinutes(1L)); + } + + @VisibleForTesting + record SubmissionDetails(String studentId, LocalDateTime submissionTime) { + + } + + public static void main(String[] args) { + if (args.length == 0) { + System.err.println("Usage: java FindUngradedSubmissions -includeReason submissionZipOrDirectory+"); + System.exit(1); + } + + 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<>(); + List needsToBeGraded = new ArrayList<>(); + analyses.forEach(analysis -> { + if (analysis.needsToBeTested()) { + needsToBeTested.add(analysis); + + } else if (analysis.needsToBeGraded()) { + needsToBeGraded.add(analysis); + } + }); + + printOutAnalyses(needsToBeTested, "tested", includeReason); + printOutAnalyses(needsToBeGraded, "graded", includeReason); + } + + 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.print(" " + analysis.submission); + if (includeReason) { + System.out.print(" " + analysis.reason); + } + System.out.println(); + }); + } + + private static Stream findSubmissionsIn(List fileNames) { + return fileNames.stream() + .map(Path::of) + .filter(Files::exists) + .flatMap(FindUngradedSubmissions::findSubmissionsIn); + } + + private static Stream findSubmissionsIn(Path path) { + if (Files.isDirectory(path)) { + 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 Stream.of(path); + } else { + return Stream.empty(); + } + } + + private static boolean isZipFile(Path p) { + return Files.isRegularFile(p) && p.getFileName().toString().endsWith(".zip"); + } + + interface SubmissionDetailsProvider { + SubmissionDetails getSubmissionDetails(Path submission); + } + + interface TestOutputPathProvider { + Path getTestOutput(Path submissionDirectory, String studentId); + } + + interface TestOutputDetailsProvider { + TestOutputDetails getTestOutputDetails(Path testOutput); + } + + @VisibleForTesting + record TestOutputDetails(LocalDateTime testedSubmissionTime, boolean hasGrade) { + } + + @VisibleForTesting + record SubmissionAnalysis (Path submission, boolean needsToBeTested, boolean needsToBeGraded, String reason) { + + } + + 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) { + return submissionDirectory.resolve(studentId + ".out"); + } + } + + @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 { + 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); + } + } + + 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) { + 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; + private int lineCount; + + @Override + public void accept(String line) { + this.lineCount++; + + LocalDateTime submissionTime = parseSubmissionTime(line); + if (submissionTime != null) { + this.testedSubmissionTime = submissionTime; + } + + Double grade = parseGrade(line); + if (grade != null) { + boolean testOutputHasNotesForStudent = lineCount > 7; + this.hasGrade = testOutputHasNotesForStudent || !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/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(); 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/main/java/edu/pdx/cs/joy/grader/SummaryReport.java b/grader/src/main/java/edu/pdx/cs/joy/grader/SummaryReport.java index 2ae79cd94..cee23fb11 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) { @@ -223,7 +233,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 +293,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); } 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); 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 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..283dba292 --- /dev/null +++ b/grader/src/test/java/edu/pdx/cs/joy/grader/FindUngradedSubmissionsTest.java @@ -0,0 +1,243 @@ +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; +import java.nio.file.Files; +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; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +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); + + 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; + } + + @Test + void submissionWithNoTestOutputNeedsToBeTested() { + FindUngradedSubmissions.SubmissionDetailsProvider submissionDetailsProvider = mock(FindUngradedSubmissions.SubmissionDetailsProvider.class); + + String studentId = "student123"; + 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(any(Path.class), eq(studentId))).thenReturn(testOutput); + + FindUngradedSubmissions finder = new FindUngradedSubmissions(submissionDetailsProvider, testOutputProvider, mock(FindUngradedSubmissions.TestOutputDetailsProvider.class)); + FindUngradedSubmissions.SubmissionAnalysis analysis = finder.analyzeSubmission(submission); + assertThat(analysis.needsToBeTested(), equalTo(true)); + assertThat(analysis.needsToBeGraded(), equalTo(true)); + assertThat(analysis.submission(), equalTo(submission)); + } + + @Test + void submissionWithTestOutputOlderThanSubmissionNeedsToBeTested() { + 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.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); + assertThat(analysis.needsToBeTested(), equalTo(true)); + 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); + + 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 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); + FindUngradedSubmissions.SubmissionAnalysis analysis = finder.analyzeSubmission(submission); + assertThat(analysis.needsToBeTested(), equalTo(false)); + assertThat(analysis.needsToBeGraded(), equalTo(true)); + } + + @Test + void submissionWithGradeIsGraded() { + 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 gradedTime = submissionTime.plusDays(1); + when(testOutputDetailsProvider.getTestOutputDetails(testOutput)).thenReturn(new FindUngradedSubmissions.TestOutputDetails(gradedTime, true)); + + FindUngradedSubmissions finder = new FindUngradedSubmissions(submissionDetailsProvider, testOutputProvider, testOutputDetailsProvider); + FindUngradedSubmissions.SubmissionAnalysis analysis = finder.analyzeSubmission(submission); + 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 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"; + 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)); + } + + @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)); + + } +} 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); 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/pom.xml b/pom.xml index 861397222..aa22459cc 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 @@ -57,43 +57,44 @@ 21 21 - 33.4.8-jre + 33.5.0-jre 7.0.0 + 2.4.240 6.1.0 UTF-8 UTF-8 - 7.13.0 + 7.20.0 2.0.4 3.5.3 5.12.2 3.0 - 5.17.0 - 3.5.3 + 5.21.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.3.1 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 0 - 1.4.0 + 1.5.0-SNAPSHOT @@ -252,6 +253,9 @@ org.apache.maven.plugins maven-javadoc-plugin ${maven-javadoc-plugin.version} + + true + attach-javadocs @@ -536,6 +540,7 @@ maven-javadoc-plugin ${maven-javadoc-plugin.version} + true true true overview.html diff --git a/projects-parent/archetypes-parent/airline-archetype/pom.xml b/projects-parent/archetypes-parent/airline-archetype/pom.xml index 81bf446b1..6815f961b 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 @@ -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/.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-archetype/src/main/resources/archetype-resources/pom.xml b/projects-parent/archetypes-parent/airline-archetype/src/main/resources/archetype-resources/pom.xml index cba2f3f53..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 @@ -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 @@ -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/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/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..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 cantParseInvalidXmlFile() throws ParserConfigurationException { - AirlineXmlHelper helper = new AirlineXmlHelper(); + void throwsExceptionWhenParsingInvalidXmlFile() throws ParserConfigurationException { + 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 cantParseInvalidXmlFile() throws ParserConfigurationException { 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/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/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/.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/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..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 @@ -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.4.0-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/.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-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-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/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..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 cantParseInvalidXmlFile() throws ParserConfigurationException { - AppointmentBookXmlHelper helper = new AppointmentBookXmlHelper(); + void throwsExceptionWhenParsingInvalidXmlFile() throws ParserConfigurationException { + 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 cantParseInvalidXmlFile() throws ParserConfigurationException { 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/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/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/.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/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..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 @@ -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.4.0-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..79795ecaf 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 @@ -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/.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/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..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 @@ -4,7 +4,7 @@ joy io.github.davidwhitlock.joy - 1.2.3 + 1.2.4-SNAPSHOT ${artifactId} ${groupId} @@ -50,7 +50,7 @@ io.github.davidwhitlock.joy.com.sandwich koans-lib - 1.2.2 + 1.2.3-SNAPSHOT @@ -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/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/.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/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..dd9a1a884 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.3.0-SNAPSHOT maven-archetype phonebill-archetype 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-archetype/src/main/resources/archetype-resources/pom.xml b/projects-parent/archetypes-parent/phonebill-archetype/src/main/resources/archetype-resources/pom.xml index 6318b3057..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,9 +1,9 @@ - + joy io.github.davidwhitlock.joy - 1.2.3 + 1.2.4-SNAPSHOT 4.0.0 ${groupId} @@ -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 @@ -38,12 +43,17 @@ io.github.davidwhitlock.joy projects - 3.0.3 + 3.0.4-SNAPSHOT + + + io.github.davidwhitlock.joy + examples + 1.4.0-SNAPSHOT io.github.davidwhitlock.joy projects - 3.0.3 + 3.0.4-SNAPSHOT tests test 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/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..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 @@ -11,13 +11,13 @@ public class Project1 { @VisibleForTesting - static boolean isValidPhoneNumber(String phoneNumber) { + static boolean isValidDateAndTime(String dateAndTime) { return true; } 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/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/PhoneBillDAOTest.java b/projects-parent/archetypes-parent/phonebill-archetype/src/main/resources/archetype-resources/src/test/java/PhoneBillDAOTest.java new file mode 100644 index 000000000..a1fdd15c4 --- /dev/null +++ b/projects-parent/archetypes-parent/phonebill-archetype/src/main/resources/archetype-resources/src/test/java/PhoneBillDAOTest.java @@ -0,0 +1,68 @@ +#set( $symbol_pound = '#' ) +#set( $symbol_dollar = '$' ) +#set( $symbol_escape = '\' ) +package ${package}; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.sql.*; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; + +/** + * A simple example unit test that demonstrates persisting a PhoneBill + * to an H2 in-memory database using JDBC. + * + * This is a starting point for students to understand how to use + * Data Access Objects (DAOs) to persist domain objects to a database. + */ +public class PhoneBillDAOTest { + + private Connection connection; + private PhoneBillDAO dao; + + @BeforeEach + public void setUp() throws SQLException { + // Create an in-memory H2 database + connection = DriverManager.getConnection("jdbc:h2:mem:${artifactId}_test"); + + // Create the phone_bills table + PhoneBillDAO.createTable(connection); + + // Create the DAO + dao = new PhoneBillDAO(connection); + } + + @AfterEach + public void tearDown() throws SQLException { + if (connection != null && !connection.isClosed()) { + connection.close(); + } + } + + @Test + public void canPersistAndFetchPhoneBillByCustomerName() throws SQLException { + String customerName = "Jane Doe"; + PhoneBill bill = new PhoneBill(customerName); + + // Persist the phone bill using the DAO + dao.save(bill); + + // Fetch the phone bill by customer name + PhoneBill fetchedBill = dao.findByCustomer(customerName); + + // Validate that the fetched bill matches the original + assertThat(fetchedBill, is(notNullValue())); + assertThat(fetchedBill.getCustomer(), is(equalTo(customerName))); + } + + @Test + public void returnsNullWhenPhoneBillNotFound() throws SQLException { + PhoneBill fetchedBill = dao.findByCustomer("Non-existent Customer"); + assertThat(fetchedBill, is(nullValue())); + } +} + diff --git a/projects-parent/archetypes-parent/phonebill-archetype/src/main/resources/archetype-resources/src/test/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/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/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/.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/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..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 @@ -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.4.0-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..e90ef36a3 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,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 - 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/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/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..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 @@ -4,7 +4,7 @@ joy io.github.davidwhitlock.joy - 1.2.3 + 1.2.4-SNAPSHOT ${groupId} ${artifactId} @@ -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 @@ -102,12 +102,12 @@ io.github.davidwhitlock.joy examples - 1.3.4 + 1.4.0-SNAPSHOT io.github.davidwhitlock.joy projects - 3.0.3 + 3.0.4-SNAPSHOT tests test 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/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-web/pom.xml b/projects-parent/originals-parent/airline-web/pom.xml index eba30bacc..c558fd073 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.4.0-SNAPSHOT io.github.davidwhitlock.joy projects - 3.0.3 + 3.0.4-SNAPSHOT tests test 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/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/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/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..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 cantParseInvalidXmlFile() throws ParserConfigurationException { - AirlineXmlHelper helper = new AirlineXmlHelper(); + void throwsExceptionWhenParsingInvalidXmlFile() throws ParserConfigurationException { + 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 cantParseInvalidXmlFile() throws ParserConfigurationException { 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-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-web/pom.xml b/projects-parent/originals-parent/apptbook-web/pom.xml index bb0227ab6..7e4a3871e 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.4.0-SNAPSHOT io.github.davidwhitlock.joy projects - 3.0.3 + 3.0.4-SNAPSHOT tests test 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/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/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/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..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 cantParseInvalidXmlFile() throws ParserConfigurationException { - AppointmentBookXmlHelper helper = new AppointmentBookXmlHelper(); + void throwsExceptionWhenParsingInvalidXmlFile() throws ParserConfigurationException { + 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 cantParseInvalidXmlFile() throws ParserConfigurationException { 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/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/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/.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-web/pom.xml b/projects-parent/originals-parent/phonebill-web/pom.xml index e3ca9ac94..e311b3736 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.4.0-SNAPSHOT io.github.davidwhitlock.joy projects - 3.0.3 + 3.0.4-SNAPSHOT tests test 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/phonebill/pom.xml b/projects-parent/originals-parent/phonebill/pom.xml index 6098d5380..1e73ae4cc 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.3.0-SNAPSHOT Phone Bill Project A Phone Bill application for The Joy of Coding at Portland State University 2000 @@ -29,6 +29,11 @@ + + com.h2database + h2 + ${h2.version} + org.mockito mockito-core @@ -38,12 +43,17 @@ io.github.davidwhitlock.joy projects - 3.0.3 + 3.0.4-SNAPSHOT + + + io.github.davidwhitlock.joy + examples + 1.4.0-SNAPSHOT io.github.davidwhitlock.joy projects - 3.0.3 + 3.0.4-SNAPSHOT tests test 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/PhoneBillDAO.java b/projects-parent/originals-parent/phonebill/src/main/java/edu/pdx/cs/joy/phonebill/PhoneBillDAO.java new file mode 100644 index 000000000..97ff802e2 --- /dev/null +++ b/projects-parent/originals-parent/phonebill/src/main/java/edu/pdx/cs/joy/phonebill/PhoneBillDAO.java @@ -0,0 +1,108 @@ +package edu.pdx.cs.joy.phonebill; + +import edu.pdx.cs.joy.jdbc.H2DatabaseHelper; + +import java.io.File; +import java.sql.*; + +/** + * A Data Access Object (DAO) for persisting PhoneBill instances to a database. + * + * This is a simple example to demonstrate basic JDBC operations. + * Students can expand this to include PhoneCall persistence and more + * sophisticated query capabilities. + */ +public class PhoneBillDAO { + + private final Connection connection; + + /** + * Creates a new PhoneBillDAO with the specified database connection. + * + * @param connection the database connection to use + */ + public PhoneBillDAO(Connection connection) { + this.connection = connection; + } + + /** + * Creates the customers table in the database. + * + * @param connection the database connection to use + * @throws SQLException if a database error occurs + */ + public static void createTable(Connection connection) throws SQLException { + String createTableSQL = + "CREATE TABLE customers (" + + " name VARCHAR(255) PRIMARY KEY" + + ")"; + + try (Statement statement = connection.createStatement()) { + statement.execute(createTableSQL); + } + } + + /** + * Saves a PhoneBill to the database. + * + * @param bill the phone bill to save + * @throws SQLException if a database error occurs + */ + public void save(PhoneBill bill) throws SQLException { + String insertSQL = "INSERT INTO customers (name) VALUES (?)"; + + try (PreparedStatement statement = connection.prepareStatement(insertSQL)) { + statement.setString(1, bill.getCustomer()); + statement.executeUpdate(); + } + } + + /** + * Finds a PhoneBill by customer name. + * + * @param customerName the customer name to search for + * @return the PhoneBill for the customer, or null if not found + * @throws SQLException if a database error occurs + */ + public PhoneBill findByCustomer(String customerName) throws SQLException { + String selectSQL = "SELECT name FROM customers WHERE name = ?"; + + try (PreparedStatement statement = connection.prepareStatement(selectSQL)) { + statement.setString(1, customerName); + + try (ResultSet resultSet = statement.executeQuery()) { + if (resultSet.next()) { + String name = resultSet.getString("name"); + return new PhoneBill(name); + } + } + } + + return null; + } + + public static void main(String[] args) throws SQLException { + if (args.length < 2) { + System.err.println("Usage: java PhoneBillDAO "); + return; + } + + String dbFile = args[0]; + String customerName = args[1]; + try (Connection connection = H2DatabaseHelper.createFileBasedConnection(new File(dbFile))) { + PhoneBillDAO dao = new PhoneBillDAO(connection); + PhoneBillDAO.createTable(connection); + + PhoneBill bill = new PhoneBill(customerName); + dao.save(bill); + + PhoneBill retrievedBill = dao.findByCustomer(customerName); + if (retrievedBill != null) { + System.out.println("Retrieved PhoneBill for customer: " + retrievedBill.getCustomer()); + } else { + System.out.println("No PhoneBill found for customer: " + customerName); + } + } + } +} + diff --git a/projects-parent/originals-parent/phonebill/src/main/java/edu/pdx/cs/joy/phonebill/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); } diff --git a/projects-parent/originals-parent/phonebill/src/test/java/edu/pdx/cs/joy/phonebill/PhoneBillDAOTest.java b/projects-parent/originals-parent/phonebill/src/test/java/edu/pdx/cs/joy/phonebill/PhoneBillDAOTest.java new file mode 100644 index 000000000..eff5e73e8 --- /dev/null +++ b/projects-parent/originals-parent/phonebill/src/test/java/edu/pdx/cs/joy/phonebill/PhoneBillDAOTest.java @@ -0,0 +1,65 @@ +package edu.pdx.cs.joy.phonebill; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.sql.*; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; + +/** + * A simple example unit test that demonstrates persisting a PhoneBill + * to an H2 in-memory database using JDBC. + * + * This is a starting point for students to understand how to use + * Data Access Objects (DAOs) to persist domain objects to a database. + */ +public class PhoneBillDAOTest { + + private Connection connection; + private PhoneBillDAO dao; + + @BeforeEach + public void setUp() throws SQLException { + // Create an in-memory H2 database + connection = DriverManager.getConnection("jdbc:h2:mem:phonebill_test"); + + // Create the phone_bills table + PhoneBillDAO.createTable(connection); + + // Create the DAO + dao = new PhoneBillDAO(connection); + } + + @AfterEach + public void tearDown() throws SQLException { + if (connection != null && !connection.isClosed()) { + connection.close(); + } + } + + @Test + public void canPersistAndFetchPhoneBillByCustomerName() throws SQLException { + String customerName = "Jane Doe"; + PhoneBill bill = new PhoneBill(customerName); + + // Persist the phone bill using the DAO + dao.save(bill); + + // Fetch the phone bill by customer name + PhoneBill fetchedBill = dao.findByCustomer(customerName); + + // Validate that the fetched bill matches the original + assertThat(fetchedBill, is(notNullValue())); + assertThat(fetchedBill.getCustomer(), is(equalTo(customerName))); + } + + @Test + public void returnsNullWhenPhoneBillNotFound() throws SQLException { + PhoneBill fetchedBill = dao.findByCustomer("Non-existent Customer"); + assertThat(fetchedBill, is(nullValue())); + } +} + diff --git a/projects-parent/originals-parent/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/.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 { diff --git a/projects-parent/originals-parent/student/pom.xml b/projects-parent/originals-parent/student/pom.xml index bcc443733..67b23a190 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.4.0-SNAPSHOT io.github.davidwhitlock.joy projects - 3.0.3 + 3.0.4-SNAPSHOT tests test 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 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/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 diff --git a/web/pom.xml b/web/pom.xml index aa380907c..b6c8715ba 100644 --- a/web/pom.xml +++ b/web/pom.xml @@ -3,16 +3,16 @@ 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 + 6.2.12.Final 8080 @@ -107,22 +107,27 @@ org.apache.commons commons-fileupload2-jakarta-servlet6 - 2.0.0-M2 + 2.0.0-M4 commons-io commons-io - 2.18.0 + 2.21.0 io.github.davidwhitlock.joy examples - 1.3.4 + 1.4.0-SNAPSHOT + + + io.github.davidwhitlock.joy + family + 1.1.6-SNAPSHOT jakarta.xml.bind jakarta.xml.bind-api - 4.0.2 + 4.0.4 org.jboss.resteasy