JCRVLT-495 allow to limit filtering to certain file patterns (#52)

diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..6b9aca9
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,5 @@
+# Set the default behavior, in case people don't have core.autocrlf set.
+* text=auto
+
+# all XML files should keep lf newlines, even on Windows
+*.xml text eol=lf
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index 1bfdfdb..bd05a83 100644
--- a/pom.xml
+++ b/pom.xml
@@ -344,12 +344,12 @@
         <dependency>
             <groupId>org.apache.maven</groupId>
             <artifactId>maven-archiver</artifactId>
-            <version>3.5.0</version>
+            <version>3.5.1</version>
         </dependency>
         <dependency>
             <groupId>org.codehaus.plexus</groupId>
             <artifactId>plexus-archiver</artifactId>
-            <version>4.2.0</version>
+            <version>4.2.3</version>
         </dependency>
         <dependency>
             <groupId>org.codehaus.plexus</groupId>
diff --git a/src/main/java/org/apache/jackrabbit/filevault/maven/packaging/VaultMojo.java b/src/main/java/org/apache/jackrabbit/filevault/maven/packaging/VaultMojo.java
index 914ba15..d08df70 100644
--- a/src/main/java/org/apache/jackrabbit/filevault/maven/packaging/VaultMojo.java
+++ b/src/main/java/org/apache/jackrabbit/filevault/maven/packaging/VaultMojo.java
@@ -63,6 +63,7 @@
 import org.codehaus.plexus.archiver.util.DefaultFileSet;
 import org.codehaus.plexus.util.DirectoryScanner;
 import org.codehaus.plexus.util.FileUtils;
+import org.codehaus.plexus.util.MatchPatterns;
 import org.codehaus.plexus.util.StringUtils;
 import org.jetbrains.annotations.NotNull;
 
@@ -107,12 +108,14 @@
     private File outputDirectory;
 
     /** Enables resource filtering on the meta-inf source files similar to what the <a href="https://maven.apache.org/plugins/maven-resources-plugin/examples/filter.html">maven-resources-plugin</a> does.
+     * It is recommended to limit filtering with {@link #filteredFilePatterns} and {@link #nonFilteredFileExtensions}.
      * @since 1.1.0 
      */
     @Parameter(property = "vault.enableMetaInfFiltering", defaultValue = "false")
     private boolean enableMetaInfFiltering;
 
     /** Enables resource filtering on the {@link AbstractSourceAndMetadataPackageMojo#jcrRootSourceDirectory} source files similar to what the <a href="https://maven.apache.org/plugins/maven-resources-plugin/examples/filter.html">maven-resources-plugin</a> does.
+     * It is recommended to limit filtering with {@link #filteredFilePatterns} and {@link #nonFilteredFileExtensions}.
      * @since 1.1.0 
      */
     @Parameter(property = "vault.enableJcrRootFiltering", defaultValue = "false") 
@@ -171,12 +174,23 @@
     @Parameter(property="vault.escapedBackslashesInFilePath", defaultValue = "false")
     private boolean escapedBackslashesInFilePath;
 
-    /** Additional list of file extensions that should not be filtered.
-     * (already defined are : jpg, jpeg, gif, bmp, png)
+    /** Additional list of file extensions that should not be filtered, e.g. binaries.
+     * Already predefined as extensions which should never be filtered are: jpg, jpeg, gif, bmp, png, ico.
+     * Instead of using this deny list approach for binary files and others which should not be filtered, 
+     * consider using an allow list via {@link #filteredFilePatterns} instead.
      * @since 1.1.0 */
     @Parameter(property="vault.nonFilteredFileExtensions")
     private List<String> nonFilteredFileExtensions;
 
+    /** 
+     * Restricts the files which should be filtered to the ones having matching one of the given <a href="http://ant.apache.org/manual/dirtasks.html#patterns">Ant patterns</a>.
+     * Evaluated before {@link #nonFilteredFileExtensions}.
+     * All patterns are relative to the root paths (given through the filter.xml root entries or the META-INF directory).
+     * If empty or not set all files except for the ones from {@link #nonFilteredFileExtensions} are filtered.
+     * @since 1.1.8 */
+    @Parameter(property="vault.filteredFilePatterns")
+    private List<String> filteredFilePatterns;
+
     /** Stop searching endToken at the end of line when filtering is applied.
      * 
      * @since 1.1.0 */
@@ -248,13 +262,16 @@
         Path destFile = Paths.get(destFileName);
         if ((destFile.startsWith(Constants.ROOT_DIR) && enableJcrRootFiltering) ||
             (destFile.startsWith(Constants.META_INF) && enableMetaInfFiltering)) {
-            getLog().info("Apply filtering to " + getProjectRelativeFilePath(sourceFile));
-            Resource resource = new Resource();
-            resource.setDirectory(sourceFile.getParent());
-            resource.setIncludes(Collections.singletonList(sourceFile.getName()));
-            resource.setFiltering(true);
-            File newTargetDirectory = applyFiltering(destFile.getParent().toString(), mavenResourcesExecution, resource);
-            sourceFile = new File(newTargetDirectory, sourceFile.getName());
+            MatchPatterns matchPatterns = MatchPatterns.from(filteredFilePatterns);
+            if (filteredFilePatterns == null || matchPatterns.matches(sourceFile.toString(), true)) {
+                getLog().info("Apply filtering to " + getProjectRelativeFilePath(sourceFile));
+                Resource resource = new Resource();
+                resource.setDirectory(sourceFile.getParent());
+                resource.setIncludes(Collections.singletonList(sourceFile.getName()));
+                resource.setFiltering(true);
+                File newTargetDirectory = applyFiltering(destFile.getParent().toString(), mavenResourcesExecution, resource);
+                sourceFile = new File(newTargetDirectory, sourceFile.getName());
+            }
         }
         getLog().debug("Adding file " + getProjectRelativeFilePath(sourceFile) + " to package at " + destFileName + "'");
         archiver.addFile(sourceFile, destFileName);
@@ -274,21 +291,60 @@
             (fileSet.getPrefix().startsWith(Constants.META_INF) && enableMetaInfFiltering)) {
             
             getLog().info("Apply filtering to FileSet below " + getProjectRelativeFilePath(fileSet.getDirectory()));
-            Resource resource = new Resource();
-            resource.setDirectory(fileSet.getDirectory().getPath());
-            if (fileSet.getIncludes() != null) {
-                resource.setIncludes(Arrays.asList(fileSet.getIncludes()));
+            Resource filteringSourceResource = new Resource();
+            filteringSourceResource.setDirectory(fileSet.getDirectory().getPath());
+            
+            // since allow lists (i.e. only filtering specific extensions) is not natively supported
+            // split up the fileSet
+            if (filteredFilePatterns != null && !filteredFilePatterns.isEmpty()) {
+                if (fileSet.getIncludes() != null) {
+                    throw new IllegalStateException("FileSet can not have includes set, as those are used for filteredFileExtensions");
+                }
+                // create an additional file set with unfiltered files
+                DefaultFileSet unfilteredFileSet = cloneFileSet(fileSet);
+                
+                // add all filtered file patterns to excludes
+                String[] excludes = Stream.of(Arrays.asList(fileSet.getExcludes()), filteredFilePatterns).flatMap(x -> x.stream()).toArray(String[]::new);
+                unfilteredFileSet.setExcludes(excludes);
+                
+                getLog().debug("Adding unfiltered fileSet '" + getString(unfilteredFileSet) + "' to package");
+                archiver.addFileSet(unfilteredFileSet);
+                filteringSourceResource.setIncludes(filteredFilePatterns);
+            } else {
+                if (fileSet.getIncludes() != null) {
+                    filteringSourceResource.setIncludes(Arrays.asList(fileSet.getIncludes()));
+                }
             }
+           
             if (fileSet.getExcludes() != null) {
-                resource.setExcludes(Arrays.asList(fileSet.getExcludes()));
+                filteringSourceResource.setExcludes(Arrays.asList(fileSet.getExcludes()));
                 // default exclude are managed via mavenResourcesExecution
             }
-            resource.setFiltering(true);
-            File newTargetDirectory = applyFiltering(fileSet.getPrefix(), mavenResourcesExecution, resource);
-            fileSet.setDirectory(newTargetDirectory);
+            filteringSourceResource.setFiltering(true);
+            File newTargetDirectory = applyFiltering(fileSet.getPrefix(), mavenResourcesExecution, filteringSourceResource);
+            if (newTargetDirectory.exists()) {
+                fileSet.setDirectory(newTargetDirectory);
+                getLog().debug("Adding filtered fileSet '" + getString(fileSet) + "' to package");
+                archiver.addFileSet(fileSet);
+            }
+        } else {
+            getLog().debug("Adding fileSet '" + getString(fileSet) + "' to package");
+            archiver.addFileSet(fileSet);
         }
-        getLog().debug("Adding fileSet '" + getString(fileSet) + "' to package");
-        archiver.addFileSet(fileSet);
+    }
+
+    static DefaultFileSet cloneFileSet(DefaultFileSet defaultFileSet) {
+        DefaultFileSet newFileSet = new DefaultFileSet(defaultFileSet.getDirectory());
+        newFileSet.setCaseSensitive(defaultFileSet.isCaseSensitive());
+        newFileSet.setExcludes(defaultFileSet.getExcludes());
+        newFileSet.setFileMappers(defaultFileSet.getFileMappers());
+        newFileSet.setFileSelectors(defaultFileSet.getFileSelectors());
+        newFileSet.setIncludes(defaultFileSet.getIncludes());
+        newFileSet.setIncludingEmptyDirectories(defaultFileSet.isIncludingEmptyDirectories());
+        newFileSet.setPrefix(defaultFileSet.getPrefix());
+        newFileSet.setStreamTransformer(defaultFileSet.getStreamTransformer());
+        newFileSet.setUsingDefaultExcludes(defaultFileSet.isUsingDefaultExcludes());
+        return newFileSet;
     }
 
     private static String getString(FileSet fileSet) {
diff --git a/src/test/java/org/apache/jackrabbit/filevault/maven/packaging/VaultMojoTest.java b/src/test/java/org/apache/jackrabbit/filevault/maven/packaging/VaultMojoTest.java
index 10a319f..7536fe4 100644
--- a/src/test/java/org/apache/jackrabbit/filevault/maven/packaging/VaultMojoTest.java
+++ b/src/test/java/org/apache/jackrabbit/filevault/maven/packaging/VaultMojoTest.java
@@ -21,7 +21,10 @@
 import java.util.Collections;
 import java.util.Set;
 
+import org.apache.commons.lang3.builder.EqualsBuilder;
+import org.apache.commons.lang3.builder.ToStringBuilder;
 import org.apache.jackrabbit.filevault.maven.packaging.it.ProjectBuilder;
+import org.codehaus.plexus.archiver.util.DefaultFileSet;
 import org.hamcrest.Matchers;
 import org.junit.Assert;
 import org.junit.Test;
@@ -45,4 +48,14 @@
         Collection<File> uncoveredFiles = VaultMojo.getUncoveredFiles(sourceDirectory, excludes, "", entryNames);
         Assert.assertThat(uncoveredFiles, Matchers.empty());
     }
