elharo commented on code in PR #508:
URL: https://github.com/apache/maven-jar-plugin/pull/508#discussion_r3945382693


##########
src/main/java/org/apache/maven/plugins/jar/DirectoryRole.java:
##########
@@ -0,0 +1,73 @@
+/*
+ * 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.
+ */
+package org.apache.maven.plugins.jar;
+
+/**
+ * Directories that the archiver needs to handle in a special way.
+ */
+enum DirectoryRole {
+    /**
+     * The root directory. This is usually {@code "target/classes"}.
+     * The next locations can be {@link #META_INF}, {@link #NAMED_MODULE} or 
{@link #RESOURCES}.
+     */
+    ROOT,
+
+    /**
+     * The {@code "META-INF"} or {@code "<module>/META-INF"} directory.
+     * This is part of the <abbr>JAR</abbr> specification.
+     * The next locations can be {@link #VERSIONS} or {@link 
#VERSIONS_MODULAR}.
+     */
+    META_INF,
+
+    /**
+     * The {@code "META-INF/versions"} or {@code "<module>/META-INF/versions"} 
directory.
+     * This is part of the <abbr>JAR</abbr> specification, except the {@code 
<module>} prefix.
+     * The sub-directories are named according to Java releases such as "21".
+     * The next location can only be {@link #RESOURCES}.
+     */
+    VERSIONS,
+
+    /**
+     * The Maven-specific {@code "META-INF/versions-modular"} directory.
+     * {@code "<module>/META-INF/versions-modular"} is not forbidden, but does 
not make sense.
+     * The sub-directories are named according to Java releases such as "21".
+     * The next location can only be {@link #MODULES}.
+     */
+    VERSIONS_MODULAR,
+
+    /**
+     * The Maven-specific {@code "META-INF/versions-modular"} directory.

Review Comment:
   Why is this the same as VERSIONS_MODULAR? Is one of them off in the docs?



##########
src/main/java/org/apache/maven/plugins/jar/ExcludedFiles.java:
##########
@@ -0,0 +1,146 @@
+/*
+ * 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.
+ */
+package org.apache.maven.plugins.jar;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.util.List;
+
+/**
+ * A list of files to temporarily move outside the directory to package in a 
<abbr>JAR</abbr> archive.
+ * This class is needed when the {@link AbstractJarMojo} configuration has 
include or exclude filters.
+ * Excluded files are temporarily moved outside the directory to archive.
+ * We move these files for making possible to specify the whole directory to 
the {@code jar} tool.
+ * This approach is used instead of enumerating files in arguments given to 
the {@code jar} tool because
+ * such enumeration cannot contain directory entries (otherwise the whole 
directory would be included).
+ * Some software such as Spring applications component scan relies on the 
presence of directory entries.
+ */
+final class ExcludedFiles implements Closeable {
+    /**
+     * The paths of files or directories to temporarily move in another 
directory.
+     */
+    private final Path[] original;
+
+    /**
+     * The paths where files or directories were moved.
+     * For each index <var>i</var>, the original path of {@code moved[i]} was 
{@code original[i]}.
+     */
+    private final Path[] moved;
+
+    /**
+     * Temporary directory which will contain the {@code moved} files.
+     * It should be a parent directory of {@link #original} files for
+     * increasing the chances that it is on the same file system.
+     */
+    private final Path temporaryDirectory;
+
+    /**
+     * Index of the first path which is a directory instead of a file.
+     * All paths before this index in the {@link #original} and {@link #moved} 
arrays are files.
+     * All paths at this index and after this index are directories.
+     */
+    private final int indexOfFirstDirectory;
+
+    /**
+     * Creates a new list of files to move in a temporary directory.
+     *
+     * @param directory the directory which was scanned for files to include 
in the <abbr>JAR</abbr>
+     * @param excludedFiles paths of files to temporarily move in another 
directory

Review Comment:
   move in --> move to



##########
src/main/java/org/apache/maven/plugins/jar/TimestampCheck.java:
##########
@@ -0,0 +1,261 @@
+/*
+ * 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.
+ */
+package org.apache.maven.plugins.jar;
+
+import java.io.IOException;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.nio.file.attribute.FileTime;
+import java.util.Collection;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+
+import org.apache.maven.api.plugin.Log;
+
+/**
+ * Checks file timestamps in order to determine if anything changed compared 
to an existing <abbr>JAR</abbr> file.
+ * This class may scan directories, but only if they have not already been 
visited by {@link FileCollector}.
+ * The latter can only occur if {@link FileCollector} has no {@code 
PathMatcher}.
+ * Therefore, this class uses no {@code PathMatcher}.
+ *
+ * <h2>Ignore files</h2>

Review Comment:
   get rid of the h2



##########
src/it/jar-without-sources/pom.xml:
##########
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"; 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"; 
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
http://maven.apache.org/maven-v4_0_0.xsd";>
+  <modelVersion>4.0.0</modelVersion>
+  <groupId>org.apache.maven.plugins</groupId>
+  <artifactId>jar-without-sources</artifactId>
+  <version>1.0-SNAPSHOT</version>
+  <packaging>jar</packaging>
+  <name>jar-without-sources-it</name>
+  <description>This project has neither `src/main` nor `src/test`, so nothing 
is compiled
+    and the `target` directory is never created by an earlier lifecycle phase.
+    Versions 3.x of the JAR plugin still produced an empty, manifest-only JAR 
in that case.
+    This IT verifies that versions 4.x of the JAR plugin has the same behavior 
for compatibility purposes.

Review Comment:
   version 4.x
   
   or
   
   has --> have



##########
src/test/java/org/apache/maven/plugins/jar/ArchiveTest.java:
##########
@@ -0,0 +1,257 @@
+/*
+ * 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.
+ */
+package org.apache.maven.plugins.jar;
+
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.jar.Attributes;
+import java.util.jar.Manifest;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Unit tests for {@link Archive}, focused on the two behaviours that are 
otherwise only
+ * exercised by integration tests whose outcome depends on the (unspecified) 
filesystem
+ * directory-iteration order, and therefore pass on some platforms while 
failing on others.
+ */
+class ArchiveTest {
+
+    /**
+     * Creates an {@code Archive} suitable for a unit test. {@code 
forceCreation = true} skips the
+     * existing-JAR timestamp check, so the (here {@code null}) logger is 
never dereferenced.
+     */
+    private static Archive archive(String moduleName, Runtime.Version version, 
Path directory) {
+        return new Archive(directory.resolve("out.jar"), moduleName, version, 
directory, true, null);
+    }
+
+    /**
+     * Creates a manifest with the main class attribute set to the given value.
+     *
+     * @param value value of the main class attribute
+     * @return a new manifest with the given attribute value
+     */
+    private static Manifest manifestWithMainClass(String value) {
+        Manifest m = new Manifest();
+        Attributes attributes = m.getMainAttributes();
+        attributes.put(Attributes.Name.MANIFEST_VERSION, "1.0");
+        attributes.put(Attributes.Name.MAIN_CLASS, value);
+        return m;
+    }
+
+    /**
+     * Returns the value of the main class attribute.
+     *
+     * @param m the manifest from which to get the value
+     * @return the main class attribute value, or {@code null} if none
+     */
+    private static Object mainClassOf(Manifest m) {
+        return m.getMainAttributes().get(Attributes.Name.MAIN_CLASS);
+    }
+
+    /**
+     * Verifies that {@link Archive#setMainClass(Manifest)} takes ownership 
for a module-qualified
+     * {@code "module/Class"} main class.
+     *
+     * The {@code "foo.bar/"} prefix in this test (the module name) is a Maven 
extension.
+     * The standard <abbr>JAR</abbr> specification accepts only the {@code 
"foo.MainFile"} class name.
+     */
+    @Test
+    void owningModuleClaimsMainClassAndRemovesItFromManifest() {
+        Archive owner = archive("foo.bar", null, Path.of("."));
+        Manifest m = manifestWithMainClass("foo.bar/foo.MainFile");
+        // The owner keeps the main class (emitted via --main-class) ...
+        assertTrue(owner.setMainClass(m));
+        // ... and the raw `module/Class` value is removed from the written 
manifest.
+        assertNull(mainClassOf(m));
+    }
+
+    /**
+     * Verifies that {@link Archive#setMainClass(Manifest)} does <em>not</em> 
take ownership of
+     * a module-qualified {@code "module/Class"} main class when the module 
name does not match.
+     * The {@code "foo.bar/"} prefix in this test (the module name) is a Maven 
extension.
+     * The standard <abbr>JAR</abbr> specification accepts only the {@code 
"foo.MainFile"} class name.
+     */
+    @Test
+    void nonOwningModuleRejectsMainClass() {
+        Archive nonOwner = archive("foo.bar.more", null, Path.of("."));
+        Manifest m = manifestWithMainClass("foo.bar/foo.MainFile");
+        assertFalse(nonOwner.setMainClass(m));
+        assertNull(mainClassOf(m));
+    }
+
+    /**
+     * Tests that which module keeps the main class does not depend on 
processing order.
+     * {@link ToolExecutor} gives each module a <em>copy</em> of the shared 
plugin manifest;
+     * this pins that the owning module (and only it) keeps the main class in 
either order,
+     * and that the shared manifest is never consumed.
+     */
+    @Test
+    void mainClassAssignmentIsIndependentOfModuleOrder() {
+        assertOwnership("foo.bar", "foo.bar.more", true); // owner processed 
first
+        assertOwnership("foo.bar.more", "foo.bar", false); // non-owner 
processed first
+    }
+
+    /**
+     * Helper method for {@link 
#mainClassAssignmentIsIndependentOfModuleOrder()}.
+     * Asserts that {@link Archive#setMainClass(Manifest)} returns {@code true}
+     * for the owner and {@code false} for the other module.
+     *
+     * <p>The {@code "foo.bar/"} prefix in this test (the module name) is a 
Maven extension.
+     * The standard <abbr>JAR</abbr> specification accepts only the {@code 
"foo.MainFile"} class name.
+     * This extension is used by the plugin for identifying in which 
<abbr>JAR</abbr> file to add this
+     * {@code Main-Class} manifest entry.</p>
+     */
+    private static void assertOwnership(String first, String second, boolean 
ownerIsFirst) {
+        final Path path = Path.of(".");
+        final Manifest shared = manifestWithMainClass("foo.bar/foo.MainFile");
+        final Manifest m1 = new Manifest(shared);
+        final Manifest m2 = new Manifest(shared);
+        assertEquals(ownerIsFirst, archive(first, null, 
path).setMainClass(m1));
+        assertEquals(!ownerIsFirst, archive(second, null, 
path).setMainClass(m2));
+        // Per-module copies must leave the shared plugin manifest untouched.
+        assertEquals("foo.bar/foo.MainFile", mainClassOf(shared));
+        assertNull(mainClassOf(m1));
+        assertNull(mainClassOf(m2));
+    }
+
+    /**
+     * Tests that {@code FileSet} association to target release is consistent 
regardless of creation order.
+     * Verifies that the base (version-less) release binds to the true {@code 
<module>} directory even
+     * when the {@link Archive} was first created from a {@code 
META-INF/versions-modular/<n>/<module>}
+     * directory (which happens when the file-tree walk visits the version 
directory first).
+     */
+    @Test
+    void baseReleaseBindingIsIndependentOfDirectoryOrder() {
+        final Path base = Path.of("classes", "foo.bar");
+        final Path v16 = Path.of("classes", "META-INF", "versions-modular", 
"16", "foo.bar");
+        final Runtime.Version r16 = Runtime.Version.parse("16");
+
+        // Version-directory first (the failing order): Archive seeded from 
v16, base registered later.
+        final Archive a = archive("foo.bar", r16, v16);
+        assertNotSame(a.newTargetRelease(v16, r16), a.newTargetRelease(base, 
null));
+        assertEquals(base, a.baseRelease().directory, "base must rebind to the 
version-less directory");
+
+        // Base-directory first: still correct.
+        final Archive b = archive("foo.bar", null, base);
+        assertNotSame(b.newTargetRelease(base, null), b.newTargetRelease(v16, 
r16));
+        assertEquals(base, b.baseRelease().directory);
+    }
+
+    /**
+     * Ensures that all path are relative.
+     *
+     * <h4>Historical note</h4>
+     * In our tests, it seems that the first <abbr>JAR</abbr> entry after the 
{@code -C} option
+     * must be relative, and only that file. Furthermore, it seems that this 
file must be the
+     * shortest. We tried to apply this heuristic rules in a branch, but it 
does not save a lot

Review Comment:
   file is the shortest or file name is the shortest?



##########
src/test/java/org/apache/maven/plugins/jar/ArchiveTest.java:
##########
@@ -0,0 +1,257 @@
+/*
+ * 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.
+ */
+package org.apache.maven.plugins.jar;
+
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.jar.Attributes;
+import java.util.jar.Manifest;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Unit tests for {@link Archive}, focused on the two behaviours that are 
otherwise only
+ * exercised by integration tests whose outcome depends on the (unspecified) 
filesystem
+ * directory-iteration order, and therefore pass on some platforms while 
failing on others.
+ */
+class ArchiveTest {
+
+    /**
+     * Creates an {@code Archive} suitable for a unit test. {@code 
forceCreation = true} skips the
+     * existing-JAR timestamp check, so the (here {@code null}) logger is 
never dereferenced.
+     */
+    private static Archive archive(String moduleName, Runtime.Version version, 
Path directory) {
+        return new Archive(directory.resolve("out.jar"), moduleName, version, 
directory, true, null);
+    }
+
+    /**
+     * Creates a manifest with the main class attribute set to the given value.
+     *
+     * @param value value of the main class attribute
+     * @return a new manifest with the given attribute value
+     */
+    private static Manifest manifestWithMainClass(String value) {
+        Manifest m = new Manifest();
+        Attributes attributes = m.getMainAttributes();
+        attributes.put(Attributes.Name.MANIFEST_VERSION, "1.0");
+        attributes.put(Attributes.Name.MAIN_CLASS, value);
+        return m;
+    }
+
+    /**
+     * Returns the value of the main class attribute.
+     *
+     * @param m the manifest from which to get the value
+     * @return the main class attribute value, or {@code null} if none
+     */
+    private static Object mainClassOf(Manifest m) {
+        return m.getMainAttributes().get(Attributes.Name.MAIN_CLASS);
+    }
+
+    /**
+     * Verifies that {@link Archive#setMainClass(Manifest)} takes ownership 
for a module-qualified
+     * {@code "module/Class"} main class.
+     *
+     * The {@code "foo.bar/"} prefix in this test (the module name) is a Maven 
extension.
+     * The standard <abbr>JAR</abbr> specification accepts only the {@code 
"foo.MainFile"} class name.
+     */
+    @Test
+    void owningModuleClaimsMainClassAndRemovesItFromManifest() {
+        Archive owner = archive("foo.bar", null, Path.of("."));
+        Manifest m = manifestWithMainClass("foo.bar/foo.MainFile");
+        // The owner keeps the main class (emitted via --main-class) ...
+        assertTrue(owner.setMainClass(m));
+        // ... and the raw `module/Class` value is removed from the written 
manifest.
+        assertNull(mainClassOf(m));
+    }
+
+    /**
+     * Verifies that {@link Archive#setMainClass(Manifest)} does <em>not</em> 
take ownership of
+     * a module-qualified {@code "module/Class"} main class when the module 
name does not match.
+     * The {@code "foo.bar/"} prefix in this test (the module name) is a Maven 
extension.
+     * The standard <abbr>JAR</abbr> specification accepts only the {@code 
"foo.MainFile"} class name.
+     */
+    @Test
+    void nonOwningModuleRejectsMainClass() {
+        Archive nonOwner = archive("foo.bar.more", null, Path.of("."));
+        Manifest m = manifestWithMainClass("foo.bar/foo.MainFile");
+        assertFalse(nonOwner.setMainClass(m));
+        assertNull(mainClassOf(m));
+    }
+
+    /**
+     * Tests that which module keeps the main class does not depend on 
processing order.
+     * {@link ToolExecutor} gives each module a <em>copy</em> of the shared 
plugin manifest;
+     * this pins that the owning module (and only it) keeps the main class in 
either order,
+     * and that the shared manifest is never consumed.
+     */
+    @Test
+    void mainClassAssignmentIsIndependentOfModuleOrder() {
+        assertOwnership("foo.bar", "foo.bar.more", true); // owner processed 
first
+        assertOwnership("foo.bar.more", "foo.bar", false); // non-owner 
processed first
+    }
+
+    /**
+     * Helper method for {@link 
#mainClassAssignmentIsIndependentOfModuleOrder()}.
+     * Asserts that {@link Archive#setMainClass(Manifest)} returns {@code true}
+     * for the owner and {@code false} for the other module.
+     *
+     * <p>The {@code "foo.bar/"} prefix in this test (the module name) is a 
Maven extension.
+     * The standard <abbr>JAR</abbr> specification accepts only the {@code 
"foo.MainFile"} class name.
+     * This extension is used by the plugin for identifying in which 
<abbr>JAR</abbr> file to add this

Review Comment:
    for identifying in which --> to identify which



##########
src/main/java/org/apache/maven/plugins/jar/ExcludedFiles.java:
##########
@@ -0,0 +1,146 @@
+/*
+ * 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.
+ */
+package org.apache.maven.plugins.jar;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.util.List;
+
+/**
+ * A list of files to temporarily move outside the directory to package in a 
<abbr>JAR</abbr> archive.
+ * This class is needed when the {@link AbstractJarMojo} configuration has 
include or exclude filters.
+ * Excluded files are temporarily moved outside the directory to archive.
+ * We move these files for making possible to specify the whole directory to 
the {@code jar} tool.

Review Comment:
   We move these files to make it possible to pass an entire directory to the 
{@code jar} tool.



##########
src/main/java/org/apache/maven/plugins/jar/FileCollector.java:
##########
@@ -0,0 +1,542 @@
+/*
+ * 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.
+ */
+package org.apache.maven.plugins.jar;
+
+import java.io.IOException;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.PathMatcher;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Deque;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.maven.api.annotations.Nonnull;
+import org.apache.maven.api.annotations.Nullable;
+import org.apache.maven.api.services.PathMatcherFactory;
+
+/**
+ * Dispatch the files from the output directory into the <abbr>JAR</abbr> 
files to create.
+ * Instead of just archiving as-is the content of the output directory, this 
class separates
+ * the following subdirectories to the options listed below:
+ *
+ * <ul>
+ *   <li>The {@code META-INF/MANIFEST.MF} file will be given to the {@code 
--manifest} option.</li>
+ *   <li>Files in the following directories will be given to the {@code 
--release} option:
+ *     <ul>
+ *       <li>{@code META-INF/versions/}</li>
+ *       <li>{@code META-INF/versions-modular/<module>/}</li>
+ *       <li>{@code <module>/META-INF/versions/}</li>
+ *     </ul>
+ *   </li>
+ * </ul>
+ *
+ * The reason for using the {@code --release} and {@code --manifest} options 
instead of adding explicitly
+ * the entries is because the options allow the {@code jar} tool to perform 
additional verifications.
+ * For example, when using the {@code --release} option, {@code jar} verifies 
the <abbr>API</abbr> compatibility.

Review Comment:
   the <abbr>API</abbr> compatibility --> <abbr>API</abbr> compatibility



##########
src/main/java/org/apache/maven/plugins/jar/ExcludedFiles.java:
##########
@@ -0,0 +1,146 @@
+/*
+ * 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.
+ */
+package org.apache.maven.plugins.jar;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.util.List;
+
+/**
+ * A list of files to temporarily move outside the directory to package in a 
<abbr>JAR</abbr> archive.
+ * This class is needed when the {@link AbstractJarMojo} configuration has 
include or exclude filters.
+ * Excluded files are temporarily moved outside the directory to archive.
+ * We move these files for making possible to specify the whole directory to 
the {@code jar} tool.
+ * This approach is used instead of enumerating files in arguments given to 
the {@code jar} tool because
+ * such enumeration cannot contain directory entries (otherwise the whole 
directory would be included).
+ * Some software such as Spring applications component scan relies on the 
presence of directory entries.
+ */
+final class ExcludedFiles implements Closeable {
+    /**
+     * The paths of files or directories to temporarily move in another 
directory.
+     */
+    private final Path[] original;
+
+    /**
+     * The paths where files or directories were moved.
+     * For each index <var>i</var>, the original path of {@code moved[i]} was 
{@code original[i]}.
+     */
+    private final Path[] moved;
+
+    /**
+     * Temporary directory which will contain the {@code moved} files.
+     * It should be a parent directory of {@link #original} files for
+     * increasing the chances that it is on the same file system.
+     */
+    private final Path temporaryDirectory;
+
+    /**
+     * Index of the first path which is a directory instead of a file.
+     * All paths before this index in the {@link #original} and {@link #moved} 
arrays are files.
+     * All paths at this index and after this index are directories.
+     */
+    private final int indexOfFirstDirectory;
+
+    /**
+     * Creates a new list of files to move in a temporary directory.
+     *
+     * @param directory the directory which was scanned for files to include 
in the <abbr>JAR</abbr>
+     * @param excludedFiles paths of files to temporarily move in another 
directory
+     * @param excludedDirectories paths of directories to temporarily move in 
another directory

Review Comment:
   move in --> move to



##########
src/main/java/org/apache/maven/plugins/jar/FileCollector.java:
##########
@@ -0,0 +1,542 @@
+/*
+ * 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.
+ */
+package org.apache.maven.plugins.jar;
+
+import java.io.IOException;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.PathMatcher;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Deque;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.maven.api.annotations.Nonnull;
+import org.apache.maven.api.annotations.Nullable;
+import org.apache.maven.api.services.PathMatcherFactory;
+
+/**
+ * Dispatch the files from the output directory into the <abbr>JAR</abbr> 
files to create.
+ * Instead of just archiving as-is the content of the output directory, this 
class separates
+ * the following subdirectories to the options listed below:
+ *
+ * <ul>
+ *   <li>The {@code META-INF/MANIFEST.MF} file will be given to the {@code 
--manifest} option.</li>
+ *   <li>Files in the following directories will be given to the {@code 
--release} option:
+ *     <ul>
+ *       <li>{@code META-INF/versions/}</li>
+ *       <li>{@code META-INF/versions-modular/<module>/}</li>
+ *       <li>{@code <module>/META-INF/versions/}</li>
+ *     </ul>
+ *   </li>
+ * </ul>
+ *
+ * The reason for using the {@code --release} and {@code --manifest} options 
instead of adding explicitly

Review Comment:
   adding the entries explicitly



##########
src/main/java/org/apache/maven/plugins/jar/FileCollector.java:
##########
@@ -0,0 +1,542 @@
+/*
+ * 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.
+ */
+package org.apache.maven.plugins.jar;
+
+import java.io.IOException;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.PathMatcher;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Deque;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.maven.api.annotations.Nonnull;
+import org.apache.maven.api.annotations.Nullable;
+import org.apache.maven.api.services.PathMatcherFactory;
+
+/**
+ * Dispatch the files from the output directory into the <abbr>JAR</abbr> 
files to create.

Review Comment:
   Dispatch might not be the right word here



##########
src/main/java/org/apache/maven/plugins/jar/ExcludedFiles.java:
##########
@@ -0,0 +1,146 @@
+/*
+ * 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.
+ */
+package org.apache.maven.plugins.jar;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.util.List;
+
+/**
+ * A list of files to temporarily move outside the directory to package in a 
<abbr>JAR</abbr> archive.
+ * This class is needed when the {@link AbstractJarMojo} configuration has 
include or exclude filters.
+ * Excluded files are temporarily moved outside the directory to archive.
+ * We move these files for making possible to specify the whole directory to 
the {@code jar} tool.
+ * This approach is used instead of enumerating files in arguments given to 
the {@code jar} tool because
+ * such enumeration cannot contain directory entries (otherwise the whole 
directory would be included).
+ * Some software such as Spring applications component scan relies on the 
presence of directory entries.
+ */
+final class ExcludedFiles implements Closeable {
+    /**
+     * The paths of files or directories to temporarily move in another 
directory.
+     */
+    private final Path[] original;
+
+    /**
+     * The paths where files or directories were moved.
+     * For each index <var>i</var>, the original path of {@code moved[i]} was 
{@code original[i]}.
+     */
+    private final Path[] moved;
+
+    /**
+     * Temporary directory which will contain the {@code moved} files.
+     * It should be a parent directory of {@link #original} files for
+     * increasing the chances that it is on the same file system.
+     */
+    private final Path temporaryDirectory;
+
+    /**
+     * Index of the first path which is a directory instead of a file.
+     * All paths before this index in the {@link #original} and {@link #moved} 
arrays are files.
+     * All paths at this index and after this index are directories.
+     */
+    private final int indexOfFirstDirectory;
+
+    /**
+     * Creates a new list of files to move in a temporary directory.
+     *
+     * @param directory the directory which was scanned for files to include 
in the <abbr>JAR</abbr>
+     * @param excludedFiles paths of files to temporarily move in another 
directory
+     * @param excludedDirectories paths of directories to temporarily move in 
another directory
+     * @throws IOException if an error occurred while creating the temporary 
directory.
+     */
+    ExcludedFiles(Path directory, List<Path> excludedFiles, List<Path> 
excludedDirectories) throws IOException {
+        indexOfFirstDirectory = excludedFiles.size();

Review Comment:
   This is backwards from the usual way this is done. Not necessarily wrong, 
but normally the files to include would be copied into a temporary location 
instead. Is any cleanup needed to move these files back if something fails?
   
   I hope this is all working only in the target directory. We shouldn't be 
moving files around anywhere else. 



##########
src/main/java/org/apache/maven/plugins/jar/FileCollector.java:
##########
@@ -0,0 +1,542 @@
+/*
+ * 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.
+ */
+package org.apache.maven.plugins.jar;
+
+import java.io.IOException;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.PathMatcher;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Deque;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.maven.api.annotations.Nonnull;
+import org.apache.maven.api.annotations.Nullable;
+import org.apache.maven.api.services.PathMatcherFactory;
+
+/**
+ * Dispatch the files from the output directory into the <abbr>JAR</abbr> 
files to create.
+ * Instead of just archiving as-is the content of the output directory, this 
class separates
+ * the following subdirectories to the options listed below:
+ *
+ * <ul>
+ *   <li>The {@code META-INF/MANIFEST.MF} file will be given to the {@code 
--manifest} option.</li>
+ *   <li>Files in the following directories will be given to the {@code 
--release} option:
+ *     <ul>
+ *       <li>{@code META-INF/versions/}</li>
+ *       <li>{@code META-INF/versions-modular/<module>/}</li>
+ *       <li>{@code <module>/META-INF/versions/}</li>
+ *     </ul>
+ *   </li>
+ * </ul>
+ *
+ * The reason for using the {@code --release} and {@code --manifest} options 
instead of adding explicitly
+ * the entries is because the options allow the {@code jar} tool to perform 
additional verifications.
+ * For example, when using the {@code --release} option, {@code jar} verifies 
the <abbr>API</abbr> compatibility.
+ */
+final class FileCollector extends SimpleFileVisitor<Path> {
+    /**
+     * The file to check for deciding whether the <abbr>JAR</abbr> is modular.
+     */
+    static final String MODULE_DESCRIPTOR_FILE_NAME = "module-info.class";
+
+    /**
+     * The {@value} directory.
+     * This is part of <abbr>JAR</abbr> file specification.
+     */
+    private static final String VERSIONS = "versions";
+
+    /**
+     * The {@value} directory.
+     * This is Maven-specific.
+     */
+    private static final String VERSIONS_MODULAR = "versions-modular";
+
+    /**
+     * Context (logger, configuration) in which the <abbr>JAR</abbr> file are 
created.
+     */
+    private final ToolExecutor context;
+
+    /**
+     * Whether to detect multi-release <abbr>JAR</abbr> files.
+     * The default value is {@code true}.
+     *
+     * @see AbstractJarMojo#detectMultiReleaseJar
+     */
+    private final boolean detectMultiReleaseJar;
+
+    /**
+     * The root directory to traverse. It will be used for temporarily moving 
excluded files.
+     */
+    private final Path rootDirectory;
+
+    /**
+     * Combination of includes and excludes path matcher applied on files.
+     */
+    @Nonnull
+    private final PathMatcher fileMatcher;
+
+    /**
+     * Combination of includes and excludes path matcher applied on 
directories.
+     */
+    @Nonnull
+    private final PathMatcher directoryMatcher;
+
+    /**
+     * Files to exclude. These files will be moved to a temporary location
+     * for allowing {@link ToolExecutor} to specify whole directories to the 
{@code jar} tool.

Review Comment:
   to allow



##########
src/main/java/org/apache/maven/plugins/jar/FileCollector.java:
##########
@@ -0,0 +1,542 @@
+/*
+ * 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.
+ */
+package org.apache.maven.plugins.jar;
+
+import java.io.IOException;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.PathMatcher;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Deque;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.maven.api.annotations.Nonnull;
+import org.apache.maven.api.annotations.Nullable;
+import org.apache.maven.api.services.PathMatcherFactory;
+
+/**
+ * Dispatch the files from the output directory into the <abbr>JAR</abbr> 
files to create.
+ * Instead of just archiving as-is the content of the output directory, this 
class separates

Review Comment:
   Instead of just archiving the content of the output directory as-is,



##########
src/main/java/org/apache/maven/plugins/jar/FileCollector.java:
##########
@@ -0,0 +1,542 @@
+/*
+ * 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.
+ */
+package org.apache.maven.plugins.jar;
+
+import java.io.IOException;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.PathMatcher;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Deque;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.maven.api.annotations.Nonnull;
+import org.apache.maven.api.annotations.Nullable;
+import org.apache.maven.api.services.PathMatcherFactory;
+
+/**
+ * Dispatch the files from the output directory into the <abbr>JAR</abbr> 
files to create.
+ * Instead of just archiving as-is the content of the output directory, this 
class separates
+ * the following subdirectories to the options listed below:
+ *
+ * <ul>
+ *   <li>The {@code META-INF/MANIFEST.MF} file will be given to the {@code 
--manifest} option.</li>
+ *   <li>Files in the following directories will be given to the {@code 
--release} option:
+ *     <ul>
+ *       <li>{@code META-INF/versions/}</li>
+ *       <li>{@code META-INF/versions-modular/<module>/}</li>
+ *       <li>{@code <module>/META-INF/versions/}</li>
+ *     </ul>
+ *   </li>
+ * </ul>
+ *
+ * The reason for using the {@code --release} and {@code --manifest} options 
instead of adding explicitly
+ * the entries is because the options allow the {@code jar} tool to perform 
additional verifications.
+ * For example, when using the {@code --release} option, {@code jar} verifies 
the <abbr>API</abbr> compatibility.
+ */
+final class FileCollector extends SimpleFileVisitor<Path> {
+    /**
+     * The file to check for deciding whether the <abbr>JAR</abbr> is modular.
+     */
+    static final String MODULE_DESCRIPTOR_FILE_NAME = "module-info.class";
+
+    /**
+     * The {@value} directory.
+     * This is part of <abbr>JAR</abbr> file specification.
+     */
+    private static final String VERSIONS = "versions";
+
+    /**
+     * The {@value} directory.
+     * This is Maven-specific.
+     */
+    private static final String VERSIONS_MODULAR = "versions-modular";
+
+    /**
+     * Context (logger, configuration) in which the <abbr>JAR</abbr> file are 
created.
+     */
+    private final ToolExecutor context;
+
+    /**
+     * Whether to detect multi-release <abbr>JAR</abbr> files.
+     * The default value is {@code true}.
+     *
+     * @see AbstractJarMojo#detectMultiReleaseJar
+     */
+    private final boolean detectMultiReleaseJar;
+
+    /**
+     * The root directory to traverse. It will be used for temporarily moving 
excluded files.
+     */
+    private final Path rootDirectory;
+
+    /**
+     * Combination of includes and excludes path matcher applied on files.
+     */
+    @Nonnull
+    private final PathMatcher fileMatcher;
+
+    /**
+     * Combination of includes and excludes path matcher applied on 
directories.
+     */
+    @Nonnull
+    private final PathMatcher directoryMatcher;
+
+    /**
+     * Files to exclude. These files will be moved to a temporary location
+     * for allowing {@link ToolExecutor} to specify whole directories to the 
{@code jar} tool.
+     * Specifying whole directories is preferable to enumerating the files 
because otherwise,
+     * the generated <abbr>JAR</abbr> file contains only entries for the files 
and is missing
+     * entries for the directories.
+     *
+     * <p>This field is {@code null} if it is not possible to have any 
excluded file
+     * (because there is no include/exclude filters).</p>
+     */
+    @Nullable
+    private final List<Path> excludedFiles;
+
+    /**
+     * Directories to exclude. This field serves the same purpose as {@link 
#excludedFiles},
+     * but where the sources a directories instead of files.

Review Comment:
   sources are



##########
src/main/java/org/apache/maven/plugins/jar/FileCollector.java:
##########
@@ -0,0 +1,542 @@
+/*
+ * 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.
+ */
+package org.apache.maven.plugins.jar;
+
+import java.io.IOException;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.PathMatcher;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Deque;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.maven.api.annotations.Nonnull;
+import org.apache.maven.api.annotations.Nullable;
+import org.apache.maven.api.services.PathMatcherFactory;
+
+/**
+ * Dispatch the files from the output directory into the <abbr>JAR</abbr> 
files to create.
+ * Instead of just archiving as-is the content of the output directory, this 
class separates
+ * the following subdirectories to the options listed below:
+ *
+ * <ul>
+ *   <li>The {@code META-INF/MANIFEST.MF} file will be given to the {@code 
--manifest} option.</li>
+ *   <li>Files in the following directories will be given to the {@code 
--release} option:
+ *     <ul>
+ *       <li>{@code META-INF/versions/}</li>
+ *       <li>{@code META-INF/versions-modular/<module>/}</li>
+ *       <li>{@code <module>/META-INF/versions/}</li>
+ *     </ul>
+ *   </li>
+ * </ul>
+ *
+ * The reason for using the {@code --release} and {@code --manifest} options 
instead of adding explicitly
+ * the entries is because the options allow the {@code jar} tool to perform 
additional verifications.
+ * For example, when using the {@code --release} option, {@code jar} verifies 
the <abbr>API</abbr> compatibility.
+ */
+final class FileCollector extends SimpleFileVisitor<Path> {
+    /**
+     * The file to check for deciding whether the <abbr>JAR</abbr> is modular.

Review Comment:
   for deciding --> to decide



##########
src/main/java/org/apache/maven/plugins/jar/FileCollector.java:
##########
@@ -0,0 +1,542 @@
+/*
+ * 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.
+ */
+package org.apache.maven.plugins.jar;
+
+import java.io.IOException;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.PathMatcher;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Deque;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.maven.api.annotations.Nonnull;
+import org.apache.maven.api.annotations.Nullable;
+import org.apache.maven.api.services.PathMatcherFactory;
+
+/**
+ * Dispatch the files from the output directory into the <abbr>JAR</abbr> 
files to create.
+ * Instead of just archiving as-is the content of the output directory, this 
class separates
+ * the following subdirectories to the options listed below:
+ *
+ * <ul>
+ *   <li>The {@code META-INF/MANIFEST.MF} file will be given to the {@code 
--manifest} option.</li>
+ *   <li>Files in the following directories will be given to the {@code 
--release} option:
+ *     <ul>
+ *       <li>{@code META-INF/versions/}</li>
+ *       <li>{@code META-INF/versions-modular/<module>/}</li>
+ *       <li>{@code <module>/META-INF/versions/}</li>
+ *     </ul>
+ *   </li>
+ * </ul>
+ *
+ * The reason for using the {@code --release} and {@code --manifest} options 
instead of adding explicitly
+ * the entries is because the options allow the {@code jar} tool to perform 
additional verifications.
+ * For example, when using the {@code --release} option, {@code jar} verifies 
the <abbr>API</abbr> compatibility.
+ */
+final class FileCollector extends SimpleFileVisitor<Path> {
+    /**
+     * The file to check for deciding whether the <abbr>JAR</abbr> is modular.
+     */
+    static final String MODULE_DESCRIPTOR_FILE_NAME = "module-info.class";
+
+    /**
+     * The {@value} directory.
+     * This is part of <abbr>JAR</abbr> file specification.
+     */
+    private static final String VERSIONS = "versions";
+
+    /**
+     * The {@value} directory.

Review Comment:
   again, why the same directory as above?



##########
src/test/java/org/apache/maven/plugins/jar/ArchiveTest.java:
##########
@@ -0,0 +1,257 @@
+/*
+ * 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.
+ */
+package org.apache.maven.plugins.jar;
+
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.jar.Attributes;
+import java.util.jar.Manifest;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Unit tests for {@link Archive}, focused on the two behaviours that are 
otherwise only
+ * exercised by integration tests whose outcome depends on the (unspecified) 
filesystem
+ * directory-iteration order, and therefore pass on some platforms while 
failing on others.
+ */
+class ArchiveTest {
+
+    /**
+     * Creates an {@code Archive} suitable for a unit test. {@code 
forceCreation = true} skips the
+     * existing-JAR timestamp check, so the (here {@code null}) logger is 
never dereferenced.
+     */
+    private static Archive archive(String moduleName, Runtime.Version version, 
Path directory) {
+        return new Archive(directory.resolve("out.jar"), moduleName, version, 
directory, true, null);
+    }
+
+    /**
+     * Creates a manifest with the main class attribute set to the given value.
+     *
+     * @param value value of the main class attribute
+     * @return a new manifest with the given attribute value
+     */
+    private static Manifest manifestWithMainClass(String value) {
+        Manifest m = new Manifest();
+        Attributes attributes = m.getMainAttributes();
+        attributes.put(Attributes.Name.MANIFEST_VERSION, "1.0");
+        attributes.put(Attributes.Name.MAIN_CLASS, value);
+        return m;
+    }
+
+    /**
+     * Returns the value of the main class attribute.
+     *
+     * @param m the manifest from which to get the value

Review Comment:
   m --> manifest



##########
src/main/java/org/apache/maven/plugins/jar/ToolExecutor.java:
##########
@@ -0,0 +1,551 @@
+/*
+ * 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.
+ */
+package org.apache.maven.plugins.jar;
+
+import javax.lang.model.SourceVersion;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.jar.Attributes;
+import java.util.jar.Manifest;
+import java.util.spi.ToolProvider;
+
+import org.apache.maven.api.Project;
+import org.apache.maven.api.Type;
+import org.apache.maven.api.plugin.Log;
+import org.apache.maven.api.plugin.MojoException;
+import org.apache.maven.shared.archiver.MavenArchiveConfiguration;
+
+/**
+ * Writer of <abbr>JAR</abbr> files using the information collected by {@link 
FileCollector}.
+ * This class uses the {@code "jar"} tool provided with the <abbr>JDK</abbr>.
+ */
+final class ToolExecutor {
+    /**
+     * First JDK feature version whose {@code jar} tool can run {@code 
--validate} on archives
+     * that contain records. Earlier {@code jar} tools failed with "This 
feature requires ASM8"
+     * (JDK-8282446, fixed by JDK-8282508 in JDK 19 by not backported to a 17u 
or 18u update).
+     *
+     * @see <a 
href="https://bugs.openjdk.org/browse/JDK-8282446";>JDK-8282446</a>
+     * @see <a 
href="https://bugs.openjdk.org/browse/JDK-8282508";>JDK-8282508</a>
+     */
+    private static final int JDK_FIXING_JAR_VALIDATE = 19;
+
+    /**
+     * First JDK feature version whose {@code jar} tool support the {@code 
--date} option.
+     */
+    private static final int JDK_SUPPORT_DATE = 19;
+
+    /**
+     * The {@value} attribute. Its value is automatically generated by {@link 
Manifest},
+     * but may need to be replaced by another value if JDK-independent value 
is desired
+     * for strictly reproducible builds.
+     */
+    private static final String CREATED_BY = "Created-By";
+
+    /**
+     * The Maven project for which to create an archive.
+     */
+    final Project project;
+
+    /**
+     * {@code "jar"} or {@link "test-jar"}.
+     */
+    private final String artifactType;
+
+    /**
+     * The output directory where to write the <abbr>JAR</abbr> file.
+     * This is usually {@code ${baseDir}/target/}.
+     */
+    private final Path outputDirectory;
+
+    /**
+     * The <abbr>JAR</abbr> file name when package hierarchy is used.
+     * This is usually a file placed in the {@link 
ToolExecutor#outputDirectory} directory.
+     */
+    private final String finalName;
+
+    /**
+     * The classifier (e.g. "test"), or {@code null} if none.
+     */
+    private final String classifier;
+
+    /**
+     * Whether to validate the <abbr>JAR</abbr> file after its creation.
+     * If {@code null}, a value will be determined automatically based on 
heuristic rules.
+     */
+    private final Boolean validate;
+
+    /**
+     * The tool to use for creating the <abbr>JAR</abbr> files.
+     */
+    private final ToolProvider tool;
+
+    /**
+     * Where to send messages emitted by the "jar" tool.
+     */
+    private final PrintWriter messageWriter;
+
+    /**
+     * Where to send error messages emitted by the "jar" tool.
+     */
+    private final PrintWriter errorWriter;
+
+    /**
+     * Where the messages sent to {@link #messageWriter} are stored.
+     */
+    private final StringBuffer messages;
+
+    /**
+     * Where the messages sent to {@link #errorWriter} are stored.
+     */
+    private final StringBuffer errors;
+
+    /**
+     * A buffer for the arguments given to the "jar" tool, reused for each 
module.
+     * Each element of the list must be instances of either {@link String} or 
{@link Path}.
+     */
+    private final List<Object> arguments;
+
+    /**
+     * The paths to the created archive files.
+     * Map keys are module names or {@code null} if the project does not use 
module hierarchy.
+     * Values are (<var>type</var>, <var>path</var>) pairs associated with 
each module where
+     * <var>type</var> is {@code "pom"}, {@code "jar"} or {@code "test-jar"} 
and <var>path</var>
+     * is the path to the <abbr>POM</abbr> or <abbr>JAR</abbr> file.
+     */
+    private final Map<String, Map<String, Path>> result;
+
+    /**
+     * Mapper from Maven dependencies to Java modules, or {@code null} if the 
project does not use module hierarchy.
+     * This mapper is created only once for a Maven project and reused for 
each Java module to archive.
+     *
+     * <p>This field is not used directly by {@code ToolExecutor}. It is 
defined in this class for transferring
+     * this information from {@link AbstractJarMojo} to {@link 
PomDerivation.ForModule}.
+     * This is an internal mechanism that should not be public or 
protected.</p>
+     */
+    PomDerivation pomDerivation;
+
+    /**
+     * Manifest to merge with the manifest found in the files to archive.
+     * This is a manifest built from the {@code <archive>} plugin 
configuration.
+     * Can be {@code null} if there is noting to add to the existing manifests.
+     */
+    private final Manifest manifestFromPlugin;
+
+    /**
+     * The file from which {@link #manifestFromPlugin} has been read, or 
{@code null} if none.
+     * If non-null, reading that file must produce the same manifest as {@link 
#manifestFromPlugin}.
+     * It implies that this field must be {@code null} if {@link 
#manifestFromPlugin} is the result
+     * of merging elements specified in {@code <archive>} with a file 
specified in the plugin configuration.
+     */
+    private final Path manifestFile;
+
+    /**
+     * The archive configuration to use.
+     */
+    private final MavenArchiveConfiguration archiveConfiguration;
+
+    /**
+     * The timestamp in ISO-8601 extended offset date-time, or {@code null} if 
none.
+     * If user provided a value in seconds, it must have been converted to 
ISO-8601.
+     * This is used for reproducible builds.
+     */
+    private final String outputTimestamp;
+
+    /**
+     * Whether to force to build new <abbr>JAR</abbr> files even if none of 
the contents appear to have changed.
+     */
+    private final boolean forceCreation;
+
+    /**
+     * Where to send informative or error messages.
+     */
+    private final Log logger;
+
+    /**
+     * Creates a new writer.
+     *
+     * @param mojo the <abbr>MOJO</abbr> from which to get the configuration
+     * @param manifest manifest built from plugin configuration, or {@code 
null} if none
+     * @param archive the archive configuration
+     * @throws IOException if an error occurred while reading the manifest file
+     */
+    ToolExecutor(AbstractJarMojo mojo, Manifest manifest, 
MavenArchiveConfiguration archive) throws IOException {
+        project = mojo.project;
+        artifactType = mojo.getType();
+        outputDirectory = mojo.getOutputDirectory();
+        classifier = AbstractJarMojo.nullIfAbsent(mojo.getClassifier());
+        finalName =
+                (mojo.finalName != null) ? mojo.finalName : 
project.getBuild().getFinalName();
+        forceCreation = mojo.forceCreation;
+        outputTimestamp = mojo.getOutputTimestamp();
+        validate = mojo.getValidate();
+        logger = mojo.log;
+        tool = mojo.getJarTool();
+
+        var buffer = new StringWriter();
+        messages = buffer.getBuffer();
+        messageWriter = new PrintWriter(buffer);
+
+        buffer = new StringWriter();
+        errors = buffer.getBuffer();
+        errorWriter = new PrintWriter(buffer);

Review Comment:
   It's strange we have an errorWriter and a logger. Should we just log error 
messages instead?



##########
src/main/java/org/apache/maven/plugins/jar/FileCollector.java:
##########
@@ -0,0 +1,542 @@
+/*
+ * 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.
+ */
+package org.apache.maven.plugins.jar;
+
+import java.io.IOException;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.PathMatcher;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Deque;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.maven.api.annotations.Nonnull;
+import org.apache.maven.api.annotations.Nullable;
+import org.apache.maven.api.services.PathMatcherFactory;
+
+/**
+ * Dispatch the files from the output directory into the <abbr>JAR</abbr> 
files to create.
+ * Instead of just archiving as-is the content of the output directory, this 
class separates
+ * the following subdirectories to the options listed below:
+ *
+ * <ul>
+ *   <li>The {@code META-INF/MANIFEST.MF} file will be given to the {@code 
--manifest} option.</li>
+ *   <li>Files in the following directories will be given to the {@code 
--release} option:
+ *     <ul>
+ *       <li>{@code META-INF/versions/}</li>
+ *       <li>{@code META-INF/versions-modular/<module>/}</li>
+ *       <li>{@code <module>/META-INF/versions/}</li>
+ *     </ul>
+ *   </li>
+ * </ul>
+ *
+ * The reason for using the {@code --release} and {@code --manifest} options 
instead of adding explicitly
+ * the entries is because the options allow the {@code jar} tool to perform 
additional verifications.
+ * For example, when using the {@code --release} option, {@code jar} verifies 
the <abbr>API</abbr> compatibility.
+ */
+final class FileCollector extends SimpleFileVisitor<Path> {
+    /**
+     * The file to check for deciding whether the <abbr>JAR</abbr> is modular.
+     */
+    static final String MODULE_DESCRIPTOR_FILE_NAME = "module-info.class";
+
+    /**
+     * The {@value} directory.
+     * This is part of <abbr>JAR</abbr> file specification.
+     */
+    private static final String VERSIONS = "versions";
+
+    /**
+     * The {@value} directory.
+     * This is Maven-specific.
+     */
+    private static final String VERSIONS_MODULAR = "versions-modular";
+
+    /**
+     * Context (logger, configuration) in which the <abbr>JAR</abbr> file are 
created.
+     */
+    private final ToolExecutor context;
+
+    /**
+     * Whether to detect multi-release <abbr>JAR</abbr> files.
+     * The default value is {@code true}.
+     *
+     * @see AbstractJarMojo#detectMultiReleaseJar
+     */
+    private final boolean detectMultiReleaseJar;
+
+    /**
+     * The root directory to traverse. It will be used for temporarily moving 
excluded files.
+     */
+    private final Path rootDirectory;
+
+    /**
+     * Combination of includes and excludes path matcher applied on files.
+     */
+    @Nonnull
+    private final PathMatcher fileMatcher;
+
+    /**
+     * Combination of includes and excludes path matcher applied on 
directories.
+     */
+    @Nonnull
+    private final PathMatcher directoryMatcher;
+
+    /**
+     * Files to exclude. These files will be moved to a temporary location
+     * for allowing {@link ToolExecutor} to specify whole directories to the 
{@code jar} tool.
+     * Specifying whole directories is preferable to enumerating the files 
because otherwise,
+     * the generated <abbr>JAR</abbr> file contains only entries for the files 
and is missing
+     * entries for the directories.
+     *
+     * <p>This field is {@code null} if it is not possible to have any 
excluded file
+     * (because there is no include/exclude filters).</p>
+     */
+    @Nullable
+    private final List<Path> excludedFiles;
+
+    /**
+     * Directories to exclude. This field serves the same purpose as {@link 
#excludedFiles},
+     * but where the sources a directories instead of files.
+     */
+    @Nullable
+    private final List<Path> excludedDirectories;
+
+    /**
+     * Files found in the output directory when package hierarchy is used.
+     * At most one of {@code packageHierarchy} and {@link #moduleHierarchy} 
can be non-empty.
+     */
+    @Nonnull
+    private final Archive packageHierarchy;
+
+    /**
+     * Files found in the output directory when module hierarchy is used. Keys 
are module names.
+     * At most one of {@link #packageHierarchy} and {@code moduleHierarchy} 
can be non-empty.
+     */
+    @Nonnull
+    private final Map<String, Archive> moduleHierarchy;
+
+    /**
+     * The current module being archived. This field is updated every times 
that {@code FileCollector}
+     * visits a new module directory.
+     */
+    @Nonnull
+    private Archive currentModule;
+
+    /**
+     * The module and target Java release currently being scanned. This field 
is updated every times that

Review Comment:
   every time



##########
src/test/java/org/apache/maven/plugins/jar/ArchiveTest.java:
##########
@@ -0,0 +1,257 @@
+/*
+ * 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.
+ */
+package org.apache.maven.plugins.jar;
+
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.jar.Attributes;
+import java.util.jar.Manifest;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Unit tests for {@link Archive}, focused on the two behaviours that are 
otherwise only
+ * exercised by integration tests whose outcome depends on the (unspecified) 
filesystem
+ * directory-iteration order, and therefore pass on some platforms while 
failing on others.
+ */
+class ArchiveTest {
+
+    /**
+     * Creates an {@code Archive} suitable for a unit test. {@code 
forceCreation = true} skips the
+     * existing-JAR timestamp check, so the (here {@code null}) logger is 
never dereferenced.
+     */
+    private static Archive archive(String moduleName, Runtime.Version version, 
Path directory) {
+        return new Archive(directory.resolve("out.jar"), moduleName, version, 
directory, true, null);
+    }
+
+    /**
+     * Creates a manifest with the main class attribute set to the given value.
+     *
+     * @param value value of the main class attribute
+     * @return a new manifest with the given attribute value
+     */
+    private static Manifest manifestWithMainClass(String value) {
+        Manifest m = new Manifest();

Review Comment:
   m --> manifest



##########
src/main/java/org/apache/maven/plugins/jar/FileCollector.java:
##########
@@ -0,0 +1,542 @@
+/*
+ * 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.
+ */
+package org.apache.maven.plugins.jar;
+
+import java.io.IOException;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.PathMatcher;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Deque;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.maven.api.annotations.Nonnull;
+import org.apache.maven.api.annotations.Nullable;
+import org.apache.maven.api.services.PathMatcherFactory;
+
+/**
+ * Dispatch the files from the output directory into the <abbr>JAR</abbr> 
files to create.
+ * Instead of just archiving as-is the content of the output directory, this 
class separates
+ * the following subdirectories to the options listed below:
+ *
+ * <ul>
+ *   <li>The {@code META-INF/MANIFEST.MF} file will be given to the {@code 
--manifest} option.</li>
+ *   <li>Files in the following directories will be given to the {@code 
--release} option:
+ *     <ul>
+ *       <li>{@code META-INF/versions/}</li>
+ *       <li>{@code META-INF/versions-modular/<module>/}</li>
+ *       <li>{@code <module>/META-INF/versions/}</li>
+ *     </ul>
+ *   </li>
+ * </ul>
+ *
+ * The reason for using the {@code --release} and {@code --manifest} options 
instead of adding explicitly
+ * the entries is because the options allow the {@code jar} tool to perform 
additional verifications.
+ * For example, when using the {@code --release} option, {@code jar} verifies 
the <abbr>API</abbr> compatibility.
+ */
+final class FileCollector extends SimpleFileVisitor<Path> {
+    /**
+     * The file to check for deciding whether the <abbr>JAR</abbr> is modular.
+     */
+    static final String MODULE_DESCRIPTOR_FILE_NAME = "module-info.class";
+
+    /**
+     * The {@value} directory.
+     * This is part of <abbr>JAR</abbr> file specification.
+     */
+    private static final String VERSIONS = "versions";
+
+    /**
+     * The {@value} directory.
+     * This is Maven-specific.
+     */
+    private static final String VERSIONS_MODULAR = "versions-modular";
+
+    /**
+     * Context (logger, configuration) in which the <abbr>JAR</abbr> file are 
created.
+     */
+    private final ToolExecutor context;
+
+    /**
+     * Whether to detect multi-release <abbr>JAR</abbr> files.
+     * The default value is {@code true}.
+     *
+     * @see AbstractJarMojo#detectMultiReleaseJar
+     */
+    private final boolean detectMultiReleaseJar;
+
+    /**
+     * The root directory to traverse. It will be used for temporarily moving 
excluded files.
+     */
+    private final Path rootDirectory;
+
+    /**
+     * Combination of includes and excludes path matcher applied on files.
+     */
+    @Nonnull
+    private final PathMatcher fileMatcher;
+
+    /**
+     * Combination of includes and excludes path matcher applied on 
directories.
+     */
+    @Nonnull
+    private final PathMatcher directoryMatcher;
+
+    /**
+     * Files to exclude. These files will be moved to a temporary location
+     * for allowing {@link ToolExecutor} to specify whole directories to the 
{@code jar} tool.
+     * Specifying whole directories is preferable to enumerating the files 
because otherwise,
+     * the generated <abbr>JAR</abbr> file contains only entries for the files 
and is missing
+     * entries for the directories.
+     *
+     * <p>This field is {@code null} if it is not possible to have any 
excluded file
+     * (because there is no include/exclude filters).</p>
+     */
+    @Nullable
+    private final List<Path> excludedFiles;
+
+    /**
+     * Directories to exclude. This field serves the same purpose as {@link 
#excludedFiles},
+     * but where the sources a directories instead of files.
+     */
+    @Nullable
+    private final List<Path> excludedDirectories;
+
+    /**
+     * Files found in the output directory when package hierarchy is used.
+     * At most one of {@code packageHierarchy} and {@link #moduleHierarchy} 
can be non-empty.
+     */
+    @Nonnull
+    private final Archive packageHierarchy;
+
+    /**
+     * Files found in the output directory when module hierarchy is used. Keys 
are module names.
+     * At most one of {@link #packageHierarchy} and {@code moduleHierarchy} 
can be non-empty.
+     */
+    @Nonnull
+    private final Map<String, Archive> moduleHierarchy;
+
+    /**
+     * The current module being archived. This field is updated every times 
that {@code FileCollector}
+     * visits a new module directory.
+     */
+    @Nonnull
+    private Archive currentModule;
+
+    /**
+     * The module and target Java release currently being scanned. This field 
is updated every times that
+     * {@code FileCollector} visits a new module directory or in a new target 
Java release for a given module.
+     */
+    @Nonnull
+    private Archive.FileSet currentFilesToArchive;
+
+    /**
+     * The current target Java release, or {@code null} if none.
+     */
+    @Nullable
+    private Runtime.Version currentTargetVersion;
+
+    /**
+     * Identification of the kinds of directories being traversed.
+     * The length of this list is the depth in the directory hierarchy.
+     * The last element identifies the type of the current directory.
+     */
+    private final Deque<DirectoryRole> directoryRoles;
+
+    /**
+     * Whether to check when a file is the {@code MANIFEST.MF} file.
+     * This is allowed only when scanning the content of a {@code META-INF} 
directory.
+     */
+    private boolean checkForManifest;
+
+    /**
+     * Creates a new file collector.
+     *
+     * @param mojo the <abbr>MOJO</abbr> from which to get the configuration
+     * @param context context (logger, configuration) in which the 
<abbr>JAR</abbr> file are created
+     * @param directory the base directory of the files to archive
+     */
+    FileCollector(AbstractJarMojo mojo, ToolExecutor context, Path directory, 
PathMatcherFactory matcherFactory) {
+        this.context = context;
+        rootDirectory = directory;
+        detectMultiReleaseJar = mojo.detectMultiReleaseJar;
+        directoryRoles = new ArrayDeque<>();
+        fileMatcher = matcherFactory.createPathMatcher(directory, 
mojo.getIncludes(), mojo.getExcludes(), false);
+        directoryMatcher = matcherFactory.deriveDirectoryMatcher(fileMatcher);
+        if (matcherFactory.isIncludesAll(fileMatcher) && 
matcherFactory.isIncludesAll(directoryMatcher)) {
+            excludedFiles = null;
+            excludedDirectories = null;
+        } else {
+            excludedFiles = new ArrayList<>();
+            excludedDirectories = new ArrayList<>();
+        }
+        packageHierarchy = context.newArchive(null, null, directory);
+        moduleHierarchy = new LinkedHashMap<>();
+        resetToPackageHierarchy();
+    }
+
+    /**
+     * Resets this {@code FileCollector} to the state where a package 
hierarchy is presumed.
+     */
+    private void resetToPackageHierarchy() {
+        currentModule = packageHierarchy;
+        currentFilesToArchive = currentModule.baseRelease();
+    }
+
+    /**
+     * Declares that the given directory is the base directory of a module.
+     * For an output generated by {@code javac} from a module source hierarchy,
+     * the directory name is the module name.
+     *
+     * @param directory a {@code "<module>"} or {@code 
"META-INF/versions-modular/<module>"} directory
+     */
+    private void preVisitVersionDirectory(final Path directory) {
+        String moduleName = directory.getFileName().toString();
+        currentModule = moduleHierarchy.computeIfAbsent(
+                moduleName, (name) -> context.newArchive(name, 
currentTargetVersion, directory));
+        currentFilesToArchive = currentModule.newTargetRelease(directory, 
currentTargetVersion);
+    }
+
+    /**
+     * Declares that the given directory is the base directory of a target 
Java version.
+     * The {@code useDirectly} argument tells whether the content of this 
directory will be specified directly
+     * as the content to add in the <abbr>JAR</abbr> file. This argument 
should be {@code false} when there is
+     * another directory level (the module names) to process before to add 
content.
+     *
+     * @param directory a {@code "META-INF/versions/<n>"} or {@code 
"META-INF/versions-modular/<n>"} directory
+     * @param useDirectly whether the directory is {@code 
"META-INF/versions/<n>"}
+     * @return whether to skip the directory because of invalid version number
+     */
+    private boolean preVisitVersionDirectory(final Path directory, final 
boolean useDirectly) {
+        try {
+            currentTargetVersion = 
Runtime.Version.parse(directory.getFileName().toString());
+        } catch (IllegalArgumentException e) {
+            context.warnInvalidVersion(directory, e);
+            return true;
+        }
+        if (useDirectly) {
+            currentFilesToArchive = currentModule.newTargetRelease(directory, 
currentTargetVersion);
+        }
+        return false;
+    }
+
+    /**
+     * Determines if the given directory should be scanned for files to 
archive.
+     * This method may also update {@link #currentFilesToArchive} if it detects
+     * that we are visiting the content of a new module or a new target Java 
release.
+     *
+     * @param directory the directory which will be traversed
+     * @param attributes the directory's basic attributes
+     */
+    @Override
+    @SuppressWarnings("checkstyle:MissingSwitchDefault")
+    public FileVisitResult preVisitDirectory(final Path directory, final 
BasicFileAttributes attributes)
+            throws IOException {
+        DirectoryRole role;
+        if (directoryRoles.isEmpty()) {
+            role = DirectoryRole.ROOT;
+        } else {
+            if (!directoryMatcher.matches(directory)) {
+                excludedDirectories.add(directory); // Cannot be null if 
excluded directories may exist.
+                return FileVisitResult.SKIP_SUBTREE;
+            }
+            checkForManifest = false;
+            role = directoryRoles.getLast();
+            switch (role) {
+                case ROOT:
+                    /*
+                     * Visiting any subdirectory of `target/classes` (or other 
directory to archive).
+                     * We need to handle `META-INF` and modules in a special 
way, and archive the rest.
+                     */
+                    if (directory.endsWith(MetadataFiles.META_INF)) {
+                        role = DirectoryRole.META_INF;
+                        checkForManifest = true;
+                    } else if 
(Files.isRegularFile(directory.resolve(MODULE_DESCRIPTOR_FILE_NAME))) {
+                        role = DirectoryRole.NAMED_MODULE;
+                        preVisitVersionDirectory(directory);
+                    } else {
+                        role = DirectoryRole.RESOURCES;
+                    }
+                    break;
+
+                case META_INF:
+                    /*
+                     * Visiting a subdirectory of `META-INF` or 
`<module>/META-INF`. We will need to handle
+                     * `MANIFEST.MF`, `versions` and `versions-modular` in a 
special way, and archive the rest.
+                     */
+                    if (detectMultiReleaseJar && directory.endsWith(VERSIONS)) 
{
+                        role = DirectoryRole.VERSIONS;
+                    } else if (directory.endsWith(VERSIONS_MODULAR)) {
+                        if (!detectMultiReleaseJar) {
+                            // Used asked for no multi-release JAR.
+                            return FileVisitResult.SKIP_SUBTREE;
+                        }
+                        role = DirectoryRole.VERSIONS_MODULAR;
+                    } else {
+                        role = DirectoryRole.RESOURCES;
+                    }
+                    break;
+
+                case VERSIONS:
+                    /*
+                     * Visiting a `META-INF/versions/<n>/` directory for a 
specific target Java release.
+                     * Can also be a `<module>/META-INF/versions/<n>/` 
directory, even if the latter is not
+                     * the layout generated by Maven Compiler Plugin.
+                     */
+                    if (preVisitVersionDirectory(directory, true)) {
+                        // An error occurred while parsing the version number.
+                        return FileVisitResult.SKIP_SUBTREE;
+                    }
+                    role = DirectoryRole.RESOURCES;
+                    break;
+
+                case VERSIONS_MODULAR:
+                    /*
+                     * Visiting a `META-INF/versions-modular/<n>/` directory 
for a specific target Java release.
+                     * That directory contains all modules for the version.
+                     */
+                    resetToPackageHierarchy(); // No module in particular yet.
+                    if (preVisitVersionDirectory(directory, false)) {
+                        // An error occurred while parsing the version number.
+                        return FileVisitResult.SKIP_SUBTREE;
+                    }
+                    role = DirectoryRole.MODULES;
+                    break;
+
+                case MODULES:
+                    /*
+                     * Visiting a `META-INF/versions-modular/<n>/<module>` 
directory.
+                     */
+                    preVisitVersionDirectory(directory);
+                    role = DirectoryRole.NAMED_MODULE;
+                    break;
+
+                case NAMED_MODULE:
+                    /*
+                     * Visiting a `<module>` or 
`META-INF/versions-modular/<n>/<module>` subdirectory.
+                     * A module could have its own `META-INF` subdirectory, so 
we need to check again.
+                     */
+                    if (directory.endsWith(MetadataFiles.META_INF)) {
+                        role = DirectoryRole.META_INF;
+                        checkForManifest = true;
+                    } else {
+                        role = DirectoryRole.RESOURCES;
+                    }
+                    break;
+            }
+        }
+        /*
+         * Do not move this condition inside the `switch` block because `role` 
may have been modified.
+         * The `role` value is now the role of `directory`, not the role of 
parent directory.
+         */
+        if (role == DirectoryRole.RESOURCES) {
+            currentFilesToArchive.add(directory, attributes, true);
+            if (excludedFiles == null) {
+                /*
+                 * Since we are skipping the whole directory, 
`postVisitDirectory(…)` will not be invoked.
+                 * We must reset `currentFilesToArchive` and 
`currentTargetVersion` by an explicit call.
+                 * This is important mostly after we added a whole 
`META-INF/versions/<n>` directory.
+                 * Otherwise, since directory iteration order is unspecified, 
base files visited afterwards
+                 * would be added to this version's file set instead of the 
base release.
+                 */
+                resetToParentDirectoryState();
+                return FileVisitResult.SKIP_SUBTREE;
+            }
+        }
+        directoryRoles.addLast(role);
+        return FileVisitResult.CONTINUE;
+    }
+
+    /**
+     * Updates the {@code FileCollector} state after we finished scanning the 
contents of a directory.
+     * The fields to update depend on which directory has been visited 
(module, version, etc.).
+     *
+     * @param directory the directory which has been traversed
+     * @param error the error that occurred while traversing the directory, or 
{@code null} if none
+     */
+    @Override
+    public FileVisitResult postVisitDirectory(final Path directory, final 
IOException error) throws IOException {
+        if (error != null) {
+            throw error;
+        }
+        switch (directoryRoles.removeLast()) {
+            case ROOT:
+                break;
+
+            case NAMED_MODULE:
+                // Exited the directory of a single module.
+                resetToPackageHierarchy();
+                break;
+
+            default:
+                resetToParentDirectoryState();
+                break;
+        }
+        return FileVisitResult.CONTINUE;
+    }
+
+    /**
+     * Updates {@code FileCollector} to a state suitable for the parent of the 
directory that we finished to scan.
+     * Contrary to {@link #postVisitDirectory(Path, IOException)}, this method 
expects the last element of
+     * {@link #directoryRoles} to describe the parent directory, not the 
directory that we finished to visit.
+     */
+    @SuppressWarnings("checkstyle:MissingSwitchDefault")
+    private void resetToParentDirectoryState() {
+        switch (directoryRoles.getLast()) {
+            case VERSIONS:
+            case VERSIONS_MODULAR:
+                // Exited the directory for one target Java release.
+                currentFilesToArchive = currentModule.baseRelease();
+                currentTargetVersion = null;
+                break;
+
+            case META_INF:
+                checkForManifest = true;
+                break;
+        }
+    }
+
+    /**
+     * Archives a single file if accepted by the matcher.
+     *
+     * @param file the file
+     * @param attributes the file's basic attributes
+     */
+    @Override
+    public FileVisitResult visitFile(final Path file, final 
BasicFileAttributes attributes) {
+        if (fileMatcher.matches(file)) {
+            if (checkForManifest && file.endsWith(MetadataFiles.MANIFEST) && 
currentModule.setManifest(file, false)) {

Review Comment:
   Instead of tracking checkForManifest can't we figure out from file if we're 
ina  META-INF directory?



##########
src/test/java/org/apache/maven/plugins/jar/ArchiveTest.java:
##########
@@ -0,0 +1,257 @@
+/*
+ * 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.
+ */
+package org.apache.maven.plugins.jar;
+
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.jar.Attributes;
+import java.util.jar.Manifest;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Unit tests for {@link Archive}, focused on the two behaviours that are 
otherwise only
+ * exercised by integration tests whose outcome depends on the (unspecified) 
filesystem
+ * directory-iteration order, and therefore pass on some platforms while 
failing on others.
+ */
+class ArchiveTest {
+
+    /**
+     * Creates an {@code Archive} suitable for a unit test. {@code 
forceCreation = true} skips the
+     * existing-JAR timestamp check, so the (here {@code null}) logger is 
never dereferenced.
+     */
+    private static Archive archive(String moduleName, Runtime.Version version, 
Path directory) {
+        return new Archive(directory.resolve("out.jar"), moduleName, version, 
directory, true, null);
+    }
+
+    /**
+     * Creates a manifest with the main class attribute set to the given value.
+     *
+     * @param value value of the main class attribute
+     * @return a new manifest with the given attribute value
+     */
+    private static Manifest manifestWithMainClass(String value) {
+        Manifest m = new Manifest();
+        Attributes attributes = m.getMainAttributes();
+        attributes.put(Attributes.Name.MANIFEST_VERSION, "1.0");
+        attributes.put(Attributes.Name.MAIN_CLASS, value);
+        return m;
+    }
+
+    /**
+     * Returns the value of the main class attribute.
+     *
+     * @param m the manifest from which to get the value
+     * @return the main class attribute value, or {@code null} if none
+     */
+    private static Object mainClassOf(Manifest m) {
+        return m.getMainAttributes().get(Attributes.Name.MAIN_CLASS);
+    }
+
+    /**
+     * Verifies that {@link Archive#setMainClass(Manifest)} takes ownership 
for a module-qualified
+     * {@code "module/Class"} main class.
+     *
+     * The {@code "foo.bar/"} prefix in this test (the module name) is a Maven 
extension.
+     * The standard <abbr>JAR</abbr> specification accepts only the {@code 
"foo.MainFile"} class name.
+     */
+    @Test
+    void owningModuleClaimsMainClassAndRemovesItFromManifest() {
+        Archive owner = archive("foo.bar", null, Path.of("."));
+        Manifest m = manifestWithMainClass("foo.bar/foo.MainFile");
+        // The owner keeps the main class (emitted via --main-class) ...
+        assertTrue(owner.setMainClass(m));
+        // ... and the raw `module/Class` value is removed from the written 
manifest.
+        assertNull(mainClassOf(m));
+    }
+
+    /**
+     * Verifies that {@link Archive#setMainClass(Manifest)} does <em>not</em> 
take ownership of
+     * a module-qualified {@code "module/Class"} main class when the module 
name does not match.
+     * The {@code "foo.bar/"} prefix in this test (the module name) is a Maven 
extension.
+     * The standard <abbr>JAR</abbr> specification accepts only the {@code 
"foo.MainFile"} class name.
+     */
+    @Test
+    void nonOwningModuleRejectsMainClass() {
+        Archive nonOwner = archive("foo.bar.more", null, Path.of("."));
+        Manifest m = manifestWithMainClass("foo.bar/foo.MainFile");
+        assertFalse(nonOwner.setMainClass(m));
+        assertNull(mainClassOf(m));
+    }
+
+    /**
+     * Tests that which module keeps the main class does not depend on 
processing order.
+     * {@link ToolExecutor} gives each module a <em>copy</em> of the shared 
plugin manifest;
+     * this pins that the owning module (and only it) keeps the main class in 
either order,
+     * and that the shared manifest is never consumed.
+     */
+    @Test
+    void mainClassAssignmentIsIndependentOfModuleOrder() {
+        assertOwnership("foo.bar", "foo.bar.more", true); // owner processed 
first
+        assertOwnership("foo.bar.more", "foo.bar", false); // non-owner 
processed first
+    }
+
+    /**
+     * Helper method for {@link 
#mainClassAssignmentIsIndependentOfModuleOrder()}.
+     * Asserts that {@link Archive#setMainClass(Manifest)} returns {@code true}
+     * for the owner and {@code false} for the other module.
+     *
+     * <p>The {@code "foo.bar/"} prefix in this test (the module name) is a 
Maven extension.
+     * The standard <abbr>JAR</abbr> specification accepts only the {@code 
"foo.MainFile"} class name.
+     * This extension is used by the plugin for identifying in which 
<abbr>JAR</abbr> file to add this
+     * {@code Main-Class} manifest entry.</p>
+     */
+    private static void assertOwnership(String first, String second, boolean 
ownerIsFirst) {
+        final Path path = Path.of(".");
+        final Manifest shared = manifestWithMainClass("foo.bar/foo.MainFile");
+        final Manifest m1 = new Manifest(shared);
+        final Manifest m2 = new Manifest(shared);
+        assertEquals(ownerIsFirst, archive(first, null, 
path).setMainClass(m1));
+        assertEquals(!ownerIsFirst, archive(second, null, 
path).setMainClass(m2));

Review Comment:
   assertNotEquals?



##########
src/main/java/org/apache/maven/plugins/jar/Providers.java:
##########
@@ -22,25 +22,13 @@
 import org.apache.maven.api.di.Named;
 import org.apache.maven.api.di.Provides;
 import org.apache.maven.api.services.ProjectManager;
-import org.codehaus.plexus.archiver.Archiver;
-import org.codehaus.plexus.archiver.jar.JarArchiver;
-import org.codehaus.plexus.archiver.jar.JarToolModularJarArchiver;
 
+/**
+ * For providing instances to fields annotated with {@code @Inject} if the 
MOJO.

Review Comment:
   Ping @gnodet 



##########
src/main/java/org/apache/maven/plugins/jar/ToolExecutor.java:
##########
@@ -0,0 +1,551 @@
+/*
+ * 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.
+ */
+package org.apache.maven.plugins.jar;
+
+import javax.lang.model.SourceVersion;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.jar.Attributes;
+import java.util.jar.Manifest;
+import java.util.spi.ToolProvider;
+
+import org.apache.maven.api.Project;
+import org.apache.maven.api.Type;
+import org.apache.maven.api.plugin.Log;
+import org.apache.maven.api.plugin.MojoException;
+import org.apache.maven.shared.archiver.MavenArchiveConfiguration;
+
+/**
+ * Writer of <abbr>JAR</abbr> files using the information collected by {@link 
FileCollector}.
+ * This class uses the {@code "jar"} tool provided with the <abbr>JDK</abbr>.
+ */
+final class ToolExecutor {
+    /**
+     * First JDK feature version whose {@code jar} tool can run {@code 
--validate} on archives
+     * that contain records. Earlier {@code jar} tools failed with "This 
feature requires ASM8"
+     * (JDK-8282446, fixed by JDK-8282508 in JDK 19 by not backported to a 17u 
or 18u update).
+     *
+     * @see <a 
href="https://bugs.openjdk.org/browse/JDK-8282446";>JDK-8282446</a>
+     * @see <a 
href="https://bugs.openjdk.org/browse/JDK-8282508";>JDK-8282508</a>
+     */
+    private static final int JDK_FIXING_JAR_VALIDATE = 19;
+
+    /**
+     * First JDK feature version whose {@code jar} tool support the {@code 
--date} option.
+     */
+    private static final int JDK_SUPPORT_DATE = 19;
+
+    /**
+     * The {@value} attribute. Its value is automatically generated by {@link 
Manifest},
+     * but may need to be replaced by another value if JDK-independent value 
is desired
+     * for strictly reproducible builds.
+     */
+    private static final String CREATED_BY = "Created-By";
+
+    /**
+     * The Maven project for which to create an archive.
+     */
+    final Project project;
+
+    /**
+     * {@code "jar"} or {@link "test-jar"}.
+     */
+    private final String artifactType;
+
+    /**
+     * The output directory where to write the <abbr>JAR</abbr> file.
+     * This is usually {@code ${baseDir}/target/}.
+     */
+    private final Path outputDirectory;
+
+    /**
+     * The <abbr>JAR</abbr> file name when package hierarchy is used.
+     * This is usually a file placed in the {@link 
ToolExecutor#outputDirectory} directory.
+     */
+    private final String finalName;
+
+    /**
+     * The classifier (e.g. "test"), or {@code null} if none.
+     */
+    private final String classifier;
+
+    /**
+     * Whether to validate the <abbr>JAR</abbr> file after its creation.
+     * If {@code null}, a value will be determined automatically based on 
heuristic rules.
+     */
+    private final Boolean validate;
+
+    /**
+     * The tool to use for creating the <abbr>JAR</abbr> files.
+     */
+    private final ToolProvider tool;
+
+    /**
+     * Where to send messages emitted by the "jar" tool.
+     */
+    private final PrintWriter messageWriter;
+
+    /**
+     * Where to send error messages emitted by the "jar" tool.
+     */
+    private final PrintWriter errorWriter;
+
+    /**
+     * Where the messages sent to {@link #messageWriter} are stored.
+     */
+    private final StringBuffer messages;
+
+    /**
+     * Where the messages sent to {@link #errorWriter} are stored.
+     */
+    private final StringBuffer errors;
+
+    /**
+     * A buffer for the arguments given to the "jar" tool, reused for each 
module.
+     * Each element of the list must be instances of either {@link String} or 
{@link Path}.
+     */
+    private final List<Object> arguments;
+
+    /**
+     * The paths to the created archive files.
+     * Map keys are module names or {@code null} if the project does not use 
module hierarchy.
+     * Values are (<var>type</var>, <var>path</var>) pairs associated with 
each module where
+     * <var>type</var> is {@code "pom"}, {@code "jar"} or {@code "test-jar"} 
and <var>path</var>
+     * is the path to the <abbr>POM</abbr> or <abbr>JAR</abbr> file.
+     */
+    private final Map<String, Map<String, Path>> result;
+
+    /**
+     * Mapper from Maven dependencies to Java modules, or {@code null} if the 
project does not use module hierarchy.
+     * This mapper is created only once for a Maven project and reused for 
each Java module to archive.
+     *
+     * <p>This field is not used directly by {@code ToolExecutor}. It is 
defined in this class for transferring
+     * this information from {@link AbstractJarMojo} to {@link 
PomDerivation.ForModule}.
+     * This is an internal mechanism that should not be public or 
protected.</p>
+     */
+    PomDerivation pomDerivation;
+
+    /**
+     * Manifest to merge with the manifest found in the files to archive.
+     * This is a manifest built from the {@code <archive>} plugin 
configuration.
+     * Can be {@code null} if there is noting to add to the existing manifests.
+     */
+    private final Manifest manifestFromPlugin;
+
+    /**
+     * The file from which {@link #manifestFromPlugin} has been read, or 
{@code null} if none.
+     * If non-null, reading that file must produce the same manifest as {@link 
#manifestFromPlugin}.
+     * It implies that this field must be {@code null} if {@link 
#manifestFromPlugin} is the result
+     * of merging elements specified in {@code <archive>} with a file 
specified in the plugin configuration.
+     */
+    private final Path manifestFile;
+
+    /**
+     * The archive configuration to use.
+     */
+    private final MavenArchiveConfiguration archiveConfiguration;
+
+    /**
+     * The timestamp in ISO-8601 extended offset date-time, or {@code null} if 
none.
+     * If user provided a value in seconds, it must have been converted to 
ISO-8601.
+     * This is used for reproducible builds.
+     */
+    private final String outputTimestamp;
+
+    /**
+     * Whether to force to build new <abbr>JAR</abbr> files even if none of 
the contents appear to have changed.
+     */
+    private final boolean forceCreation;
+
+    /**
+     * Where to send informative or error messages.
+     */
+    private final Log logger;
+
+    /**
+     * Creates a new writer.
+     *
+     * @param mojo the <abbr>MOJO</abbr> from which to get the configuration
+     * @param manifest manifest built from plugin configuration, or {@code 
null} if none
+     * @param archive the archive configuration
+     * @throws IOException if an error occurred while reading the manifest file
+     */
+    ToolExecutor(AbstractJarMojo mojo, Manifest manifest, 
MavenArchiveConfiguration archive) throws IOException {
+        project = mojo.project;
+        artifactType = mojo.getType();
+        outputDirectory = mojo.getOutputDirectory();
+        classifier = AbstractJarMojo.nullIfAbsent(mojo.getClassifier());
+        finalName =
+                (mojo.finalName != null) ? mojo.finalName : 
project.getBuild().getFinalName();
+        forceCreation = mojo.forceCreation;
+        outputTimestamp = mojo.getOutputTimestamp();
+        validate = mojo.getValidate();
+        logger = mojo.log;
+        tool = mojo.getJarTool();
+
+        var buffer = new StringWriter();
+        messages = buffer.getBuffer();
+        messageWriter = new PrintWriter(buffer);
+
+        buffer = new StringWriter();
+        errors = buffer.getBuffer();
+        errorWriter = new PrintWriter(buffer);
+
+        arguments = new ArrayList<>();
+        result = new LinkedHashMap<>();
+        archiveConfiguration = archive;
+
+        Path file = archive.getManifestFile();
+        if (file != null) {
+            try (InputStream in = Files.newInputStream(file)) {
+                // No need to wrap in `BufferedInputStream`.
+                if (manifest != null) {
+                    manifest.read(in);
+                    file = null; // Because the manifest is the result of a 
merge.
+                } else {
+                    manifest = new Manifest(in);
+                }
+            }
+        }
+        if (manifest != null) {
+            final Attributes mainAttributes = manifest.getMainAttributes();
+            if (mojo.detectMultiReleaseJar) {
+                mainAttributes.remove(Attributes.Name.MULTI_RELEASE);
+            }
+            if (isReproducible()) {
+                // If a "Created-By" attribute was generated by Maven 
Archiver, it is assumed JDK-independent.
+                // Otherwise, substitute by another value which does not 
depend on the JDK.
+                if (mainAttributes.getValue(CREATED_BY) == null) {
+                    mainAttributes.putValue(CREATED_BY, createdBy());
+                }
+            } else {
+                // If reproducible build was not requested, let the tool 
declares itself.
+                // This is a workaround until we port Maven archiver to this 
JAR plugin.
+                mainAttributes.remove(CREATED_BY);
+            }
+        }
+        manifestFromPlugin = manifest;
+        manifestFile = file;
+    }
+
+    /**
+     * Whether reproducible build was requested.
+     * In current version, the output time stamp is used as a sentinel value.
+     */
+    public boolean isReproducible() {
+        return outputTimestamp != null;
+    }
+
+    /**
+     * Returns the default {@value #CREATED_BY} value to use when strictly 
reproducible builds is requested.
+     * This value is ignored if the Maven Archiver or if user's configuration 
provided themselves a value.
+     *
+     * @return a JDK-independent {@value #CREATED_BY} value
+     */
+    private static String createdBy() {
+        String value = "Maven JAR Plugin";
+        String version = 
ToolExecutor.class.getPackage().getImplementationVersion();
+        if (version != null) {
+            value = value + ' ' + version;
+        }
+        return value;
+    }
+
+    /**
+     * Creates an initially empty archive for <abbr>JAR</abbr> file to 
generate.
+     * This method does not create the <abbr>JAR</abbr> file immediately,
+     * but collect information for creating the file later.
+     *
+     * @param  moduleName the module name if using module hierarchy, or {@code 
null} if using package hierarchy
+     * @param  version the target Java release, or {@code null} for the base 
version
+     * @param  directory the directory of the classes targeting the base Java 
release
+     */
+    Archive newArchive(final String moduleName, final Runtime.Version version, 
final Path directory) {
+        var sb = new StringBuilder(60);
+        if (moduleName != null) {
+            sb.append(moduleName).append('-').append(project.getVersion());
+        } else {
+            sb.append(finalName);
+        }
+        if (classifier != null) {
+            sb.append('-').append(classifier);
+        }
+        String filename = sb.append(".jar").toString();
+        return new Archive(outputDirectory.resolve(filename), moduleName, 
version, directory, forceCreation, logger);
+    }
+
+    /**
+     * Writes all <abbr>JAR</abbr> files, together with their derived 
<abbr>POM</abbr> files if applicable.
+     * The derived <abbr>POM</abbr> files are the intersections of the project 
<abbr>POM</abbr> with the
+     * content of {@code module-info.class} files.
+     *
+     * <h4>Prerequisites</h4>
+     * The {@link FileCollector#prune(boolean)} method should have been 
invoked once before to invoke this method.
+     *
+     * @param files the result of scanning the build directory for listing the 
files or directories to archive
+     * @return the paths to the created archive files
+     * @throws MojoException if an error occurred during the execution of the 
"jar" tool
+     * @throws IOException if an error occurred while reading or writing a 
manifest file
+     */
+    @SuppressWarnings("ReturnOfCollectionOrArrayField")
+    public Map<String, Map<String, Path>> writeAllJARs(final FileCollector 
files) throws IOException {
+        Path ignored = files.handleOrphanFiles();
+        Path parentPOM = files.writeAllJARs(this);
+        if (ignored != null) {
+            logger.warn("Some files in \"" + relativize(outputDirectory, 
ignored)
+                    + "\" were ignored because they belong to no module.");
+        }
+        if (parentPOM != null) {
+            if (result.put(null, Map.of(Type.POM, parentPOM)) != null) {
+                throw new MojoException("Internal error."); // Should never 
happen.
+            }
+        }
+        return result;
+    }
+
+    /**
+     * Creates the <abbr>JAR</abbr> files for the specified set of files.
+     * If the operation fails, an error message may be available in the {@link 
#errors} buffer.
+     *
+     * @param files the result of scanning the build directory for listing the 
files or directories to archive
+     * @throws MojoException if an error occurred during the execution of the 
"jar" tool
+     * @throws IOException if an error occurred while reading or writing a 
manifest file
+     */
+    void writeSingleJAR(final FileCollector files, final Archive archive) 
throws IOException {
+        final Path relativePath = relativize(project.getRootDirectory(), 
archive.jarFile);
+        if (archive.isUpToDateJAR()) {
+            logger.info("Keep up-to-date JAR: \"" + relativePath + "\".");
+            archive.saveArtifactPaths(artifactType, result);
+            return;
+        }
+        logger.info("Building JAR: \"" + relativePath + "\".");
+        /*
+         * If `MANIFEST.MF` entries were specified by JAR plugin configuration,
+         * merge those entries with the content of `MANIFEST.MF` file found in
+         * the files to archive.
+         */
+        boolean writeTemporaryManifest = (manifestFromPlugin != null && 
manifestFile == null); // Check <archive>.
+        Manifest manifest = archive.mergeManifest(manifestFile, 
manifestFromPlugin);
+        if (manifest != manifestFromPlugin) {
+            writeTemporaryManifest |= (manifestFromPlugin != null); // Check 
if a merge of two manifests.
+        } else if (manifest != null) {
+            /*
+             * `setMainClass` below removes the Main-Class attribute, and 
`manifestFromPlugin` is
+             * shared across every module of a module hierarchy. Work on a 
per-module copy so that
+             * the (unspecified) directory iteration order does not decide 
which module keeps the
+             * main class: otherwise a non-owning module processed first 
consumes the attribute and
+             * the owning module never receives it.
+             */
+            manifest = new Manifest(manifest);
+        }
+        writeTemporaryManifest |= archive.setMainClass(manifest);
+        if (manifest != null) {
+            String name = 
manifest.getMainAttributes().getValue("Automatic-Module-Name");
+            if (name != null && !SourceVersion.isName(name)) {
+                throw new MojoException("Invalid automatic module name: \"" + 
name + "\".");
+            }
+        }
+        /*
+         * Creates temporary files for META-INF (if the existing file cannot 
be used directly)
+         * and for the Maven metadata (if requested). The temporary files are 
in the `target`
+         * directory and will be deleted, unless the build fails or is run in 
verbose mode.
+         */
+        try (MetadataFiles metadata = new MetadataFiles(project, 
outputDirectory)) {
+            if (writeTemporaryManifest) {
+                archive.setManifest(metadata.addManifest(manifest), true);
+            }
+            if (archive.moduleName != null) {
+                metadata.deriveModulePOM(this, archive, manifest);
+            }
+            if (archiveConfiguration.isAddMavenDescriptor()) {
+                archive.mavenFiles = metadata.addPOM(archiveConfiguration, 
isReproducible());
+            }
+            /*
+             * Prepare the arguments to send to the `jar` tool and log a 
message.
+             */
+            arguments.add("--create");
+            if (!archiveConfiguration.isCompress()) {
+                arguments.add("--no-compress");
+            }
+            if (outputTimestamp != null) {
+                if (Runtime.version().feature() >= JDK_SUPPORT_DATE) {

Review Comment:
   maybe OK but the variable name needs to be clearer



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to