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 **&#47;*.p12} or {@code **&#47;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