+
+    @Test
+    public void testCloneFileSet() {
+        DefaultFileSet fileSet = new DefaultFileSet();
+        fileSet.setIncludes(new String[] { "ab", "c" });
+        fileSet.setExcludes(new String[] { "de", "f" });
+        fileSet.setIncludingEmptyDirectories(true);
+        DefaultFileSet clonedFileSet = VaultMojo.cloneFileSet(fileSet);
+        Assert.assertTrue("Expected " + ToStringBuilder.reflectionToString(fileSet) + " but got " + ToStringBuilder.reflectionToString(clonedFileSet), EqualsBuilder.reflectionEquals(fileSet, clonedFileSet));
+    }
 }
diff --git a/src/test/java/org/apache/jackrabbit/filevault/maven/packaging/it/FilteringIT.java b/src/test/java/org/apache/jackrabbit/filevault/maven/packaging/it/FilteringIT.java
index b7adc82..57fa2cf 100644
--- a/src/test/java/org/apache/jackrabbit/filevault/maven/packaging/it/FilteringIT.java
+++ b/src/test/java/org/apache/jackrabbit/filevault/maven/packaging/it/FilteringIT.java
@@ -18,6 +18,7 @@
 
 import java.io.IOException;
 
+import org.apache.commons.lang3.StringUtils;
 import org.apache.maven.it.VerificationException;
 import org.junit.Test;
 
