From e9e2d1782409a0cb490c5862f4aee4c62dfb3075 Mon Sep 17 00:00:00 2001
From: Martin Desruisseaux
Date: Sun, 12 Oct 2025 17:39:27 +0200
Subject: [PATCH 1/5] Replace `org.apache.maven.shared.model.fileset` by a
temporary copy of the `PathSelector` class of Maven core. This is for the
transition to Maven 4.0.0 API. As a side effect, this change may improve
performances because the new implementation based on `FileVisitor` makes some
efforts for avoiding to scan unnecessary directories.
---
pom.xml | 5 -
.../verify.bsh | 2 +-
.../maven/plugins/jar/AbstractJarMojo.java | 95 ++--
.../apache/maven/plugins/jar/Archiver.java | 198 +++++++
.../maven/plugins/jar/PathSelector.java | 538 ++++++++++++++++++
5 files changed, 778 insertions(+), 60 deletions(-)
create mode 100644 src/main/java/org/apache/maven/plugins/jar/Archiver.java
create mode 100644 src/main/java/org/apache/maven/plugins/jar/PathSelector.java
diff --git a/pom.xml b/pom.xml
index 9016cc4a..1986349d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -144,11 +144,6 @@
${mavenVersion}provided
-
- org.apache.maven.shared
- file-management
- ${mavenFileManagementVersion}
- org.apache.maven.sharedmaven-archiver
diff --git a/src/it/MJAR-260-invalid-automatic-module-name/verify.bsh b/src/it/MJAR-260-invalid-automatic-module-name/verify.bsh
index 5b9d457f..452aed0e 100644
--- a/src/it/MJAR-260-invalid-automatic-module-name/verify.bsh
+++ b/src/it/MJAR-260-invalid-automatic-module-name/verify.bsh
@@ -45,7 +45,7 @@ try
String[] snippets = new String[] {
"[INFO] BUILD FAILURE",
"[ERROR] Failed to execute goal org.apache.maven.plugins:maven-jar-plugin",
- "Caused by: org.apache.maven.api.plugin.MojoException: Error assembling JAR",
+ "Caused by: org.apache.maven.api.plugin.MojoException: Error while assembling the JAR file.",
"Caused by: org.codehaus.plexus.archiver.jar.ManifestException: Invalid automatic module name: 'in-valid.name.with.new.keyword'"
};
diff --git a/src/main/java/org/apache/maven/plugins/jar/AbstractJarMojo.java b/src/main/java/org/apache/maven/plugins/jar/AbstractJarMojo.java
index 058ac058..ef167879 100644
--- a/src/main/java/org/apache/maven/plugins/jar/AbstractJarMojo.java
+++ b/src/main/java/org/apache/maven/plugins/jar/AbstractJarMojo.java
@@ -18,11 +18,11 @@
*/
package org.apache.maven.plugins.jar;
-import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
+import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.jar.Attributes;
@@ -39,8 +39,6 @@
import org.apache.maven.shared.archiver.MavenArchiveConfiguration;
import org.apache.maven.shared.archiver.MavenArchiver;
import org.apache.maven.shared.archiver.MavenArchiverException;
-import org.apache.maven.shared.model.fileset.FileSet;
-import org.apache.maven.shared.model.fileset.util.FileSetManager;
import org.codehaus.plexus.archiver.Archiver;
import org.codehaus.plexus.archiver.jar.JarArchiver;
@@ -55,8 +53,6 @@ public abstract class AbstractJarMojo implements org.apache.maven.api.plugin.Moj
private static final String[] DEFAULT_INCLUDES = new String[] {"**/**"};
- private static final String MODULE_DESCRIPTOR_FILE_NAME = "module-info.class";
-
/**
* List of files to include. Specified as fileset patterns which are relative to the input directory whose contents
* is being packaged into the JAR.
@@ -222,43 +218,36 @@ protected Path getJarFile(Path basedir, String resultFinalName, String classifie
}
/**
- * Generates the JAR.
+ * Generates the JAR file.
*
* @return the path to the created archive file
- * @throws MojoException in case of an error
+ * @throws IOException in case of an error while reading a file or writing in the JAR file
*/
- public Path createArchive() throws MojoException {
- Path jarFile = getJarFile(outputDirectory, finalName, getClassifier());
-
- FileSetManager fileSetManager = new FileSetManager();
- FileSet jarContentFileSet = new FileSet();
- jarContentFileSet.setDirectory(getClassesDirectory().toAbsolutePath().toString());
- jarContentFileSet.setIncludes(Arrays.asList(getIncludes()));
- jarContentFileSet.setExcludes(Arrays.asList(getExcludes()));
-
- String[] includedFiles = fileSetManager.getIncludedFiles(jarContentFileSet);
-
- if (detectMultiReleaseJar
- && Arrays.stream(includedFiles)
- .anyMatch(
- p -> p.startsWith("META-INF" + File.separatorChar + "versions" + File.separatorChar))) {
- getLog().debug("Adding 'Multi-Release: true' manifest entry.");
- archive.addManifestEntry(Attributes.Name.MULTI_RELEASE.toString(), "true");
+ @SuppressWarnings("checkstyle:UnusedLocalVariable") // Checkstyle bug: does not detect `includedFiles` usage.
+ public Path createArchive() throws IOException {
+ String archiverName = "jar";
+ final Path jarFile = getJarFile(outputDirectory, finalName, getClassifier());
+ final Path classesDirectory = getClassesDirectory();
+ if (Files.exists(classesDirectory)) {
+ var includedFiles = new org.apache.maven.plugins.jar.Archiver(
+ classesDirectory,
+ (includes != null) ? Arrays.asList(includes) : List.of(),
+ (excludes != null && excludes.length > 0)
+ ? Arrays.asList(excludes)
+ : List.of(AbstractJarMojo.DEFAULT_EXCLUDES));
+
+ var scanner = includedFiles.new VersionScanner(detectMultiReleaseJar);
+ if (Files.exists(classesDirectory)) {
+ Files.walkFileTree(classesDirectory, scanner);
+ }
+ if (detectMultiReleaseJar && scanner.detectedMultiReleaseJAR) {
+ getLog().debug("Adding 'Multi-Release: true' manifest entry.");
+ archive.addManifestEntry(Attributes.Name.MULTI_RELEASE.toString(), "true");
+ }
+ if (scanner.containsModuleDescriptor) {
+ archiverName = "mjar";
+ }
}
-
- // May give false positives if the files is named as module descriptor
- // but is not in the root of the archive or in the versioned area
- // (and hence not actually a module descriptor).
- // That is fine since the modular Jar archiver will gracefully
- // handle such case.
- // And also such case is unlikely to happen as file ending
- // with "module-info.class" is unlikely to be included in Jar file
- // unless it is a module descriptor.
- boolean containsModuleDescriptor =
- Arrays.stream(includedFiles).anyMatch(p -> p.endsWith(MODULE_DESCRIPTOR_FILE_NAME));
-
- String archiverName = containsModuleDescriptor ? "mjar" : "jar";
-
MavenArchiver archiver = new MavenArchiver();
archiver.setCreatedBy("Maven JAR Plugin", "org.apache.maven.plugins", "maven-jar-plugin");
archiver.setArchiver((JarArchiver) archivers.get(archiverName));
@@ -269,23 +258,16 @@ public Path createArchive() throws MojoException {
archive.setForced(forceCreation);
- try {
- Path contentDirectory = getClassesDirectory();
- if (!Files.exists(contentDirectory)) {
- if (!forceCreation) {
- getLog().warn("JAR will be empty - no content was marked for inclusion!");
- }
- } else {
- archiver.getArchiver().addDirectory(contentDirectory.toFile(), getIncludes(), getExcludes());
+ Path contentDirectory = getClassesDirectory();
+ if (!Files.exists(contentDirectory)) {
+ if (!forceCreation) {
+ getLog().warn("JAR will be empty - no content was marked for inclusion!");
}
-
- archiver.createArchive(session, project, archive);
-
- return jarFile;
- } catch (Exception e) {
- // TODO: improve error handling
- throw new MojoException("Error assembling JAR", e);
+ } else {
+ archiver.getArchiver().addDirectory(contentDirectory.toFile(), getIncludes(), getExcludes());
}
+ archiver.createArchive(session, project, archive);
+ return jarFile;
}
/**
@@ -298,7 +280,12 @@ public void execute() throws MojoException {
if (skipIfEmpty && isEmpty(getClassesDirectory())) {
getLog().info(String.format("Skipping packaging of the %s.", getType()));
} else {
- Path jarFile = createArchive();
+ Path jarFile;
+ try {
+ jarFile = createArchive();
+ } catch (Exception e) {
+ throw new MojoException("Error while assembling the JAR file.", e);
+ }
ProducedArtifact artifact;
String classifier = getClassifier();
if (attach) {
diff --git a/src/main/java/org/apache/maven/plugins/jar/Archiver.java b/src/main/java/org/apache/maven/plugins/jar/Archiver.java
new file mode 100644
index 00000000..d44dc415
--- /dev/null
+++ b/src/main/java/org/apache/maven/plugins/jar/Archiver.java
@@ -0,0 +1,198 @@
+/*
+ * 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.Path;
+import java.nio.file.PathMatcher;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.Collection;
+
+import org.apache.maven.api.annotations.Nonnull;
+
+/**
+ * Creates the JAR file.
+ *
+ * TODO: this is a work in progress.
+ */
+final class Archiver extends SimpleFileVisitor {
+ /**
+ * Combination of includes and excludes path matchers applied on files.
+ */
+ @Nonnull
+ private final PathMatcher fileMatcher;
+
+ /**
+ * Combination of includes and excludes path matchers applied on directories.
+ */
+ @Nonnull
+ private final PathMatcher directoryMatcher;
+
+ /**
+ * Creates a new archiver.
+ *
+ * @param directory the base directory of the files to archive
+ * @param includes the patterns of the files to include, or null or empty for including all files
+ * @param excludes the patterns of the files to exclude, or null or empty for no exclusion
+ */
+ Archiver(Path directory, Collection includes, Collection excludes) {
+ fileMatcher = PathSelector.of(directory, includes, excludes);
+ if (fileMatcher instanceof PathSelector ps) {
+ if (ps.canFilterDirectories()) {
+ directoryMatcher = (path) -> ps.couldHoldSelected(path);
+ return;
+ }
+ }
+ directoryMatcher = PathSelector.INCLUDES_ALL;
+ }
+
+ /**
+ * A scanner of {@code module-info.class} file and {@code META-INF/versions/*} directories.
+ * This is used only when we need to auto-detect whether the JAR is modular or multiè-release.
+ * This is not needed anymore for projects using the Maven 4 {@code } elements.
+ */
+ final class VersionScanner extends SimpleFileVisitor {
+ /**
+ * The file to check for deciding whether the JAR is a modular JAR.
+ */
+ private static final String MODULE_DESCRIPTOR_FILE_NAME = "module-info.class";
+
+ /**
+ * The directory level in the {@code ./META-INF/versions/###/} path.
+ * The root directory is at level 0 before we enter in that directory.
+ * After the execution of {@code preVisitDirectory} (i.e., at the time of visiting files), values are:
+ *
+ *
+ *
for any file in the {@code ./} root classes directory.
+ *
for any file in the {@code ./META-INF/} directory.
+ *
for any file in the {@code ./META-INF/versions/} directory (i.e., any version number).
+ *
for any file in the {@code ./META-INF/versions/###/} directory (i.e., any versioned file)
+ *
+ */
+ private int level;
+
+ /**
+ * First level of versioned files in a {@code ./META-INF/versions/###/} directory.
+ */
+ private static final int LEVEL_OF_VERSIONED_FILES = 4;
+
+ /**
+ * Whether a {@code META-INF/versions/} directory has been found.
+ * The value of this flag is invalid if this scanner was constructed
+ * with a {@code detectMultiReleaseJar} argument value of {@code true}.
+ */
+ boolean detectedMultiReleaseJAR;
+
+ /**
+ * Whether a {@code module-info.class} file has been found.
+ */
+ boolean containsModuleDescriptor;
+
+ /**
+ * Creates a scanner with all flags initially {@code false}.
+ *
+ * @param detectMultiReleaseJar whether to enable the detection of multi-release JAR
+ */
+ VersionScanner(boolean detectMultiReleaseJar) {
+ detectedMultiReleaseJAR = !detectMultiReleaseJar; // Not actually true, but will cause faster stop.
+ }
+
+ /**
+ * Returns {@code true} if the given directory is one of the parts of {@code ./META-INF/versions/*}.
+ * The {@code .} directory is at level 0, {@code META-INF} at level 1, {@code versions} at level 2,
+ * and one of the versions at level 3. Files at level 4 are versioned class files.
+ */
+ @Override
+ public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
+ if (detectedMultiReleaseJAR && level >= LEVEL_OF_VERSIONED_FILES) {
+ // When searching only for `module-info.class`, we do not need to go further than that level.
+ return (level > LEVEL_OF_VERSIONED_FILES)
+ ? FileVisitResult.SKIP_SIBLINGS
+ : FileVisitResult.SKIP_SUBTREE;
+ }
+ if (directoryMatcher.matches(dir)) {
+ String expected =
+ switch (level) {
+ case 1 -> "META-INF";
+ case 2 -> "versions";
+ default -> null;
+ };
+ if (expected == null || dir.endsWith(expected)) {
+ level++;
+ return FileVisitResult.CONTINUE;
+ }
+ }
+ return FileVisitResult.SKIP_SUBTREE;
+ }
+
+ /**
+ * Decrements our count of directory levels after visiting a directory.
+ */
+ @Override
+ public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
+ level--;
+ return super.postVisitDirectory(dir, exc);
+ }
+
+ /**
+ * Updates the flags for the given files. This method terminates the
+ * scanning if it detects that there is no new information to collect.
+ */
+ @Override
+ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
+ if (fileMatcher.matches(file)) {
+ if (level == 1 || level == LEVEL_OF_VERSIONED_FILES) {
+ // Root directory or a `META-INF/versions/###/` directory.
+ containsModuleDescriptor |= file.endsWith(MODULE_DESCRIPTOR_FILE_NAME);
+ }
+ detectedMultiReleaseJAR |= (level >= LEVEL_OF_VERSIONED_FILES);
+ if (detectedMultiReleaseJAR) {
+ if (containsModuleDescriptor) {
+ return FileVisitResult.TERMINATE;
+ }
+ if (level > LEVEL_OF_VERSIONED_FILES) {
+ return FileVisitResult.SKIP_SIBLINGS;
+ }
+ }
+ }
+ return FileVisitResult.CONTINUE;
+ }
+ }
+
+ /**
+ * Determines if the given directory should be scanned for files to archive.
+ */
+ @Override
+ public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
+ return directoryMatcher.matches(dir) ? FileVisitResult.CONTINUE : FileVisitResult.SKIP_SUBTREE;
+ }
+
+ /**
+ * Archives a file in a directory.
+ */
+ @Override
+ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
+ if (fileMatcher.matches(file)) {
+ // TODO
+ }
+ return FileVisitResult.CONTINUE;
+ }
+}
diff --git a/src/main/java/org/apache/maven/plugins/jar/PathSelector.java b/src/main/java/org/apache/maven/plugins/jar/PathSelector.java
new file mode 100644
index 00000000..9eba96b7
--- /dev/null
+++ b/src/main/java/org/apache/maven/plugins/jar/PathSelector.java
@@ -0,0 +1,538 @@
+/*
+ * 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.File;
+import java.nio.file.FileSystem;
+import java.nio.file.Path;
+import java.nio.file.PathMatcher;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+import org.apache.maven.api.annotations.Nonnull;
+
+/**
+ * Temporary copy of the {@code PathSelector} class in Maven core.
+ * This class will be removed after the release of the Maven core version after 4.0.0-rc-4.
+ * It is used for the transition from {@code org.apache.maven.shared} to Maven 4.0.0 core.
+ */
+final class PathSelector implements PathMatcher {
+ /**
+ * Maximum number of characters of the prefix before {@code ':'} for handling as a Maven syntax.
+ */
+ private static final int MAVEN_SYNTAX_THRESHOLD = 1;
+
+ /**
+ * The default syntax to use if none was specified. Note that when this default syntax is applied,
+ * the user-provided pattern get some changes as documented in class Javadoc.
+ */
+ private static final String DEFAULT_SYNTAX = "glob:";
+
+ /**
+ * Characters having a special meaning in the glob syntax.
+ *
+ * @see FileSystem#getPathMatcher(String)
+ */
+ private static final String SPECIAL_CHARACTERS = "*?[]{}\\";
+
+ /**
+ * A path matcher which accepts all files.
+ *
+ * @see #simplify()
+ */
+ static final PathMatcher INCLUDES_ALL = (path) -> true;
+
+ /**
+ * String representations of the normalized include filters.
+ * Each pattern shall be prefixed by its syntax, which is {@value #DEFAULT_SYNTAX} by default.
+ * An empty array means to include all files.
+ *
+ * @see #toString()
+ */
+ private final String[] includePatterns;
+
+ /**
+ * String representations of the normalized exclude filters.
+ * Each pattern shall be prefixed by its syntax. If no syntax is specified,
+ * the default is a Maven 3 syntax similar, but not identical, to {@value #DEFAULT_SYNTAX}.
+ * This array may be longer or shorter than the user-supplied excludes, depending on whether
+ * default excludes have been added and whether some unnecessary excludes have been omitted.
+ *
+ * @see #toString()
+ */
+ private final String[] excludePatterns;
+
+ /**
+ * The matcher for includes. The length of this array is equal to {@link #includePatterns} array length.
+ * An empty array means to include all files.
+ */
+ private final PathMatcher[] includes;
+
+ /**
+ * The matcher for excludes. The length of this array is equal to {@link #excludePatterns} array length.
+ */
+ private final PathMatcher[] excludes;
+
+ /**
+ * The matcher for all directories to include. This array includes the parents of all those directories,
+ * because they need to be accepted before we can walk to the sub-directories.
+ * This is an optimization for skipping whole directories when possible.
+ * An empty array means to include all directories.
+ */
+ private final PathMatcher[] dirIncludes;
+
+ /**
+ * The matcher for directories to exclude. This array does not include the parent directories,
+ * because they may contain other sub-trees that need to be included.
+ * This is an optimization for skipping whole directories when possible.
+ */
+ private final PathMatcher[] dirExcludes;
+
+ /**
+ * The base directory. All files will be relativized to that directory before to be matched.
+ */
+ private final Path baseDirectory;
+
+ /**
+ * Whether paths must be relativized before being given to a matcher. If {@code true}, then every paths
+ * will be made relative to {@link #baseDirectory} for allowing patterns like {@code "foo/bar/*.java"}
+ * to work. As a slight optimization, we can skip this step if all patterns start with {@code "**"}.
+ */
+ private final boolean needRelativize;
+
+ /**
+ * Creates a new selector from the given includes and excludes.
+ *
+ * @param directory the base directory of the files to filter
+ * @param includes the patterns of the files to include, or null or empty for including all files
+ * @param excludes the patterns of the files to exclude, or null or empty for no exclusion
+ * @throws NullPointerException if directory is null
+ */
+ private PathSelector(@Nonnull Path directory, Collection includes, Collection excludes) {
+ baseDirectory = Objects.requireNonNull(directory, "directory cannot be null");
+ includePatterns = normalizePatterns(includes, false);
+ excludePatterns = normalizePatterns(effectiveExcludes(excludes, includePatterns), true);
+ FileSystem fileSystem = baseDirectory.getFileSystem();
+ this.includes = matchers(fileSystem, includePatterns);
+ this.excludes = matchers(fileSystem, excludePatterns);
+ dirIncludes = matchers(fileSystem, directoryPatterns(includePatterns, false));
+ dirExcludes = matchers(fileSystem, directoryPatterns(excludePatterns, true));
+ needRelativize = needRelativize(includePatterns) || needRelativize(excludePatterns);
+ }
+
+ /**
+ * Creates a new matcher from the given includes and excludes.
+ *
+ * @param directory the base directory of the files to filter
+ * @param includes the patterns of the files to include, or null or empty for including all files
+ * @param excludes the patterns of the files to exclude, or null or empty for no exclusion
+ * @throws NullPointerException if directory is null
+ * @return a path matcher for the given includes and excludes
+ */
+ public static PathMatcher of(@Nonnull Path directory, Collection includes, Collection excludes) {
+ return new PathSelector(directory, includes, excludes).simplify();
+ }
+
+ /**
+ * Returns the given array of excludes, optionally expanded with a default set of excludes,
+ * then with unnecessary excludes omitted. An unnecessary exclude is an exclude which will never
+ * match a file because there are no includes which would accept a file that could match the exclude.
+ * For example, if the only include is {@code "*.java"}, then the "**/project.pj",
+ * "**/.DS_Store" and other excludes will never match a file and can be omitted.
+ * Because the list of {@linkplain #DEFAULT_EXCLUDES default excludes} contains many elements,
+ * removing unnecessary excludes can reduce a lot the number of matches tested on each source file.
+ *
+ *
Implementation note
+ * The removal of unnecessary excludes is done on a best effort basis. The current implementation
+ * compares only the prefixes and suffixes of each pattern, keeping the pattern in case of doubt.
+ * This is not bad, but it does not remove all unnecessary patterns. It would be possible to do
+ * better in the future if benchmarking suggests that it would be worth the effort.
+ *
+ * @param excludes the user-specified excludes, potentially not yet converted to glob syntax
+ * @param includes the include patterns converted to glob syntax
+ * @return the potentially expanded or reduced set of excludes to use
+ */
+ private static Collection effectiveExcludes(Collection excludes, final String[] includes) {
+ if (excludes == null || excludes.isEmpty()) {
+ return List.of();
+ } else {
+ excludes = new ArrayList<>(excludes);
+ excludes.removeIf(Objects::isNull);
+ }
+ if (includes.length == 0) {
+ return excludes;
+ }
+ /*
+ * Get the prefixes and suffixes of all includes, stopping at the first special character.
+ * Redundant prefixes and suffixes are omitted.
+ */
+ var prefixes = new String[includes.length];
+ var suffixes = new String[includes.length];
+ for (int i = 0; i < includes.length; i++) {
+ String include = includes[i];
+ if (!include.startsWith(DEFAULT_SYNTAX)) {
+ return excludes; // Do not filter if at least one pattern is too complicated.
+ }
+ include = include.substring(DEFAULT_SYNTAX.length());
+ prefixes[i] = prefixOrSuffix(include, false);
+ suffixes[i] = prefixOrSuffix(include, true);
+ }
+ prefixes = sortByLength(prefixes, false);
+ suffixes = sortByLength(suffixes, true);
+ /*
+ * Keep only the exclude which start with one of the prefixes and end with one of the suffixes.
+ * Note that a prefix or suffix may be the empty string, which match everything.
+ */
+ final Iterator it = excludes.iterator();
+ nextExclude:
+ while (it.hasNext()) {
+ final String exclude = it.next();
+ final int s = exclude.indexOf(':');
+ if (s <= MAVEN_SYNTAX_THRESHOLD || exclude.startsWith(DEFAULT_SYNTAX)) {
+ if (cannotMatch(exclude, prefixes, false) || cannotMatch(exclude, suffixes, true)) {
+ it.remove();
+ }
+ }
+ }
+ return excludes;
+ }
+
+ /**
+ * Returns the maximal amount of ordinary characters at the beginning or end of the given pattern.
+ * The prefix or suffix stops at the first {@linkplain #SPECIAL_CHARACTERS special character}.
+ *
+ * @param include the pattern for which to get a prefix or suffix without special character
+ * @param suffix {@code false} if a prefix is desired, or {@code true} if a suffix is desired
+ */
+ private static String prefixOrSuffix(final String include, boolean suffix) {
+ int s = suffix ? -1 : include.length();
+ for (int i = SPECIAL_CHARACTERS.length(); --i >= 0; ) {
+ char c = SPECIAL_CHARACTERS.charAt(i);
+ if (suffix) {
+ s = Math.max(s, include.lastIndexOf(c));
+ } else {
+ int p = include.indexOf(c);
+ if (p >= 0 && p < s) {
+ s = p;
+ }
+ }
+ }
+ return suffix ? include.substring(s + 1) : include.substring(0, s);
+ }
+
+ /**
+ * Returns {@code true} if the given exclude cannot match any include patterns.
+ * In case of doubt, returns {@code false}.
+ *
+ * @param exclude the exclude pattern to test
+ * @param fragments the prefixes or suffixes (fragments without special characters) of the includes
+ * @param suffix {@code false} if the specified fragments are prefixes, {@code true} if they are suffixes
+ * @return {@code true} if it is certain that the exclude pattern cannot match, or {@code false} in case of doubt
+ */
+ private static boolean cannotMatch(String exclude, final String[] fragments, final boolean suffix) {
+ exclude = prefixOrSuffix(exclude, suffix);
+ for (String fragment : fragments) {
+ int fg = fragment.length();
+ int ex = exclude.length();
+ int length = Math.min(fg, ex);
+ if (suffix) {
+ fg -= length;
+ ex -= length;
+ } else {
+ fg = 0;
+ ex = 0;
+ }
+ if (exclude.regionMatches(ex, fragment, fg, length)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Sorts the given patterns by their length. The main intent is to have the empty string first,
+ * while will cause the loops testing for prefixes and suffixes to stop almost immediately.
+ * Short prefixes or suffixes are also more likely to be matched.
+ *
+ * @param fragments the fragments to sort in-place
+ * @param suffix {@code false} if the specified fragments are prefixes, {@code true} if they are suffixes
+ * @return the given array, or a smaller array if some fragments were discarded because redundant
+ */
+ private static String[] sortByLength(final String[] fragments, final boolean suffix) {
+ Arrays.sort(fragments, (s1, s2) -> s1.length() - s2.length());
+ int count = 0;
+ /*
+ * Simplify the array of prefixes or suffixes by removing all redundant elements.
+ * An element is redundant if there is a shorter prefix or suffix with the same characters.
+ */
+ nextBase:
+ for (String fragment : fragments) {
+ for (int i = count; --i >= 0; ) {
+ String base = fragments[i];
+ if (suffix ? fragment.endsWith(base) : fragment.startsWith(base)) {
+ continue nextBase; // Skip this fragment
+ }
+ }
+ fragments[count++] = fragment;
+ }
+ return (fragments.length == count) ? fragments : Arrays.copyOf(fragments, count);
+ }
+
+ /**
+ * Returns the given array of patterns with path separator normalized to {@code '/'}.
+ * Null or empty patterns are ignored, and duplications are removed.
+ *
+ * @param patterns the patterns to normalize
+ * @param excludes whether the patterns are exclude patterns
+ * @return normalized patterns without null, empty or duplicated patterns
+ */
+ private static String[] normalizePatterns(final Collection patterns, final boolean excludes) {
+ if (patterns == null || patterns.isEmpty()) {
+ return new String[0];
+ }
+ // TODO: use `LinkedHashSet.newLinkedHashSet(int)` instead with JDK19.
+ final var normalized = new LinkedHashSet(patterns.size());
+ for (String pattern : patterns) {
+ if (pattern != null && !pattern.isEmpty()) {
+ if (pattern.indexOf(':') <= MAVEN_SYNTAX_THRESHOLD) {
+ pattern = pattern.replace(File.separatorChar, '/');
+ if (pattern.endsWith("/")) {
+ pattern += "**";
+ }
+ // Following are okay only when "**" means "0 or more directories".
+ while (pattern.endsWith("/**/**")) {
+ pattern = pattern.substring(0, pattern.length() - 3);
+ }
+ while (pattern.startsWith("**/**/")) {
+ pattern = pattern.substring(3);
+ }
+ pattern = pattern.replace("/**/**/", "/**/");
+
+ // Escape special characters, including braces
+ // Braces from user input must be literals; we'll inject our own braces for expansion below
+ pattern = pattern.replace("\\", "\\\\")
+ .replace("[", "\\[")
+ .replace("]", "\\]")
+ .replace("{", "\\{")
+ .replace("}", "\\}");
+
+ // Transform ** patterns to use brace expansion for POSIX behavior
+ // This replaces the complex addPatternsWithOneDirRemoved logic
+ // We perform this after escaping so that only these injected braces participate in expansion
+ pattern = pattern.replace("**/", "{**/,}");
+
+ normalized.add(DEFAULT_SYNTAX + pattern);
+ } else {
+ normalized.add(pattern);
+ }
+ }
+ }
+ return simplify(normalized, excludes);
+ }
+
+ /**
+ * Applies some heuristic rules for simplifying the set of patterns,
+ * then returns the patterns as an array.
+ *
+ * @param patterns the patterns to simplify and return as an array
+ * @param excludes whether the patterns are exclude patterns
+ * @return the set content as an array, after simplification
+ */
+ private static String[] simplify(Set patterns, boolean excludes) {
+ /*
+ * If the "**" pattern is present, it makes all other patterns useless.
+ * In the case of include patterns, an empty set means to include everything.
+ */
+ if (patterns.remove("**")) {
+ patterns.clear();
+ if (excludes) {
+ patterns.add("**");
+ }
+ }
+ return patterns.toArray(String[]::new);
+ }
+
+ /**
+ * Eventually adds the parent directory of the given patterns, without duplicated values.
+ * The patterns given to this method should have been normalized.
+ *
+ * @param patterns the normalized include or exclude patterns
+ * @param excludes whether the patterns are exclude patterns
+ * @return patterns of directories to include or exclude
+ */
+ private static String[] directoryPatterns(final String[] patterns, final boolean excludes) {
+ // TODO: use `LinkedHashSet.newLinkedHashSet(int)` instead with JDK19.
+ final var directories = new LinkedHashSet(patterns.length);
+ for (String pattern : patterns) {
+ if (pattern.startsWith(DEFAULT_SYNTAX)) {
+ if (excludes) {
+ if (pattern.endsWith("/**")) {
+ directories.add(pattern.substring(0, pattern.length() - 3));
+ }
+ } else {
+ int s = pattern.indexOf(':');
+ if (pattern.regionMatches(++s, "**/", 0, 3)) {
+ s = pattern.indexOf('/', s + 3);
+ if (s < 0) {
+ return new String[0]; // Pattern is "**", so we need to accept everything.
+ }
+ directories.add(pattern.substring(0, s));
+ }
+ }
+ }
+ }
+ return simplify(directories, excludes);
+ }
+
+ /**
+ * Returns {@code true} if at least one pattern requires path being relativized before to be matched.
+ *
+ * @param patterns include or exclude patterns
+ * @return whether at least one pattern require relativization
+ */
+ private static boolean needRelativize(String[] patterns) {
+ for (String pattern : patterns) {
+ if (!pattern.startsWith(DEFAULT_SYNTAX + "**/")) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Creates the path matchers for the given patterns.
+ * The syntax (usually {@value #DEFAULT_SYNTAX}) must be specified for each pattern.
+ */
+ private static PathMatcher[] matchers(final FileSystem fs, final String[] patterns) {
+ final var matchers = new PathMatcher[patterns.length];
+ for (int i = 0; i < patterns.length; i++) {
+ matchers[i] = fs.getPathMatcher(patterns[i]);
+ }
+ return matchers;
+ }
+
+ /**
+ * {@return a potentially simpler matcher equivalent to this matcher}.
+ */
+ @SuppressWarnings("checkstyle:MissingSwitchDefault")
+ private PathMatcher simplify() {
+ if (!needRelativize && excludes.length == 0) {
+ switch (includes.length) {
+ case 0:
+ return INCLUDES_ALL;
+ case 1:
+ return includes[0];
+ }
+ }
+ return this;
+ }
+
+ /**
+ * Determines whether a path is selected.
+ * This is true if the given file matches an include pattern and no exclude pattern.
+ *
+ * @param path the pathname to test, must not be {@code null}
+ * @return {@code true} if the given path is selected, {@code false} otherwise
+ */
+ @Override
+ public boolean matches(Path path) {
+ if (needRelativize) {
+ path = baseDirectory.relativize(path);
+ }
+ return (includes.length == 0 || isMatched(path, includes))
+ && (excludes.length == 0 || !isMatched(path, excludes));
+ }
+
+ /**
+ * {@return whether the given file matches according to one of the given matchers}.
+ */
+ private static boolean isMatched(Path path, PathMatcher[] matchers) {
+ for (PathMatcher matcher : matchers) {
+ if (matcher.matches(path)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns whether {@link #couldHoldSelected(Path)} may return {@code false} for some directories.
+ * This method can be used to determine if directory filtering optimization is possible.
+ *
+ * @return {@code true} if directory filtering is possible, {@code false} if all directories
+ * will be considered as potentially containing selected files
+ */
+ boolean canFilterDirectories() {
+ return dirIncludes.length != 0 || dirExcludes.length != 0;
+ }
+
+ /**
+ * Determines whether a directory could contain selected paths.
+ *
+ * @param directory the directory pathname to test, must not be {@code null}
+ * @return {@code true} if the given directory might contain selected paths, {@code false} if the
+ * directory will definitively not contain selected paths
+ */
+ public boolean couldHoldSelected(Path directory) {
+ if (baseDirectory.equals(directory)) {
+ return true;
+ }
+ directory = baseDirectory.relativize(directory);
+ return (dirIncludes.length == 0 || isMatched(directory, dirIncludes))
+ && (dirExcludes.length == 0 || !isMatched(directory, dirExcludes));
+ }
+
+ /**
+ * Appends the elements of the given array in the given buffer.
+ * This is a helper method for {@link #toString()} implementations.
+ *
+ * @param buffer the buffer to add the elements to
+ * @param label label identifying the array of elements to add
+ * @param patterns the elements to append, or {@code null} if none
+ */
+ private static void append(StringBuilder buffer, String label, String[] patterns) {
+ buffer.append(label).append(": [");
+ if (patterns != null) {
+ for (int i = 0; i < patterns.length; i++) {
+ if (i != 0) {
+ buffer.append(", ");
+ }
+ buffer.append(patterns[i]);
+ }
+ }
+ buffer.append(']');
+ }
+
+ /**
+ * {@return a string representation for logging purposes}.
+ */
+ @Override
+ public String toString() {
+ var buffer = new StringBuilder();
+ append(buffer, "includes", includePatterns);
+ append(buffer.append(", "), "excludes", excludePatterns);
+ return buffer.toString();
+ }
+}
From cf2a27507dec34c1f4d711d0bd3795077ade1548 Mon Sep 17 00:00:00 2001
From: Martin Desruisseaux
Date: Sat, 22 Nov 2025 11:32:50 +0100
Subject: [PATCH 2/5] Migrate from Maven Archiver to standard `jar` tool. Use
`java.util.spi.ToolProvider` API (requires Java 9+). Files in the `classes`
directories are dispatched to the `--manifest` and `--release` options, which
allow additional verifications by the `jar` tool.
Side effects:
* Remove the default `**/package.html` exclude.
* Automatic use of `META-INF/MANIFEST.MF` file found in `classes` directory.
* Automatic Multi-Release always enabled, unless `detectMultiReleaseJar` is set to `false`.
* In a multi-module project, use Java module names as artifact names.
---
pom.xml | 22 +-
.../verify.bsh | 3 +-
src/it/MJAR-292-detect-mjar/pom.xml | 4 +-
src/it/MJAR-292-disable-detect-mjar/pom.xml | 1 +
src/it/MJAR-30-include/verify.groovy | 2 +-
src/it/MJAR-70-recreation/verify.bsh | 2 +-
src/it/mjar-71-01/pom.xml | 4 +-
.../some-manifest.mf} | 0
src/it/mjar-71-01/verify.groovy | 16 +-
src/it/mjar-71-02/pom.xml | 2 +-
src/it/mjar-71-02/verify.groovy | 16 +-
src/it/multi-module/pom.xml | 67 +++
.../foo.bar.more/main/java/module-info.java | 19 +
.../foo.bar.more/main/java/more/MainFile.java | 29 +
.../src/foo.bar/main/java/foo/MainFile.java | 29 +
.../src/foo.bar/main/java/module-info.java | 19 +
src/it/multi-module/verify.groovy | 62 ++
src/it/multirelease-with-modules/pom.xml | 102 ++++
.../foo.bar.more/main/java/module-info.java | 19 +
.../foo.bar.more/main/java/more/MainFile.java | 29 +
.../main/java/more/OtherFile.java | 29 +
.../main/java_16/more/OtherFile.java | 30 +
.../src/foo.bar/main/java/foo/MainFile.java | 29 +
.../src/foo.bar/main/java/foo/OtherFile.java | 29 +
.../foo.bar/main/java/foo/YetAnotherFile.java | 29 +
.../src/foo.bar/main/java/module-info.java | 19 +
.../foo.bar/main/java_16/foo/OtherFile.java | 34 ++
.../multirelease-with-modules/verify.groovy | 67 +++
.../maven/plugins/jar/AbstractJarMojo.java | 326 ++++++-----
.../org/apache/maven/plugins/jar/Archive.java | 547 ++++++++++++++++++
.../apache/maven/plugins/jar/Archiver.java | 198 -------
.../maven/plugins/jar/DirectoryRole.java | 73 +++
.../maven/plugins/jar/FileCollector.java | 416 +++++++++++++
.../maven/plugins/jar/MetadataFiles.java | 221 +++++++
.../maven/plugins/jar/TimestampCheck.java | 239 ++++++++
.../maven/plugins/jar/ToolExecutor.java | 407 +++++++++++++
36 files changed, 2768 insertions(+), 372 deletions(-)
rename src/it/mjar-71-01/src/main/{resources/META-INF/MANIFEST.MF => my-custom-dir/some-manifest.mf} (100%)
create mode 100644 src/it/multi-module/pom.xml
create mode 100644 src/it/multi-module/src/foo.bar.more/main/java/module-info.java
create mode 100644 src/it/multi-module/src/foo.bar.more/main/java/more/MainFile.java
create mode 100644 src/it/multi-module/src/foo.bar/main/java/foo/MainFile.java
create mode 100644 src/it/multi-module/src/foo.bar/main/java/module-info.java
create mode 100644 src/it/multi-module/verify.groovy
create mode 100644 src/it/multirelease-with-modules/pom.xml
create mode 100644 src/it/multirelease-with-modules/src/foo.bar.more/main/java/module-info.java
create mode 100644 src/it/multirelease-with-modules/src/foo.bar.more/main/java/more/MainFile.java
create mode 100644 src/it/multirelease-with-modules/src/foo.bar.more/main/java/more/OtherFile.java
create mode 100644 src/it/multirelease-with-modules/src/foo.bar.more/main/java_16/more/OtherFile.java
create mode 100644 src/it/multirelease-with-modules/src/foo.bar/main/java/foo/MainFile.java
create mode 100644 src/it/multirelease-with-modules/src/foo.bar/main/java/foo/OtherFile.java
create mode 100644 src/it/multirelease-with-modules/src/foo.bar/main/java/foo/YetAnotherFile.java
create mode 100644 src/it/multirelease-with-modules/src/foo.bar/main/java/module-info.java
create mode 100644 src/it/multirelease-with-modules/src/foo.bar/main/java_16/foo/OtherFile.java
create mode 100644 src/it/multirelease-with-modules/verify.groovy
create mode 100644 src/main/java/org/apache/maven/plugins/jar/Archive.java
delete mode 100644 src/main/java/org/apache/maven/plugins/jar/Archiver.java
create mode 100644 src/main/java/org/apache/maven/plugins/jar/DirectoryRole.java
create mode 100644 src/main/java/org/apache/maven/plugins/jar/FileCollector.java
create mode 100644 src/main/java/org/apache/maven/plugins/jar/MetadataFiles.java
create mode 100644 src/main/java/org/apache/maven/plugins/jar/TimestampCheck.java
create mode 100644 src/main/java/org/apache/maven/plugins/jar/ToolExecutor.java
diff --git a/pom.xml b/pom.xml
index 1986349d..326dfcc1 100644
--- a/pom.xml
+++ b/pom.xml
@@ -34,6 +34,26 @@
Apache Maven JAR PluginBuilds a Java Archive (JAR) file from the compiled project classes and resources.
+
+
+ evenisse
+ Emmanuel Venisse
+ evenisse@apache.org
+
+ Java Developer
+
+
+
+ desruisseaux
+ Martin Desruisseaux
+ desruisseaux@apache.org
+ Geomatys
+
+ Java Developer
+
+ +1
+
+ Jerome Lacoste
@@ -208,7 +228,7 @@
- src/it/mjar-71-01/src/main/resources/META-INF/MANIFEST.MF
+ src/it/mjar-71-01/src/main/my-custom-dir/some-manifest.mfsrc/it/mjar-71-02/src/main/resources/META-INF/MANIFEST.MF
diff --git a/src/it/MJAR-260-invalid-automatic-module-name/verify.bsh b/src/it/MJAR-260-invalid-automatic-module-name/verify.bsh
index 452aed0e..7b39fb52 100644
--- a/src/it/MJAR-260-invalid-automatic-module-name/verify.bsh
+++ b/src/it/MJAR-260-invalid-automatic-module-name/verify.bsh
@@ -45,8 +45,7 @@ try
String[] snippets = new String[] {
"[INFO] BUILD FAILURE",
"[ERROR] Failed to execute goal org.apache.maven.plugins:maven-jar-plugin",
- "Caused by: org.apache.maven.api.plugin.MojoException: Error while assembling the JAR file.",
- "Caused by: org.codehaus.plexus.archiver.jar.ManifestException: Invalid automatic module name: 'in-valid.name.with.new.keyword'"
+ "Caused by: org.apache.maven.api.plugin.MojoException: Invalid automatic module name: \"in-valid.name.with.new.keyword\"."
};
System.out.println("\nVerifying log snippets...");
diff --git a/src/it/MJAR-292-detect-mjar/pom.xml b/src/it/MJAR-292-detect-mjar/pom.xml
index 9c0afd52..d473e74c 100644
--- a/src/it/MJAR-292-detect-mjar/pom.xml
+++ b/src/it/MJAR-292-detect-mjar/pom.xml
@@ -86,8 +86,8 @@
false
diff --git a/src/it/MJAR-292-disable-detect-mjar/pom.xml b/src/it/MJAR-292-disable-detect-mjar/pom.xml
index 1cc61ea9..c868bdce 100644
--- a/src/it/MJAR-292-disable-detect-mjar/pom.xml
+++ b/src/it/MJAR-292-disable-detect-mjar/pom.xml
@@ -84,6 +84,7 @@
myproject.HelloWorld
+ false
diff --git a/src/it/MJAR-30-include/verify.groovy b/src/it/MJAR-30-include/verify.groovy
index 28d3e5f3..1b0b838c 100644
--- a/src/it/MJAR-30-include/verify.groovy
+++ b/src/it/MJAR-30-include/verify.groovy
@@ -67,7 +67,7 @@ try
{
String artifactName = artifactNames[i];
if ( !contents.contains( artifactName ) )
- {
+ {
System.err.println( "Artifact[" + artifactName + "] not found in jar archive" );
return false;
}
diff --git a/src/it/MJAR-70-recreation/verify.bsh b/src/it/MJAR-70-recreation/verify.bsh
index 29b80ca2..25da0f97 100644
--- a/src/it/MJAR-70-recreation/verify.bsh
+++ b/src/it/MJAR-70-recreation/verify.bsh
@@ -58,7 +58,7 @@ if ( buildLog.exists() ) {
int jarPluginExecutions = 0;
String[] lines = buildLogContent.split( "\n" );
for ( String line : lines ) {
- if ( line.contains( "Building jar:" ) && line.contains( "MJAR-70-recreation-1.0-SNAPSHOT.jar" ) ) {
+ if ( line.contains( "Building JAR:" ) && line.contains( "MJAR-70-recreation-1.0-SNAPSHOT.jar" ) ) {
jarPluginExecutions++;
System.out.println( "Found JAR creation: " + line );
}
diff --git a/src/it/mjar-71-01/pom.xml b/src/it/mjar-71-01/pom.xml
index cb6b3195..d5b6de07 100644
--- a/src/it/mjar-71-01/pom.xml
+++ b/src/it/mjar-71-01/pom.xml
@@ -25,7 +25,7 @@ under the License.
1.0jarit-mjar-71
- Test that the default manifest is added by default if found under target/classes. Can also be overriden.
+ Test that the specified manifest is used.http://maven.apache.org
@@ -36,7 +36,7 @@ under the License.
@project.version@
- src/main/resources/META-INF/MANIFEST.MF
+ src/main/my-custom-dir/some-manifest.mf
diff --git a/src/it/mjar-71-01/src/main/resources/META-INF/MANIFEST.MF b/src/it/mjar-71-01/src/main/my-custom-dir/some-manifest.mf
similarity index 100%
rename from src/it/mjar-71-01/src/main/resources/META-INF/MANIFEST.MF
rename to src/it/mjar-71-01/src/main/my-custom-dir/some-manifest.mf
diff --git a/src/it/mjar-71-01/verify.groovy b/src/it/mjar-71-01/verify.groovy
index 97f78e88..60faf65e 100644
--- a/src/it/mjar-71-01/verify.groovy
+++ b/src/it/mjar-71-01/verify.groovy
@@ -51,14 +51,14 @@ try
// Only compare files
if ( entry.getName().equals( "META-INF/MANIFEST.MF" ) )
{
- String manifest = IOUtils.toString( jar.getInputStream ( entry ) );
- int index = manifest.indexOf( "Archiver-Version: foobar-1.23456" );
- if ( index <= 0 )
- {
- System.err.println( "MANIFEST doesn't contain: 'Archiver-Version: foobar-1.23456'" );
- return false;
- }
- return true;
+ String manifest = IOUtils.toString( jar.getInputStream ( entry ) );
+ int index = manifest.indexOf( "Archiver-Version: foobar-1.23456" );
+ if ( index <= 0 )
+ {
+ System.err.println( "MANIFEST doesn't contain: 'Archiver-Version: foobar-1.23456'" );
+ return false;
+ }
+ return true;
}
}
}
diff --git a/src/it/mjar-71-02/pom.xml b/src/it/mjar-71-02/pom.xml
index fa3adac7..3fd9b506 100644
--- a/src/it/mjar-71-02/pom.xml
+++ b/src/it/mjar-71-02/pom.xml
@@ -25,7 +25,7 @@ under the License.
1.0jarit-mjar-71-02
- Test that the default manifest is not added when found under target/classes but support is disabled.
+ Test that the manifest found under target/classes is automatically used.http://maven.apache.org
diff --git a/src/it/mjar-71-02/verify.groovy b/src/it/mjar-71-02/verify.groovy
index 33e40531..ffddfcbf 100644
--- a/src/it/mjar-71-02/verify.groovy
+++ b/src/it/mjar-71-02/verify.groovy
@@ -51,14 +51,14 @@ try
// Only compare files
if ( entry.getName().equals ( "META-INF/MANIFEST.MF" ) )
{
- String manifest = IOUtils.toString( jar.getInputStream ( entry ) );
- int index = manifest.indexOf( "Archiver-Version: foobar-1.23456" );
- if ( index > 0 )
- {
- System.err.println( "MANIFEST contains: 'Archiver-Version: foobar-1.23456', but shouldn't" );
- return false;
- }
- return true;
+ String manifest = IOUtils.toString( jar.getInputStream ( entry ) );
+ int index = manifest.indexOf( "Archiver-Version: foobar-1.23456" );
+ if ( index <= 0 )
+ {
+ System.err.println( "MANIFEST doesn't contain: 'Archiver-Version: foobar-1.23456'" );
+ return false;
+ }
+ return true;
}
}
}
diff --git a/src/it/multi-module/pom.xml b/src/it/multi-module/pom.xml
new file mode 100644
index 00000000..d4a781e9
--- /dev/null
+++ b/src/it/multi-module/pom.xml
@@ -0,0 +1,67 @@
+
+
+
+ 4.1.0
+
+ org.apache.maven.plugins
+ multi-module
+ 1.0-SNAPSHOT
+ jar
+ Multi-module
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 4.0.0-beta-3
+
+
+
+
+ 17
+
+
+
+ org.apache.maven.plugins
+ maven-jar-plugin
+ @project.version@
+
+
+
+ true
+ foo.bar/foo.MainFile
+
+
+
+
+
+
+
+ foo.bar
+ src/foo.bar/main/java
+
+
+ foo.bar.more
+ src/foo.bar.more/main/java
+
+
+
+
diff --git a/src/it/multi-module/src/foo.bar.more/main/java/module-info.java b/src/it/multi-module/src/foo.bar.more/main/java/module-info.java
new file mode 100644
index 00000000..778a3a4a
--- /dev/null
+++ b/src/it/multi-module/src/foo.bar.more/main/java/module-info.java
@@ -0,0 +1,19 @@
+/*
+ * 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.
+ */
+module foo.bar.more {}
diff --git a/src/it/multi-module/src/foo.bar.more/main/java/more/MainFile.java b/src/it/multi-module/src/foo.bar.more/main/java/more/MainFile.java
new file mode 100644
index 00000000..d64f30a7
--- /dev/null
+++ b/src/it/multi-module/src/foo.bar.more/main/java/more/MainFile.java
@@ -0,0 +1,29 @@
+/*
+ * 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 more;
+
+/**
+ * Test {@code <Source>}.
+ * Another {@code <Source>}.
+ */
+public class MainFile {
+ public static void main(String[] args) {
+ System.out.println("MainFile of more");
+ }
+}
diff --git a/src/it/multi-module/src/foo.bar/main/java/foo/MainFile.java b/src/it/multi-module/src/foo.bar/main/java/foo/MainFile.java
new file mode 100644
index 00000000..502f2780
--- /dev/null
+++ b/src/it/multi-module/src/foo.bar/main/java/foo/MainFile.java
@@ -0,0 +1,29 @@
+/*
+ * 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 foo;
+
+/**
+ * Test {@code <Source>}.
+ * Another {@code <Source>}.
+ */
+public class MainFile {
+ public static void main(String[] args) {
+ System.out.println("MainFile");
+ }
+}
diff --git a/src/it/multi-module/src/foo.bar/main/java/module-info.java b/src/it/multi-module/src/foo.bar/main/java/module-info.java
new file mode 100644
index 00000000..38f61c0e
--- /dev/null
+++ b/src/it/multi-module/src/foo.bar/main/java/module-info.java
@@ -0,0 +1,19 @@
+/*
+ * 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.
+ */
+module foo.bar {}
diff --git a/src/it/multi-module/verify.groovy b/src/it/multi-module/verify.groovy
new file mode 100644
index 00000000..df8756de
--- /dev/null
+++ b/src/it/multi-module/verify.groovy
@@ -0,0 +1,62 @@
+/*
+ * 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.
+ */
+
+import java.io.*;
+import java.util.*;
+import java.util.jar.*;
+
+File target = new File(basedir, "target");
+
+Set content = new HashSet<>();
+content.add("module-info.class")
+content.add("foo/MainFile.class")
+content.add("META-INF/MANIFEST.MF")
+content.add("META-INF/maven/org.apache.maven.plugins/multi-module/pom.xml")
+content.add("META-INF/maven/org.apache.maven.plugins/multi-module/pom.properties")
+verify(new File(target, "foo.bar-1.0-SNAPSHOT.jar"), content, "foo.MainFile")
+
+content.clear()
+content.add("module-info.class")
+content.add("more/MainFile.class")
+content.add("META-INF/MANIFEST.MF")
+content.add("META-INF/maven/org.apache.maven.plugins/multi-module/pom.xml")
+content.add("META-INF/maven/org.apache.maven.plugins/multi-module/pom.properties")
+verify(new File(target, "foo.bar.more-1.0-SNAPSHOT.jar"), content, null)
+
+void verify(File artifact, Set content, String mainClass)
+{
+ JarFile jar = new JarFile(artifact)
+ Enumeration jarEntries = jar.entries()
+ while (jarEntries.hasMoreElements())
+ {
+ JarEntry entry = (JarEntry) jarEntries.nextElement()
+ if (!entry.isDirectory())
+ {
+ String name = entry.getName()
+ assert content.remove(name) : "Missing entry: " + name
+ }
+ }
+ assert content.isEmpty() : "Unexpected entries: " + content
+
+ Attributes attributes = jar.getManifest().getMainAttributes()
+ assert attributes.get(Attributes.Name.MULTI_RELEASE) == null
+ assert Objects.equals(mainClass, attributes.get(Attributes.Name.MAIN_CLASS))
+
+ jar.close();
+}
diff --git a/src/it/multirelease-with-modules/pom.xml b/src/it/multirelease-with-modules/pom.xml
new file mode 100644
index 00000000..5f02bbbe
--- /dev/null
+++ b/src/it/multirelease-with-modules/pom.xml
@@ -0,0 +1,102 @@
+
+
+
+ 4.1.0
+
+ org.apache.maven.plugins
+ multirelease-with-modules
+ 1.0-SNAPSHOT
+ jar
+ Multirelease with modules
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 4.0.0-beta-3
+
+
+
+
+ 17
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-antrun-plugin
+ 3.1.0
+
+
+ move
+
+ run
+
+ prepare-package
+
+
+
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-jar-plugin
+ @project.version@
+
+
+
+ true
+ true
+ foo.bar/foo.MainFile
+
+
+
+
+
+
+
+ foo.bar
+ src/foo.bar/main/java
+ 15
+
+
+ foo.bar
+ src/foo.bar/main/java_16
+ 16
+
+
+ foo.bar.more
+ src/foo.bar.more/main/java
+ 15
+
+
+ foo.bar.more
+ src/foo.bar.more/main/java_16
+ 16
+
+
+
+
diff --git a/src/it/multirelease-with-modules/src/foo.bar.more/main/java/module-info.java b/src/it/multirelease-with-modules/src/foo.bar.more/main/java/module-info.java
new file mode 100644
index 00000000..778a3a4a
--- /dev/null
+++ b/src/it/multirelease-with-modules/src/foo.bar.more/main/java/module-info.java
@@ -0,0 +1,19 @@
+/*
+ * 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.
+ */
+module foo.bar.more {}
diff --git a/src/it/multirelease-with-modules/src/foo.bar.more/main/java/more/MainFile.java b/src/it/multirelease-with-modules/src/foo.bar.more/main/java/more/MainFile.java
new file mode 100644
index 00000000..d64f30a7
--- /dev/null
+++ b/src/it/multirelease-with-modules/src/foo.bar.more/main/java/more/MainFile.java
@@ -0,0 +1,29 @@
+/*
+ * 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 more;
+
+/**
+ * Test {@code <Source>}.
+ * Another {@code <Source>}.
+ */
+public class MainFile {
+ public static void main(String[] args) {
+ System.out.println("MainFile of more");
+ }
+}
diff --git a/src/it/multirelease-with-modules/src/foo.bar.more/main/java/more/OtherFile.java b/src/it/multirelease-with-modules/src/foo.bar.more/main/java/more/OtherFile.java
new file mode 100644
index 00000000..54e29b3c
--- /dev/null
+++ b/src/it/multirelease-with-modules/src/foo.bar.more/main/java/more/OtherFile.java
@@ -0,0 +1,29 @@
+/*
+ * 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 more;
+
+/**
+ * Test {@code <Source>}.
+ * Another {@code <Source>}.
+ */
+public class OtherFile {
+ public static void main(String[] args) {
+ System.out.println("OtherFile of more");
+ }
+}
diff --git a/src/it/multirelease-with-modules/src/foo.bar.more/main/java_16/more/OtherFile.java b/src/it/multirelease-with-modules/src/foo.bar.more/main/java_16/more/OtherFile.java
new file mode 100644
index 00000000..4b21485e
--- /dev/null
+++ b/src/it/multirelease-with-modules/src/foo.bar.more/main/java_16/more/OtherFile.java
@@ -0,0 +1,30 @@
+/*
+ * 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 more;
+
+/**
+ * Test {@code <Source>}.
+ * Another {@code <Source>}.
+ */
+public class OtherFile {
+ public static void main(String[] args) {
+ System.out.println("OtherFile of more on Java 16");
+ MainFile.main(args); // Verify that we have access to the base version.
+ }
+}
diff --git a/src/it/multirelease-with-modules/src/foo.bar/main/java/foo/MainFile.java b/src/it/multirelease-with-modules/src/foo.bar/main/java/foo/MainFile.java
new file mode 100644
index 00000000..502f2780
--- /dev/null
+++ b/src/it/multirelease-with-modules/src/foo.bar/main/java/foo/MainFile.java
@@ -0,0 +1,29 @@
+/*
+ * 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 foo;
+
+/**
+ * Test {@code <Source>}.
+ * Another {@code <Source>}.
+ */
+public class MainFile {
+ public static void main(String[] args) {
+ System.out.println("MainFile");
+ }
+}
diff --git a/src/it/multirelease-with-modules/src/foo.bar/main/java/foo/OtherFile.java b/src/it/multirelease-with-modules/src/foo.bar/main/java/foo/OtherFile.java
new file mode 100644
index 00000000..472210e1
--- /dev/null
+++ b/src/it/multirelease-with-modules/src/foo.bar/main/java/foo/OtherFile.java
@@ -0,0 +1,29 @@
+/*
+ * 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 foo;
+
+/**
+ * Test {@code <Source>}.
+ * Another {@code <Source>}.
+ */
+public class OtherFile {
+ public static void main(String[] args) {
+ System.out.println("OtherFile");
+ }
+}
diff --git a/src/it/multirelease-with-modules/src/foo.bar/main/java/foo/YetAnotherFile.java b/src/it/multirelease-with-modules/src/foo.bar/main/java/foo/YetAnotherFile.java
new file mode 100644
index 00000000..ab5f9009
--- /dev/null
+++ b/src/it/multirelease-with-modules/src/foo.bar/main/java/foo/YetAnotherFile.java
@@ -0,0 +1,29 @@
+/*
+ * 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 foo;
+
+/**
+ * Test {@code <Source>}.
+ * Another {@code <Source>}.
+ */
+public class YetAnotherFile {
+ public static void main(String[] args) {
+ System.out.println("YetAnotherFile");
+ }
+}
diff --git a/src/it/multirelease-with-modules/src/foo.bar/main/java/module-info.java b/src/it/multirelease-with-modules/src/foo.bar/main/java/module-info.java
new file mode 100644
index 00000000..38f61c0e
--- /dev/null
+++ b/src/it/multirelease-with-modules/src/foo.bar/main/java/module-info.java
@@ -0,0 +1,19 @@
+/*
+ * 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.
+ */
+module foo.bar {}
diff --git a/src/it/multirelease-with-modules/src/foo.bar/main/java_16/foo/OtherFile.java b/src/it/multirelease-with-modules/src/foo.bar/main/java_16/foo/OtherFile.java
new file mode 100644
index 00000000..cbfa0b98
--- /dev/null
+++ b/src/it/multirelease-with-modules/src/foo.bar/main/java_16/foo/OtherFile.java
@@ -0,0 +1,34 @@
+/*
+ * 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 foo;
+
+/**
+ * Test {@code <Source>}.
+ * Another {@code <Source>}.
+ */
+public class OtherFile {
+ public static void main(String[] args) {
+ System.out.println("OtherFile on Java 16");
+ MainFile.main(args); // Verify that we have access to the base version.
+ }
+
+ static void requireJava16() {
+ System.out.println("Method available only on Java 16+");
+ }
+}
diff --git a/src/it/multirelease-with-modules/verify.groovy b/src/it/multirelease-with-modules/verify.groovy
new file mode 100644
index 00000000..d62d91f2
--- /dev/null
+++ b/src/it/multirelease-with-modules/verify.groovy
@@ -0,0 +1,67 @@
+/*
+ * 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.
+ */
+
+import java.io.*;
+import java.util.*;
+import java.util.jar.*;
+
+File target = new File(basedir, "target");
+
+Set content = new HashSet<>();
+content.add("module-info.class")
+content.add("foo/MainFile.class")
+content.add("foo/OtherFile.class")
+content.add("foo/YetAnotherFile.class")
+content.add("META-INF/versions/16/foo/OtherFile.class")
+content.add("META-INF/MANIFEST.MF")
+content.add("META-INF/maven/org.apache.maven.plugins/multirelease-with-modules/pom.xml")
+content.add("META-INF/maven/org.apache.maven.plugins/multirelease-with-modules/pom.properties")
+verify(new File(target, "foo.bar-1.0-SNAPSHOT.jar"), content, "foo.MainFile")
+
+content.clear()
+content.add("module-info.class")
+content.add("more/MainFile.class")
+content.add("more/OtherFile.class")
+content.add("META-INF/versions/16/more/OtherFile.class")
+content.add("META-INF/MANIFEST.MF")
+content.add("META-INF/maven/org.apache.maven.plugins/multirelease-with-modules/pom.xml")
+content.add("META-INF/maven/org.apache.maven.plugins/multirelease-with-modules/pom.properties")
+verify(new File(target, "foo.bar.more-1.0-SNAPSHOT.jar"), content, null)
+
+void verify(File artifact, Set content, String mainClass)
+{
+ JarFile jar = new JarFile(artifact)
+ Enumeration jarEntries = jar.entries()
+ while (jarEntries.hasMoreElements())
+ {
+ JarEntry entry = (JarEntry) jarEntries.nextElement()
+ if (!entry.isDirectory())
+ {
+ String name = entry.getName()
+ assert content.remove(name) : "Missing entry: " + name
+ }
+ }
+ assert content.isEmpty() : "Unexpected entries: " + content
+
+ Attributes attributes = jar.getManifest().getMainAttributes()
+ assert Objects.equals("true", attributes.get(Attributes.Name.MULTI_RELEASE))
+ assert Objects.equals(mainClass, attributes.get(Attributes.Name.MAIN_CLASS))
+
+ jar.close();
+}
diff --git a/src/main/java/org/apache/maven/plugins/jar/AbstractJarMojo.java b/src/main/java/org/apache/maven/plugins/jar/AbstractJarMojo.java
index ef167879..0fa813d9 100644
--- a/src/main/java/org/apache/maven/plugins/jar/AbstractJarMojo.java
+++ b/src/main/java/org/apache/maven/plugins/jar/AbstractJarMojo.java
@@ -21,12 +21,12 @@
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
+import java.time.Instant;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
-import java.util.Objects;
-import java.util.jar.Attributes;
-import java.util.stream.Stream;
+import java.util.jar.Manifest;
+import java.util.spi.ToolProvider;
import org.apache.maven.api.ProducedArtifact;
import org.apache.maven.api.Project;
@@ -38,20 +38,22 @@
import org.apache.maven.api.services.ProjectManager;
import org.apache.maven.shared.archiver.MavenArchiveConfiguration;
import org.apache.maven.shared.archiver.MavenArchiver;
-import org.apache.maven.shared.archiver.MavenArchiverException;
-import org.codehaus.plexus.archiver.Archiver;
-import org.codehaus.plexus.archiver.jar.JarArchiver;
/**
* Base class for creating a JAR file from project classes.
*
* @author Emmanuel Venisse
+ * @author Martin Desruisseaux
*/
public abstract class AbstractJarMojo implements org.apache.maven.api.plugin.Mojo {
-
- private static final String[] DEFAULT_EXCLUDES = new String[] {"**/package.html"};
-
- private static final String[] DEFAULT_INCLUDES = new String[] {"**/**"};
+ /**
+ * Identifier of the tool to use. This identifier shall match the identifier of a tool
+ * registered as a {@link ToolProvider}. By default, the {@code "jar"} tool is used.
+ *
+ * @since 4.0.0-beta-2
+ */
+ @Parameter(defaultValue = "jar", required = true)
+ private String toolId;
/**
* List of files to include. Specified as fileset patterns which are relative to the input directory whose contents
@@ -68,23 +70,20 @@ public abstract class AbstractJarMojo implements org.apache.maven.api.plugin.Moj
private String[] excludes;
/**
- * Directory containing the generated JAR.
+ * Directory containing the generated JAR files.
*/
@Parameter(defaultValue = "${project.build.directory}", required = true)
private Path outputDirectory;
/**
- * Name of the generated JAR.
+ * Name of the generated JAR file.
+ * The default value is {@code "${project.build.finalName}"},
+ * which itself defaults to {@code "${artifactId}-${version}"}.
+ * Ignored if the Maven sub-project to archive uses module hierarchy.
*/
@Parameter(defaultValue = "${project.build.finalName}", readonly = true)
private String finalName;
- /**
- * The JAR archiver.
- */
- @Inject
- private Map archivers;
-
/**
* The Maven project.
*/
@@ -108,9 +107,9 @@ public abstract class AbstractJarMojo implements org.apache.maven.api.plugin.Moj
private ProjectManager projectManager;
/**
- * Require the jar plugin to build a new JAR even if none of the contents appear to have changed.
- * By default, this plugin looks to see if the output JAR exists and inputs have not changed.
- * If these conditions are true, the plugin skips creation of the JAR file.
+ * Require the jar plugin to build new JAR files even if none of the contents appear to have changed.
+ * By default, this plugin looks to see if the output JAR files exist and inputs have not changed.
+ * If these conditions are true, the plugin skips creation of the JAR files.
* This does not work when other plugins, like the maven-shade-plugin, are configured to post-process the JAR.
* This plugin can not detect the post-processing, and so leaves the post-processed JAR file in place.
* This can lead to failures when those plugins do not expect to find their own output as an input.
@@ -120,20 +119,23 @@ public abstract class AbstractJarMojo implements org.apache.maven.api.plugin.Moj
* to {@code maven.jar.forceCreation}.
*/
@Parameter(property = "maven.jar.forceCreation", defaultValue = "false")
- private boolean forceCreation;
+ protected boolean forceCreation;
/**
* Skip creating empty archives.
*/
@Parameter(defaultValue = "false")
- private boolean skipIfEmpty;
+ protected boolean skipIfEmpty;
/**
* Timestamp for reproducible output archive entries.
* This is either formatted as ISO 8601 extended offset date-time
* (e.g. in UTC such as '2011-12-03T10:15:30Z' or with an offset '2019-10-05T20:37:42+06:00'),
- * or as an integer representing seconds since the epoch
- * (like SOURCE_DATE_EPOCH).
+ * or as an integer representing seconds since the Java epoch (January 1st, 1970).
+ * If not configured or disabled,
+ * the SOURCE_DATE_EPOCH
+ * environment variable is used as a fallback value,
+ * to ease forcing Reproducible Build externally when the build has not enabled it natively in POM.
*
* @since 3.2.0
*/
@@ -141,14 +143,25 @@ public abstract class AbstractJarMojo implements org.apache.maven.api.plugin.Moj
private String outputTimestamp;
/**
- * Whether to detect multi-release JAR files.
- * If the JAR contains the {@code META-INF/versions} directory it will be detected as a multi-release JAR file
- * ("MRJAR"), adding the {@code Multi-Release: true} attribute to the main section of the JAR {@code MANIFEST.MF}.
+ * Whether to detect multi-release JAR files.
+ * If the JAR contains the {@code META-INF/versions} directory it will be detected as a multi-release JAR file,
+ * adding the {@code Multi-Release: true} attribute to the main section of the JAR {@code MANIFEST.MF} entry.
+ * In addition, the class files in {@code META-INF/versions} will be checked for API compatibility
+ * with the class files in the base version. If this flag is {@code false}, then the {@code META-INF/versions}
+ * directories are included without processing.
*
* @since 3.4.0
*/
@Parameter(property = "maven.jar.detectMultiReleaseJar", defaultValue = "true")
- private boolean detectMultiReleaseJar;
+ protected boolean detectMultiReleaseJar;
+
+ /**
+ * Specifies whether to attach the JAR files to the project.
+ *
+ * @since 4.0.0-beta-2
+ */
+ @Parameter(property = "maven.jar.attach", defaultValue = "true")
+ protected boolean attach;
/**
* The MOJO logger.
@@ -162,22 +175,19 @@ public abstract class AbstractJarMojo implements org.apache.maven.api.plugin.Moj
protected AbstractJarMojo() {}
/**
- * Specifies whether to attach the jar to the project
- *
- * @since 4.0.0-beta-2
+ * {@return the specific output directory to serve as the root for the archive}
*/
- @Parameter(property = "maven.jar.attach", defaultValue = "true")
- protected boolean attach;
+ protected abstract Path getClassesDirectory();
/**
- * {@return the specific output directory to serve as the root for the archive}
+ * {@return the directory containing the generated JAR files}
*/
- protected abstract Path getClassesDirectory();
+ protected Path getOutputDirectory() {
+ return outputDirectory;
+ }
/**
- * Return the {@linkplain #project Maven project}.
- *
- * @return the Maven project
+ * {@return the Maven project}
*/
protected final Project getProject() {
return project;
@@ -186,12 +196,19 @@ protected final Project getProject() {
/**
* {@return the MOJO logger}
*/
- protected final Log getLog() {
+ protected Log getLog() {
return log;
}
/**
- * {@return the classifier of the JAR file to produce}
+ * {@return the name of the generated JAR file}
+ */
+ protected String getFinalName() {
+ return finalName;
+ }
+
+ /**
+ * {@return the classifier of the JAR file to produce}
* This is usually null or empty for the main artifact, or {@code "tests"} for the JAR file of test code.
*/
protected abstract String getClassifier();
@@ -203,151 +220,164 @@ protected final Log getLog() {
protected abstract String getType();
/**
- * Returns the JAR file to generate, based on an optional classifier.
+ * {@return the JAR tool to use for archiving the code}
*
- * @param basedir the output directory
- * @param resultFinalName the name of the ear file
- * @param classifier an optional classifier
- * @return the file to generate
+ * @throws MojoException if no JAR tool was found
+ *
+ * @since 4.0.0-beta-2
*/
- protected Path getJarFile(Path basedir, String resultFinalName, String classifier) {
- Objects.requireNonNull(basedir, "basedir is not allowed to be null");
- Objects.requireNonNull(resultFinalName, "finalName is not allowed to be null");
- String fileName = resultFinalName + (hasClassifier(classifier) ? '-' + classifier : "") + ".jar";
- return basedir.resolve(fileName);
+ protected ToolProvider getJarTool() throws MojoException {
+ return ToolProvider.findFirst(toolId).orElseThrow(() -> new MojoException("No such \"" + toolId + "\" tool."));
}
/**
- * Generates the JAR file.
+ * Returns the output time stamp or, as a fallback, the {@code SOURCE_DATE_EPOCH} environment variable.
+ * If the time stamp is expressed in seconds, it is converted to ISO 8601 format. Otherwise it is returned as-is.
*
- * @return the path to the created archive file
- * @throws IOException in case of an error while reading a file or writing in the JAR file
+ * @return the time stamp in presumed ISO 8601 format, or {@code null} if none
+ *
+ * @since 4.0.0-beta-2
*/
- @SuppressWarnings("checkstyle:UnusedLocalVariable") // Checkstyle bug: does not detect `includedFiles` usage.
- public Path createArchive() throws IOException {
- String archiverName = "jar";
- final Path jarFile = getJarFile(outputDirectory, finalName, getClassifier());
- final Path classesDirectory = getClassesDirectory();
- if (Files.exists(classesDirectory)) {
- var includedFiles = new org.apache.maven.plugins.jar.Archiver(
- classesDirectory,
- (includes != null) ? Arrays.asList(includes) : List.of(),
- (excludes != null && excludes.length > 0)
- ? Arrays.asList(excludes)
- : List.of(AbstractJarMojo.DEFAULT_EXCLUDES));
-
- var scanner = includedFiles.new VersionScanner(detectMultiReleaseJar);
- if (Files.exists(classesDirectory)) {
- Files.walkFileTree(classesDirectory, scanner);
+ protected String getOutputTimestamp() {
+ String time = nullIfAbsent(outputTimestamp);
+ if (time == null) {
+ time = nullIfAbsent(System.getenv("SOURCE_DATE_EPOCH"));
+ if (time == null) {
+ return null;
}
- if (detectMultiReleaseJar && scanner.detectedMultiReleaseJAR) {
- getLog().debug("Adding 'Multi-Release: true' manifest entry.");
- archive.addManifestEntry(Attributes.Name.MULTI_RELEASE.toString(), "true");
- }
- if (scanner.containsModuleDescriptor) {
- archiverName = "mjar";
+ }
+ for (int i = time.length(); --i >= 0; ) {
+ char c = time.charAt(i);
+ if ((c < '0' || c > '9') && (i != 0 || c != '-')) {
+ return time;
}
}
- MavenArchiver archiver = new MavenArchiver();
- archiver.setCreatedBy("Maven JAR Plugin", "org.apache.maven.plugins", "maven-jar-plugin");
- archiver.setArchiver((JarArchiver) archivers.get(archiverName));
- archiver.setOutputFile(jarFile.toFile());
+ return Instant.ofEpochSecond(Long.parseLong(time)).toString();
+ }
- // configure for Reproducible Builds based on outputTimestamp value
- archiver.configureReproducibleBuild(outputTimestamp);
+ /**
+ * {@return the patterns of files to include, or an empty list if no include pattern was specified}
+ */
+ protected List getIncludes() {
+ return asList(includes);
+ }
- archive.setForced(forceCreation);
+ /**
+ * {@return the patterns of files to exclude, or an empty list if no exclude pattern was specified}
+ */
+ protected List getExcludes() {
+ return asList(excludes);
+ }
- Path contentDirectory = getClassesDirectory();
- if (!Files.exists(contentDirectory)) {
- if (!forceCreation) {
- getLog().warn("JAR will be empty - no content was marked for inclusion!");
+ /**
+ * Returns the given elements as a list if non-null.
+ *
+ * @param elements the elements, or {@code null}
+ * @return the elements as a list, or {@code null} if the given array was null
+ */
+ private static List asList(String[] elements) {
+ return (elements == null) ? List.of() : Arrays.asList(elements);
+ }
+
+ /**
+ * Generates the JAR files.
+ * Map keys are module names or {@code null} if the project does not use module hierarchy.
+ * Values are paths to the JAR file associated with each module.
+ *
+ *
Note that a null key does not necessarily means that the JAR is not modular.
+ * It only means that the project was not compiled with module hierarchy,
+ * i.e. {@code target/classes/} subdirectories having module names.
+ * A project can be compiled with package hierarchy and still be modular.
+ *
+ * @return the paths to the created archive files
+ * @throws IOException if an error occurred while walking the file tree
+ * @throws MojoException if an error occurred while writing a JAR file
+ */
+ public Map createArchives() throws IOException, MojoException {
+ final Path classesDirectory = getClassesDirectory();
+ final boolean notExists = Files.notExists(classesDirectory);
+ if (notExists) {
+ if (forceCreation) {
+ getLog().warn("No JAR created because no content was marked for inclusion.");
+ }
+ if (skipIfEmpty) {
+ getLog().info(String.format("Skipping packaging of the %s.", getType()));
+ return Map.of();
}
- } else {
- archiver.getArchiver().addDirectory(contentDirectory.toFile(), getIncludes(), getExcludes());
}
- archiver.createArchive(session, project, archive);
- return jarFile;
+ archive.setForced(forceCreation);
+ // TODO: we want a null manifest if there is no configuration.
+ Manifest manifest = new MavenArchiver().getManifest(session, project, archive);
+ var executor = new ToolExecutor(this, manifest, archive);
+ var files = new FileCollector(this, executor, classesDirectory);
+ if (!notExists) {
+ Files.walkFileTree(classesDirectory, files);
+ }
+ return executor.writeAllJARs(files);
}
/**
- * Generates the JAR.
+ * Generates the JAR file, then attaches the artifact.
*
* @throws MojoException in case of an error
*/
@Override
+ @SuppressWarnings("UseSpecificCatch")
public void execute() throws MojoException {
- if (skipIfEmpty && isEmpty(getClassesDirectory())) {
- getLog().info(String.format("Skipping packaging of the %s.", getType()));
- } else {
- Path jarFile;
- try {
- jarFile = createArchive();
- } catch (Exception e) {
- throw new MojoException("Error while assembling the JAR file.", e);
- }
- ProducedArtifact artifact;
- String classifier = getClassifier();
- if (attach) {
- if (hasClassifier(classifier)) {
+ final Map jarFiles;
+ try {
+ jarFiles = createArchives();
+ } catch (MojoException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new MojoException("Error while assembling the JAR file.", e);
+ }
+ if (jarFiles.isEmpty()) {
+ // Message already logged by `createArchives()`.
+ return;
+ }
+ if (attach) {
+ final String classifier = nullIfAbsent(getClassifier());
+ for (Map.Entry entry : jarFiles.entrySet()) {
+ String moduleName = entry.getKey();
+ ProducedArtifact artifact;
+ if (moduleName == null && classifier == null) {
+ if (projectHasAlreadySetAnArtifact()) {
+ throw new MojoException("You have to use a classifier "
+ + "to attach supplemental artifacts to the project instead of replacing them.");
+ }
+ artifact = project.getMainArtifact().orElseThrow();
+ } else {
+ /*
+ * TODO: we need to generate artifact with dependencies filtered from the module-info.
+ */
artifact = session.createProducedArtifact(
project.getGroupId(),
- project.getArtifactId(),
+ (moduleName != null) ? moduleName : project.getArtifactId(),
project.getVersion(),
classifier,
null,
getType());
- } else {
- if (projectHasAlreadySetAnArtifact()) {
- throw new MojoException("You have to use a classifier "
- + "to attach supplemental artifacts to the project instead of replacing them.");
- }
- artifact = project.getMainArtifact().get();
}
- projectManager.attachArtifact(project, artifact, jarFile);
- } else {
- getLog().debug("Skipping attachment of the " + getType() + " artifact to the project.");
+ projectManager.attachArtifact(project, artifact, entry.getValue());
}
+ } else {
+ getLog().debug("Skipping attachment of the " + getType() + " artifact to the project.");
}
}
- private static boolean isEmpty(Path directory) {
- if (!Files.isDirectory(directory)) {
- return true;
- }
- try (Stream children = Files.list(directory)) {
- return children.findAny().isEmpty();
- } catch (IOException e) {
- throw new MavenArchiverException("Unable to access directory", e);
- }
- }
-
+ /**
+ * Verifies whether the main artifact is already set.
+ * This verification does not apply for module hierarchy, where more than one artifact is produced.
+ */
private boolean projectHasAlreadySetAnArtifact() {
- Path path = projectManager.getPath(project).orElse(null);
- return path != null && Files.isRegularFile(path);
+ return projectManager.getPath(project).filter(Files::isRegularFile).isPresent();
}
/**
- * Return {@code true} in case where the classifier is not {@code null} and contains something else than white spaces.
- *
- * @param classifier the classifier to verify
- * @return {@code true} if the classifier is set
+ * Returns the given value if non-null, non-empty and non-blank, or {@code null} otherwise.
*/
- private static boolean hasClassifier(String classifier) {
- return classifier != null && !classifier.isBlank();
- }
-
- private String[] getIncludes() {
- if (includes != null && includes.length > 0) {
- return includes;
- }
- return DEFAULT_INCLUDES;
- }
-
- private String[] getExcludes() {
- if (excludes != null && excludes.length > 0) {
- return excludes;
- }
- return DEFAULT_EXCLUDES;
+ static String nullIfAbsent(String value) {
+ return (value == null || value.isBlank()) ? null : value;
}
}
diff --git a/src/main/java/org/apache/maven/plugins/jar/Archive.java b/src/main/java/org/apache/maven/plugins/jar/Archive.java
new file mode 100644
index 00000000..feb7c785
--- /dev/null
+++ b/src/main/java/org/apache/maven/plugins/jar/Archive.java
@@ -0,0 +1,547 @@
+/*
+ * 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.BufferedWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.jar.Attributes;
+import java.util.jar.Manifest;
+
+import org.apache.maven.api.annotations.Nonnull;
+import org.apache.maven.api.annotations.Nullable;
+import org.apache.maven.api.plugin.Log;
+
+/**
+ * Files or root directories to archive for a single module.
+ * A single instance of {@code Archive} may contain many directories for different target Java releases.
+ * Many instances of {@code Archive} may exist when archiving a multi-modules project.
+ */
+final class Archive {
+ /**
+ * The JAR file to create. May be an existing file,
+ * in which case the file creation may be skipped if the file is still up-to-date.
+ */
+ @Nonnull
+ final Path jarFile;
+
+ /**
+ * A helper class for checking whether an existing JAR file is still up-to-date.
+ * This is null if there is no existing JAR file, or if we determined that the file is outdated.
+ */
+ private TimestampCheck existingJAR;
+
+ /**
+ * Name of the module being archived when the project is using module hierarchy.
+ * This is {@code null} if the project is using package hierarchy, either because it is a classical
+ * class-path project or because it is a single module compiled without using the module hierarchy.
+ * When using module source hierarchy, {@code javac} guarantees that the module name in the output
+ * directory is the name of the parent directory of {@code module-info.class}.
+ */
+ @Nullable
+ final String moduleName;
+
+ /**
+ * Path to {@code META-INF/MANIFEST.MF}, or {@code null} if none. The manifest file
+ * should be included by the {@code --manifest} option instead of as an ordinary file.
+ */
+ @Nullable
+ private Path manifest;
+
+ /**
+ * The Maven generated {@code pom.xml} and {@code pom.properties} files, or {@code null} if none.
+ * This first item shall be the base directory where the files are located.
+ */
+ @Nullable
+ List mavenFiles;
+
+ /**
+ * Fully-qualified name of the main class, or {@code null} if none.
+ * This is the value to provide to the {@code --main-class} option.
+ */
+ private String mainClass;
+
+ /**
+ * Files or root directories to store in the JAR file for targeting the base Java release.
+ */
+ @Nonnull
+ final FileSet baseRelease;
+
+ /**
+ * Files or root directories to store in the JAR file for each target Java release
+ * other than the base release.
+ *
+ *
Note on duplicated versions
+ * In principle, we should not have two elements with the same {@link FileSet#version} value.
+ * However, while it should not happen in default Maven builds, we do not forbid the case where
+ * the same version would be defined in {@code "./META-INF"} and {@code ".//META-INF"}.
+ * In such case, two {@code FileSet} instances would exist for the same Java release but with
+ * two different {@link FileSet#directory} values.
+ */
+ @Nonnull
+ private final List additionalReleases;
+
+ /**
+ * Files or root directories to archive for a single target Java release of a single module.
+ * The {@link Archive} enclosing shall contain at least one instance of {@code FileSet} for
+ * the base release, and an arbitrary amount of other instances for other target releases.
+ */
+ final class FileSet {
+ /**
+ * The target Java release, or {@code null} for the base version of the JAR file.
+ */
+ @Nullable
+ final Runtime.Version version;
+
+ /**
+ * The root directory of all files or directories to archive.
+ * This is the value to pass to the {@code -C} tool option.
+ */
+ @Nonnull
+ private final Path directory;
+
+ /**
+ * The files or directories to include in the JAR file.
+ * May be absolute paths or paths relative to {@link #directory}.
+ */
+ @Nonnull
+ private final List files;
+
+ /**
+ * Creates an initially empty set of files or directories for the specified target Java release.
+ *
+ * @param directory the base directory of the files or directories to archive
+ * @param version the target Java release, or {@code null} for the base version of the JAR file
+ */
+ private FileSet(final Path directory, final Runtime.Version version) {
+ this.version = version;
+ this.directory = directory;
+ this.files = new ArrayList<>();
+ }
+
+ /**
+ * Finds a common directory for all remaining files, then clears the list of file.
+ * The common directory can be used for logging a warning message.
+ *
+ * @param base base directory found by previous invocations of this method, or {@code null} if none
+ * @return common directory of remaining files
+ */
+ private Path clear(Path base) {
+ for (Path file : files) {
+ base = findCommonBaseDirectory(base, directory.resolve(file));
+ }
+ files.clear();
+ return base;
+ }
+
+ /**
+ * Returns files as values, together with the base directory (as key) for resolving relative files.
+ */
+ private Map.Entry> files() {
+ return Map.entry(directory, files);
+ }
+
+ /**
+ * Adds the given path to the list of files or directories to archive.
+ * This method may store a relative path instead of the absolute path.
+ *
+ * @param item a file or directory to archive
+ * @param attributes the file's basic attributes
+ * @param isDirectory whether the file is a directory
+ * @throws IllegalArgumentException if the given path cannot be made relative to the base directory
+ */
+ void add(Path item, final BasicFileAttributes attributes, final boolean isDirectory) {
+ TimestampCheck tc = existingJAR;
+ if (tc != null && tc.isUpdated(item, attributes, isDirectory)) {
+ existingJAR = null; // Signal that the existing file is outdated.
+ }
+ if (files.isEmpty()) {
+ /*
+ * In our tests, it seems that the first file after the "-C" option needs to be relative
+ * to the directory given to "-C" and all other files need to be absolute. This behavior
+ * does not seem to be documented, but we couldn't get the "jar" tool to work otherwise
+ * (except by repeating "-C" before each file).
+ */
+ item = directory.relativize(item);
+ }
+ files.add(item);
+ }
+
+ /**
+ * Adds to the given list the arguments to provide to the "jar" tool for this version.
+ * Elements added to the list shall be instances of {@link String} or {@link Path}.
+ *
+ * @param addTo the list where to add the arguments as {@link String} or {@link Path} instances
+ * @param versioned whether to add arguments for the version specified by {@linkplain #version}
+ * @return whether at least one file has been added as argument
+ */
+ private boolean arguments(final List