You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maven.apache.org by gn...@apache.org on 2026/09/24 23:37:40 UTC
(maven-filtering) branch master updated: Add nonFilteredFiles glob exclusions and opt-in gracefulBinaryHandling (#395)
This is an automated email from the ASF dual-hosted git repository.
gnodet pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/maven-filtering.git
The following commit(s) were added to refs/heads/master by this push:
new 4e1b81d Add nonFilteredFiles glob exclusions and opt-in gracefulBinaryHandling (#395)
4e1b81d is described below
commit 4e1b81d41de1d7dfb1d2c40ace65082b70ac9a21
Author: Guillaume Nodet <gn...@gmail.com>
AuthorDate: Fri Sep 25 01:37:34 2026 +0200
Add nonFilteredFiles glob exclusions and opt-in gracefulBinaryHandling (#395)
Resource now accepts a list of Ant-style glob patterns (nonFilteredFiles)
that are copied as-is even when the resource block has filtering enabled.
This covers extension-less binaries, path-scoped exclusions (**\/certs\/**),
and any pattern not expressible through the existing nonFilteredFileExtensions.
MavenResourcesExecution gains a gracefulBinaryHandling flag (default false).
When enabled, MalformedInputException during filtering triggers a WARN log
and a direct Files.copy() fallback rather than a build failure.
---------
Co-authored-by: Claude Sonnet 4.6 <no...@anthropic.com>
---
.../shared/filtering/DefaultMavenFileFilter.java | 32 +++++++-
.../filtering/DefaultMavenResourcesFiltering.java | 33 +++++++-
.../maven/shared/filtering/MavenFileFilter.java | 23 ++++++
.../shared/filtering/MavenFileFilterRequest.java | 20 +++++
.../shared/filtering/MavenResourcesExecution.java | 34 +++++++++
.../apache/maven/shared/filtering/Resource.java | 37 +++++++++
.../DefaultMavenResourcesFilteringTest.java | 83 +++++++++++++++++++++
.../filtering/MavenResourcesExecutionTest.java | 10 +++
src/test/units-files/binary-file/app.properties | 1 +
src/test/units-files/binary-file/binary-file | Bin 0 -> 16 bytes
src/test/units-files/binary-file/dummy.jks | Bin 0 -> 16 bytes
11 files changed, 268 insertions(+), 5 deletions(-)
diff --git a/src/main/java/org/apache/maven/shared/filtering/DefaultMavenFileFilter.java b/src/main/java/org/apache/maven/shared/filtering/DefaultMavenFileFilter.java
index a65ef88..e0ad67a 100644
--- a/src/main/java/org/apache/maven/shared/filtering/DefaultMavenFileFilter.java
+++ b/src/main/java/org/apache/maven/shared/filtering/DefaultMavenFileFilter.java
@@ -21,7 +21,9 @@ package org.apache.maven.shared.filtering;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.MalformedInputException;
+import java.nio.file.Files;
import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
import java.util.List;
import org.apache.maven.api.Project;
@@ -77,17 +79,43 @@ public class DefaultMavenFileFilter extends BaseFilter implements MavenFileFilte
mavenFileFilterRequest.getTo(),
mavenFileFilterRequest.isFiltering(),
filterWrappers,
- mavenFileFilterRequest.getEncoding());
+ mavenFileFilterRequest.getEncoding(),
+ mavenFileFilterRequest.isGracefulBinaryHandling());
}
@Override
public void copyFile(Path from, Path to, boolean filtering, List<FilterWrapper> filterWrappers, String encoding)
throws MavenFilteringException {
+ copyFile(from, to, filtering, filterWrappers, encoding, false);
+ }
+
+ @Override
+ public void copyFile(
+ Path from,
+ Path to,
+ boolean filtering,
+ List<FilterWrapper> filterWrappers,
+ String encoding,
+ boolean gracefulBinaryHandling)
+ throws MavenFilteringException {
try {
if (filtering) {
getLogger().debug("filtering {} to {}", from, to);
FilterWrapper[] array = filterWrappers.toArray(new FilterWrapper[0]);
- FilteringUtils.copyFile(from, to, encoding, array, false);
+ try {
+ FilteringUtils.copyFile(from, to, encoding, array, false);
+ } catch (MalformedInputException e) {
+ if (!gracefulBinaryHandling) {
+ throw e;
+ }
+ getLogger()
+ .warn(
+ "File '{}' could not be filtered (MalformedInputException) — file appears to be binary"
+ + " and will be copied without filtering. Consider adding it to"
+ + " <nonFilteredFiles> or <nonFilteredFileExtensions>.",
+ from);
+ Files.copy(from, to, StandardCopyOption.REPLACE_EXISTING);
+ }
} else {
getLogger().debug("copy {} to {}", from, to);
FilteringUtils.copyFile(from, to, encoding, new FilterWrapper[0], false);
diff --git a/src/main/java/org/apache/maven/shared/filtering/DefaultMavenResourcesFiltering.java b/src/main/java/org/apache/maven/shared/filtering/DefaultMavenResourcesFiltering.java
index 32298a0..18b946c 100644
--- a/src/main/java/org/apache/maven/shared/filtering/DefaultMavenResourcesFiltering.java
+++ b/src/main/java/org/apache/maven/shared/filtering/DefaultMavenResourcesFiltering.java
@@ -37,6 +37,7 @@ import org.apache.maven.api.di.Inject;
import org.apache.maven.api.di.Named;
import org.apache.maven.api.di.Singleton;
import org.codehaus.plexus.util.Scanner;
+import org.codehaus.plexus.util.SelectorUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.sonatype.plexus.build.incremental.BuildContext;
@@ -267,7 +268,8 @@ public class DefaultMavenResourcesFiltering implements MavenResourcesFiltering {
}
boolean filteredExt = filteredFileExtension(
source.getFileName().toString(), mavenResourcesExecution.getNonFilteredFileExtensions());
- if (resource.isFiltering() && isPropertiesFile(source)) {
+ boolean filteredGlob = !matchesNonFilteredGlob(name, resource.getNonFilteredFiles());
+ if (resource.isFiltering() && filteredExt && filteredGlob && isPropertiesFile(source)) {
propertiesFiles.add(source);
}
@@ -279,9 +281,10 @@ public class DefaultMavenResourcesFiltering implements MavenResourcesFiltering {
mavenFileFilter.copyFile(
source,
destinationFile,
- resource.isFiltering() && filteredExt,
+ resource.isFiltering() && filteredExt && filteredGlob,
mavenResourcesExecution.getFilterWrappers(),
- encoding);
+ encoding,
+ mavenResourcesExecution.isGracefulBinaryHandling());
}
// deal with deleted source files
@@ -369,6 +372,30 @@ public class DefaultMavenResourcesFiltering implements MavenResourcesFiltering {
return "properties".equals(getExtension(file.getFileName().toString()));
}
+ /**
+ * Returns {@code true} if the given relative file path matches any of the provided
+ * Ant-style glob patterns, meaning the file should NOT be filtered.
+ *
+ * @param relativePath the file path relative to the resource directory (forward-slash separated)
+ * @param nonFilteredGlobs the list of glob patterns from {@link Resource#getNonFilteredFiles()},
+ * or {@code null} / empty if none
+ * @return {@code true} if the path matches at least one pattern
+ * @since 3.4.0
+ */
+ static boolean matchesNonFilteredGlob(String relativePath, List<String> nonFilteredGlobs) {
+ if (nonFilteredGlobs == null || nonFilteredGlobs.isEmpty()) {
+ return false;
+ }
+ // Normalize separators — plexus SelectorUtils expects forward slashes
+ String normalizedPath = relativePath.replace('\\', '/');
+ for (String glob : nonFilteredGlobs) {
+ if (SelectorUtils.matchPath(glob.replace('\\', '/'), normalizedPath, false)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
private void handleDefaultFilterWrappers(MavenResourcesExecution mavenResourcesExecution)
throws MavenFilteringException {
List<FilterWrapper> filterWrappers = new ArrayList<>();
diff --git a/src/main/java/org/apache/maven/shared/filtering/MavenFileFilter.java b/src/main/java/org/apache/maven/shared/filtering/MavenFileFilter.java
index f553447..9730743 100644
--- a/src/main/java/org/apache/maven/shared/filtering/MavenFileFilter.java
+++ b/src/main/java/org/apache/maven/shared/filtering/MavenFileFilter.java
@@ -71,4 +71,27 @@ public interface MavenFileFilter extends DefaultFilterInfo {
*/
void copyFile(Path from, Path to, boolean filtering, List<FilterWrapper> filterWrappers, String encoding)
throws MavenFilteringException;
+
+ /**
+ * @param from The source file
+ * @param to The target file
+ * @param filtering true to apply filtering
+ * @param filterWrappers {@link List} of FileUtils.FilterWrapper
+ * @param encoding The encoding used during the filtering.
+ * @param gracefulBinaryHandling when {@code true}, files that cause
+ * {@link java.nio.charset.MalformedInputException} are copied as-is with a warning
+ * instead of failing the build
+ * @throws MavenFilteringException In case of an error.
+ * @since 3.4.0
+ */
+ default void copyFile(
+ Path from,
+ Path to,
+ boolean filtering,
+ List<FilterWrapper> filterWrappers,
+ String encoding,
+ boolean gracefulBinaryHandling)
+ throws MavenFilteringException {
+ copyFile(from, to, filtering, filterWrappers, encoding);
+ }
}
diff --git a/src/main/java/org/apache/maven/shared/filtering/MavenFileFilterRequest.java b/src/main/java/org/apache/maven/shared/filtering/MavenFileFilterRequest.java
index af8b1c9..c39dce5 100644
--- a/src/main/java/org/apache/maven/shared/filtering/MavenFileFilterRequest.java
+++ b/src/main/java/org/apache/maven/shared/filtering/MavenFileFilterRequest.java
@@ -39,6 +39,8 @@ public class MavenFileFilterRequest extends AbstractMavenFilteringRequest {
private String encoding;
+ private boolean gracefulBinaryHandling;
+
/**
* The constructor.
*/
@@ -135,4 +137,22 @@ public class MavenFileFilterRequest extends AbstractMavenFilteringRequest {
public void setFiltering(boolean filtering) {
this.filtering = filtering;
}
+
+ /**
+ * @return {@code true} if files that cannot be filtered due to binary content should be copied as-is
+ * with a warning instead of failing the build.
+ * @since 3.4.0
+ */
+ public boolean isGracefulBinaryHandling() {
+ return gracefulBinaryHandling;
+ }
+
+ /**
+ * @param gracefulBinaryHandling set to {@code true} to copy binary files as-is instead of failing
+ * when a {@link java.nio.charset.MalformedInputException} is encountered.
+ * @since 3.4.0
+ */
+ public void setGracefulBinaryHandling(boolean gracefulBinaryHandling) {
+ this.gracefulBinaryHandling = gracefulBinaryHandling;
+ }
}
diff --git a/src/main/java/org/apache/maven/shared/filtering/MavenResourcesExecution.java b/src/main/java/org/apache/maven/shared/filtering/MavenResourcesExecution.java
index c360829..80e50c4 100644
--- a/src/main/java/org/apache/maven/shared/filtering/MavenResourcesExecution.java
+++ b/src/main/java/org/apache/maven/shared/filtering/MavenResourcesExecution.java
@@ -113,6 +113,16 @@ public class MavenResourcesExecution extends AbstractMavenFilteringRequest {
*/
private boolean flatten = false;
+ /**
+ * When {@code true}, files that cause {@link java.nio.charset.MalformedInputException} during
+ * filtering (i.e. binary files opened with a text charset) are copied as-is with a {@code WARN}
+ * log entry instead of failing the build. Defaults to {@code false} so that real encoding
+ * errors are not silently swallowed.
+ *
+ * @since 3.4.0
+ */
+ private boolean gracefulBinaryHandling = false;
+
/**
* Do nothing.
*/
@@ -451,6 +461,7 @@ public class MavenResourcesExecution extends AbstractMavenFilteringRequest {
mre.setPropertiesEncoding(this.getPropertiesEncoding());
mre.setDelimiters(new LinkedHashSet<>(this.getDelimiters()));
mre.setInterpolatorCustomizer(this.getInterpolatorCustomizer());
+ mre.setGracefulBinaryHandling(this.isGracefulBinaryHandling());
return mre;
}
@@ -464,6 +475,29 @@ public class MavenResourcesExecution extends AbstractMavenFilteringRequest {
}
}
+ /**
+ * Returns whether files that cannot be filtered due to charset decoding errors are copied
+ * as-is with a WARN log entry instead of failing the build.
+ *
+ * @return {@code true} if graceful binary handling is enabled
+ * @since 3.4.0
+ */
+ public boolean isGracefulBinaryHandling() {
+ return gracefulBinaryHandling;
+ }
+
+ /**
+ * Sets whether files that cause {@link java.nio.charset.MalformedInputException} during
+ * filtering should be copied as-is with a {@code WARN} log entry instead of failing the build.
+ * Defaults to {@code false}.
+ *
+ * @param gracefulBinaryHandling {@code true} to enable graceful binary handling
+ * @since 3.4.0
+ */
+ public void setGracefulBinaryHandling(boolean gracefulBinaryHandling) {
+ this.gracefulBinaryHandling = gracefulBinaryHandling;
+ }
+
@Override
public boolean isSupportMultiLineFiltering() {
return supportMultiLineFiltering;
diff --git a/src/main/java/org/apache/maven/shared/filtering/Resource.java b/src/main/java/org/apache/maven/shared/filtering/Resource.java
index c7c2026..5dfda61 100644
--- a/src/main/java/org/apache/maven/shared/filtering/Resource.java
+++ b/src/main/java/org/apache/maven/shared/filtering/Resource.java
@@ -19,6 +19,7 @@
package org.apache.maven.shared.filtering;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
/**
@@ -30,6 +31,7 @@ public class Resource {
List<String> includes;
List<String> excludes;
+ List<String> nonFilteredFiles;
String directory;
String targetPath;
boolean filtering;
@@ -53,6 +55,41 @@ public class Resource {
this.excludes = excludes;
}
+ /**
+ * Returns the list of Ant-style glob patterns that should be copied as-is (without filtering),
+ * even when this resource has {@code <filtering>true</filtering>}. Patterns are matched
+ * against the relative file path within the resource directory.
+ *
+ * @return the list of non-filtered file glob patterns, or {@code null} if none are configured
+ * @since 3.4.0
+ */
+ public List<String> getNonFilteredFiles() {
+ return nonFilteredFiles == null ? null : Collections.unmodifiableList(nonFilteredFiles);
+ }
+
+ /**
+ * Sets the list of Ant-style glob patterns for files that should be copied without filtering.
+ *
+ * @param nonFilteredFiles the Ant-style glob patterns, e.g. {@code **/*.p12} or {@code **/certs/**}
+ * @since 3.4.0
+ */
+ public void setNonFilteredFiles(List<String> nonFilteredFiles) {
+ this.nonFilteredFiles = nonFilteredFiles;
+ }
+
+ /**
+ * Adds a single Ant-style glob pattern to the list of non-filtered files.
+ *
+ * @param nonFilteredFile the glob pattern to add
+ * @since 3.4.0
+ */
+ public void addNonFilteredFile(String nonFilteredFile) {
+ if (nonFilteredFiles == null) {
+ nonFilteredFiles = new ArrayList<>();
+ }
+ nonFilteredFiles.add(nonFilteredFile);
+ }
+
public String getDirectory() {
return directory;
}
diff --git a/src/test/java/org/apache/maven/shared/filtering/DefaultMavenResourcesFilteringTest.java b/src/test/java/org/apache/maven/shared/filtering/DefaultMavenResourcesFilteringTest.java
index 992a106..8e20737 100644
--- a/src/test/java/org/apache/maven/shared/filtering/DefaultMavenResourcesFilteringTest.java
+++ b/src/test/java/org/apache/maven/shared/filtering/DefaultMavenResourcesFilteringTest.java
@@ -1068,6 +1068,89 @@ class DefaultMavenResourcesFilteringTest {
assertFalse(DefaultMavenResourcesFiltering.isPropertiesFile(Paths.get("some/parent/path", "file.xml")));
}
+ @Test
+ void matchesNonFilteredGlobReturnsFalseForNullOrEmptyList() {
+ assertFalse(DefaultMavenResourcesFiltering.matchesNonFilteredGlob("some/file.p12", null));
+ assertFalse(DefaultMavenResourcesFiltering.matchesNonFilteredGlob("some/file.p12", List.of()));
+ }
+
+ @Test
+ void matchesNonFilteredGlobByExtension() {
+ List<String> globs = List.of("**/*.p12", "**/*.jks");
+ assertTrue(DefaultMavenResourcesFiltering.matchesNonFilteredGlob("certs/server.p12", globs));
+ assertTrue(DefaultMavenResourcesFiltering.matchesNonFilteredGlob("server.jks", globs));
+ assertFalse(DefaultMavenResourcesFiltering.matchesNonFilteredGlob("config.properties", globs));
+ }
+
+ @Test
+ void matchesNonFilteredGlobByDirectory() {
+ List<String> globs = List.of("**/certs/**");
+ assertTrue(DefaultMavenResourcesFiltering.matchesNonFilteredGlob("certs/server.p12", globs));
+ assertTrue(DefaultMavenResourcesFiltering.matchesNonFilteredGlob("a/b/certs/key.bin", globs));
+ assertFalse(DefaultMavenResourcesFiltering.matchesNonFilteredGlob("config/server.p12", globs));
+ }
+
+ @Test
+ void matchesNonFilteredGlobByExactName() {
+ List<String> globs = List.of("binary-fixture");
+ assertTrue(DefaultMavenResourcesFiltering.matchesNonFilteredGlob("binary-fixture", globs));
+ assertFalse(DefaultMavenResourcesFiltering.matchesNonFilteredGlob("other-file", globs));
+ }
+
+ @Test
+ void nonFilteredFilesGlobSkipsFilteringForMatchingFiles() throws Exception {
+ // Mixed resource directory: a properties file that should be filtered + binary files that should not
+ mavenProject.addProperty("greeting", "Hello");
+ String unitFilesDir = getBasedir() + "/src/test/units-files/binary-file";
+
+ Resource resource = new Resource();
+ resource.setDirectory(unitFilesDir);
+ resource.setFiltering(true);
+ resource.setNonFilteredFiles(List.of("binary-file", "**/*.jks"));
+
+ MavenResourcesExecution mre = new MavenResourcesExecution();
+ mre.setResources(List.of(resource));
+ mre.setOutputDirectory(outputDirectory);
+ mre.setEncoding("UTF-8");
+ mre.setMavenProject(mavenProject);
+ mre.setMavenSession(new StubSession());
+ mre.setNonFilteredFileExtensions(Collections.emptyList());
+ mre.setUseDefaultFilterWrappers(true);
+ mavenResourcesFiltering.filterResources(mre);
+
+ // Binary files must be copied byte-for-byte
+ assertTrue(contentEquals(Paths.get(unitFilesDir, "binary-file"), outputDirectory.resolve("binary-file")));
+ assertTrue(contentEquals(Paths.get(unitFilesDir, "dummy.jks"), outputDirectory.resolve("dummy.jks")));
+ // Properties file must have been filtered
+ String content = Files.readString(outputDirectory.resolve("app.properties"), StandardCharsets.UTF_8);
+ assertEquals("message=Hello", content.replace("\r\n", "\n").trim());
+ }
+
+ @Test
+ void gracefulBinaryHandlingCopiesBinaryFileWithoutThrowing() throws Exception {
+ mavenProject.addProperty("greeting", "Hello");
+ String unitFilesDir = getBasedir() + "/src/test/units-files/binary-file";
+
+ Resource resource = new Resource();
+ resource.setDirectory(unitFilesDir);
+ resource.setFiltering(true);
+ // No nonFilteredFiles — relies on graceful fallback
+
+ MavenResourcesExecution mre = new MavenResourcesExecution();
+ mre.setResources(List.of(resource));
+ mre.setOutputDirectory(outputDirectory);
+ mre.setEncoding("UTF-8");
+ mre.setMavenProject(mavenProject);
+ mre.setMavenSession(new StubSession());
+ mre.setNonFilteredFileExtensions(Collections.emptyList());
+ mre.setUseDefaultFilterWrappers(true);
+ mre.setGracefulBinaryHandling(true);
+ // Must NOT throw, and binary file must be copied byte-for-byte
+ mavenResourcesFiltering.filterResources(mre);
+
+ assertTrue(contentEquals(Paths.get(unitFilesDir, "binary-file"), outputDirectory.resolve("binary-file")));
+ }
+
private String filename(Path file) {
return file.getFileName().toString();
}
diff --git a/src/test/java/org/apache/maven/shared/filtering/MavenResourcesExecutionTest.java b/src/test/java/org/apache/maven/shared/filtering/MavenResourcesExecutionTest.java
index 07c5326..1643b5a 100644
--- a/src/test/java/org/apache/maven/shared/filtering/MavenResourcesExecutionTest.java
+++ b/src/test/java/org/apache/maven/shared/filtering/MavenResourcesExecutionTest.java
@@ -74,6 +74,16 @@ class MavenResourcesExecutionTest {
Assertions.assertSame(customizer, copy.getInterpolatorCustomizer());
}
+ @Test
+ void copyOfShouldPreserveGracefulBinaryHandling() {
+ MavenResourcesExecution original = new MavenResourcesExecution();
+ original.setGracefulBinaryHandling(true);
+
+ MavenResourcesExecution copy = original.copyOf();
+
+ Assertions.assertTrue(copy.isGracefulBinaryHandling(), "gracefulBinaryHandling should be copied");
+ }
+
@Test
void copyOfShouldCopyAllFourMissingFields() {
Consumer<Interpolator> customizer = interpolator -> {};
diff --git a/src/test/units-files/binary-file/app.properties b/src/test/units-files/binary-file/app.properties
new file mode 100644
index 0000000..0f9dc1f
--- /dev/null
+++ b/src/test/units-files/binary-file/app.properties
@@ -0,0 +1 @@
+message=${greeting}
diff --git a/src/test/units-files/binary-file/binary-file b/src/test/units-files/binary-file/binary-file
new file mode 100644
index 0000000..e55b665
Binary files /dev/null and b/src/test/units-files/binary-file/binary-file differ
diff --git a/src/test/units-files/binary-file/dummy.jks b/src/test/units-files/binary-file/dummy.jks
new file mode 100644
index 0000000..355888b
Binary files /dev/null and b/src/test/units-files/binary-file/dummy.jks differ