@@ -26,41 +27,68 @@
  */
 public class FilteringIT {
 
-    private ProjectBuilder verify(String projectName, boolean enableJcrRootFiltering, boolean enableMetaInfFiltering, String ... goals) throws VerificationException, IOException {
-        return new ProjectBuilder()
+    private ProjectBuilder verify(String projectName, boolean enableJcrRootFiltering, boolean enableMetaInfFiltering, String filteredFilePatterns, String ... goals) throws VerificationException, IOException {
+        ProjectBuilder projectBuilder = new ProjectBuilder()
                 .setTestProjectDir("filtering-tests/" + projectName)
                 .setTestGoals(goals)
                 .setProperty("vault.enableMetaInfFiltering", Boolean.toString(enableMetaInfFiltering))
-                .setProperty("vault.enableJcrRootFiltering", Boolean.toString(enableJcrRootFiltering))
+                .setProperty("vault.enableJcrRootFiltering", Boolean.toString(enableJcrRootFiltering));
+        if (StringUtils.isNotBlank(filteredFilePatterns)) {
+            projectBuilder.setProperty("vault.filteredFilePatterns", filteredFilePatterns);
+        }
+        return projectBuilder
                 .build()
                 .verifyExpectedFiles();
     }
 
     @Test
     public void test_simple_filter_with_filtering_enabled() throws Exception {
-        verify("simple-filter", true, true)
+        verify("simple-filter", true, true, null)
+            // cannot check checksum of properties.xml as that one has platform dependent new lines!
+            //.verifyExpectedFileChecksum("META-INF/vault/properties.xml", "295fb69e")
             .verifyExpectedFileChecksum("jcr_root/apps/bar/test1.properties", "10791371")
-            .verifyExpectedFileChecksum("jcr_root/apps/foo/test2.properties", "7563f01d");
+            .verifyExpectedFileChecksum("jcr_root/apps/foo/test2.properties", "7563f01d")
+            .verifyExpectedFileChecksum("jcr_root/apps/foo/child/.content.xml", "d9b1ad2");
     }
 
     @Test
     public void test_simple_filter_with_filtering_disabled() throws Exception {
-        verify("simple-filter", false, false)
-        .verifyExpectedFileChecksum("jcr_root/apps/bar/test1.properties", "34e5a01d")
-        .verifyExpectedFileChecksum("jcr_root/apps/foo/test2.properties", "a41ae6f8");
+        verify("simple-filter", false, false, null)
+            // cannot check checksum of properties.xml as that one has platform dependent new lines!
+            //.verifyExpectedFileChecksum("META-INF/vault/properties.xml", "5953911b")
+            .verifyExpectedFileChecksum("jcr_root/apps/bar/test1.properties", "34e5a01d")
+            .verifyExpectedFileChecksum("jcr_root/apps/foo/test2.properties", "a41ae6f8")
+            .verifyExpectedFileChecksum("jcr_root/apps/foo/child/.content.xml", "f8aee8df");
     }
 
     @Test
-    public void test_simple_filter_with_filtering_partially_enabled() throws Exception {
-        verify("simple-filter", true, false)
-        .verifyExpectedFileChecksum("jcr_root/apps/bar/test1.properties", "10791371")
-        .verifyExpectedFileChecksum("jcr_root/apps/foo/test2.properties", "7563f01d");
+    public void test_simple_filter_with_filtering_enabled_on_jcrroot() throws Exception {
+        verify("simple-filter", true, false, null)
+            // cannot check checksum of properties.xml as that one has platform dependent new lines!
+            //.verifyExpectedFileChecksum("META-INF/vault/properties.xml", "5953911b")
+            .verifyExpectedFileChecksum("jcr_root/apps/bar/test1.properties", "10791371")
+            .verifyExpectedFileChecksum("jcr_root/apps/foo/test2.properties", "7563f01d")
+            .verifyExpectedFileChecksum("jcr_root/apps/foo/child/.content.xml", "d9b1ad2");
     }
 
     @Test
-    public void test_simple_filter_with_filtering_partially_enabled2() throws Exception {
-        verify("simple-filter", false, true)
-        .verifyExpectedFileChecksum("jcr_root/apps/bar/test1.properties", "34e5a01d")
-        .verifyExpectedFileChecksum("jcr_root/apps/foo/test2.properties", "a41ae6f8");
+    public void test_simple_filter_with_filtering_enabled_on_metainf() throws Exception {
+        verify("simple-filter", false, true, null)
+            // cannot check checksum of properties.xml as that one has platform dependent new lines!
+            //.verifyExpectedFileChecksum("META-INF/vault/properties.xml", "295fb69e")
+            .verifyExpectedFileChecksum("jcr_root/apps/bar/test1.properties", "34e5a01d")
+            .verifyExpectedFileChecksum("jcr_root/apps/foo/test2.properties", "a41ae6f8")
+            .verifyExpectedFileChecksum("jcr_root/apps/foo/child/.content.xml", "f8aee8df");
+    }
+    
+    @Test
+    public void test_simple_filter_with_filtering_partially_enabled_on_jcrroot() throws Exception {
+        verify("simple-filter", true, false, "**/child/*.xml")
+            // cannot check checksum of properties.xml as that one has platform dependent new lines!
+            //.verifyExpectedFileChecksum("META-INF/vault/properties.xml", "5953911b")
+            .verifyExpectedFileChecksum("jcr_root/apps/bar/test1.properties", "34e5a01d")
+            .verifyExpectedFileChecksum("jcr_root/apps/foo/test2.properties", "a41ae6f8")
+            .verifyExpectedFileChecksum("jcr_root/apps/bar/.content.xml", "f8aee8df")
+            .verifyExpectedFileChecksum("jcr_root/apps/foo/child/.content.xml", "d9b1ad2");
     }
 }
diff --git a/src/test/resources/test-projects/filtering-tests/simple-filter/expected-files.txt b/src/test/resources/test-projects/filtering-tests/simple-filter/expected-files.txt
index 2a8bd38..f63b6da 100644
--- a/src/test/resources/test-projects/filtering-tests/simple-filter/expected-files.txt
+++ b/src/test/resources/test-projects/filtering-tests/simple-filter/expected-files.txt
@@ -14,5 +14,8 @@
 jcr_root/apps/
 jcr_root/apps/bar/
 jcr_root/apps/bar/test1.properties
+jcr_root/apps/bar/.content.xml
 jcr_root/apps/foo/
-jcr_root/apps/foo/test2.properties
\ No newline at end of file
+jcr_root/apps/foo/test2.properties
+jcr_root/apps/foo/child/
+jcr_root/apps/foo/child/.content.xml
\ No newline at end of file
diff --git a/src/test/resources/test-projects/filtering-tests/simple-filter/jcr_root/apps/bar/.content.xml b/src/test/resources/test-projects/filtering-tests/simple-filter/jcr_root/apps/bar/.content.xml
new file mode 100644
index 0000000..d61361e
--- /dev/null
+++ b/src/test/resources/test-projects/filtering-tests/simple-filter/jcr_root/apps/bar/.content.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<jcr:root xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0"
+jcr:primaryType="nt:unstructured"
+customKey2="${customKey}" />
\ No newline at end of file
diff --git a/src/test/resources/test-projects/filtering-tests/simple-filter/jcr_root/apps/foo/child/.content.xml b/src/test/resources/test-projects/filtering-tests/simple-filter/jcr_root/apps/foo/child/.content.xml
new file mode 100644
index 0000000..d61361e
--- /dev/null
+++ b/src/test/resources/test-projects/filtering-tests/simple-filter/jcr_root/apps/foo/child/.content.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<jcr:root xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0"
+jcr:primaryType="nt:unstructured"
+customKey2="${customKey}" />
\ No newline at end of file