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


##########
src/main/java/org/apache/maven/plugins/jar/FileCollector.java:
##########
@@ -0,0 +1,496 @@
+/*
+ * 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.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 in 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 options is that they 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;
+
+    /**
+     * 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;
+
+    /**
+     * Whether the matchers accept all files and there is no need to sort the 
files.
+     * In such case, we can declare whole directories to the {@code jar} tool 
instead
+     * of scanning the directory tree ourselves.
+     */
+    private final boolean addDirectories;
+
+    /**
+     * 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}
+     * enters in 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} enters in 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;
+        detectMultiReleaseJar = mojo.detectMultiReleaseJar;
+        directoryRoles = new ArrayDeque<>();
+        fileMatcher = matcherFactory.createPathMatcher(directory, 
mojo.getIncludes(), mojo.getExcludes(), false);
+        directoryMatcher = matcherFactory.deriveDirectoryMatcher(fileMatcher);
+        addDirectories = !context.isReproducible()
+                && matcherFactory.isIncludesAll(fileMatcher)
+                && matcherFactory.isIncludesAll(directoryMatcher);
+        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 guaranteed to be the module name.
+     *
+     * @param directory a {@code "<module>"} or {@code 
"META-INF/versions-modular/<module>"} directory
+     */
+    private void enterModuleDirectory(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 enterVersionDirectory(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 entering in 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)) {
+                return FileVisitResult.SKIP_SUBTREE;
+            }
+            checkForManifest = false;
+            role = directoryRoles.getLast();
+            switch (role) {
+                case ROOT:
+                    /*
+                     * Entering in 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;
+                        enterModuleDirectory(directory);
+                    } else {
+                        role = DirectoryRole.RESOURCES;
+                    }
+                    break;
+
+                case META_INF:
+                    /*
+                     * Entering in 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:
+                    /*
+                     * Entering in a `META-INF/versions/<n>/` directory for a 
specific target Java release.
+                     * May also be a `<module>/META-INF/versions/<n>/` 
directory, even if the latter is not
+                     * the layout generated by Maven Compiler Plugin.
+                     */
+                    if (enterVersionDirectory(directory, true)) {
+                        // An error occurred while parsing the version number.
+                        return FileVisitResult.SKIP_SUBTREE;
+                    }
+                    role = DirectoryRole.RESOURCES;
+                    break;
+
+                case VERSIONS_MODULAR:
+                    /*
+                     * Entering in 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 (enterVersionDirectory(directory, false)) {
+                        // An error occurred while parsing the version number.
+                        return FileVisitResult.SKIP_SUBTREE;
+                    }
+                    role = DirectoryRole.MODULES;
+                    break;
+
+                case MODULES:
+                    /*
+                     * Entering in a `META-INF/versions-modular/<n>/<module>` 
directory.
+                     */
+                    enterModuleDirectory(directory);
+                    role = DirectoryRole.NAMED_MODULE;
+                    break;
+
+                case NAMED_MODULE:
+                    /*
+                     * Entering in 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 anymore the 
role of parent directory.
+         */
+        if (addDirectories && role == DirectoryRole.RESOURCES) {
+            currentFilesToArchive.add(directory, attributes, true);
+            /*
+             * 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 base files visited afterwards (directory iteration 
order is unspecified)
+             * would be added to this version's file set instead of the base 
release.
+             */
+            resetToParentDirectoryState();
+            return FileVisitResult.SKIP_SUBTREE;
+        } else {
+            directoryRoles.addLast(role);
+            return FileVisitResult.CONTINUE;
+        }
+    }
+
+    /**
+     * Updates the {@code FileCollector} state after we finished to scan the 
content of a directory.
+     * The fields to update depend on which directory has been visited 
(module, version, <i>etc.</i>).

Review Comment:
   Done



-- 
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