You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maven.apache.org by sl...@apache.org on 2020/06/22 22:38:36 UTC

[maven-shared-utils] 01/02: rework directory scanner to ensure it can enforce exclusions and it uses Path

This is an automated email from the ASF dual-hosted git repository.

slachiewicz pushed a commit to branch rmannibucau/directory-scanner-rework
in repository https://gitbox.apache.org/repos/asf/maven-shared-utils.git

commit fc050ad089c8a0c4bfdaeb0c49cc8a3f446faba2
Author: Romain Manni-Bucau <rm...@gmail.com>
AuthorDate: Wed Jun 10 11:00:15 2020 +0200

    rework directory scanner to ensure it can enforce exclusions and it uses Path
---
 .../maven/shared/utils/io/DirectoryScanner.java    | 455 +++++++++------------
 .../apache/maven/shared/utils/io/MatchPattern.java |   2 -
 .../maven/shared/utils/io/MatchPatterns.java       |   2 -
 .../maven/shared/utils/io/ScanConductor.java       |   2 -
 .../apache/maven/shared/utils/io/ScannerAware.java |  29 ++
 .../maven/shared/utils/io/SelectorUtils.java       |   3 -
 .../io/conductor/EnforceExcludesOverIncludes.java  |  58 +++
 .../shared/utils/io/DirectoryScannerTest.java      |  10 +-
 .../conductor/EnforceExcludesOverIncludesTest.java |  92 +++++
 9 files changed, 376 insertions(+), 277 deletions(-)

diff --git a/src/main/java/org/apache/maven/shared/utils/io/DirectoryScanner.java b/src/main/java/org/apache/maven/shared/utils/io/DirectoryScanner.java
index 5d03525..582336f 100644
--- a/src/main/java/org/apache/maven/shared/utils/io/DirectoryScanner.java
+++ b/src/main/java/org/apache/maven/shared/utils/io/DirectoryScanner.java
@@ -19,11 +19,19 @@ package org.apache.maven.shared.utils.io;
  * under the License.
  */
 
+import org.apache.maven.shared.utils.io.conductor.EnforceExcludesOverIncludes;
+
 import java.io.File;
 import java.io.IOException;
+import java.nio.file.FileVisitOption;
+import java.nio.file.FileVisitResult;
 import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.EnumSet;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
@@ -31,6 +39,8 @@ import java.util.Set;
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
+import static java.nio.file.FileVisitOption.FOLLOW_LINKS;
+
 /**
  * Class for scanning a directory for files/directories which match certain criteria.
  * <p/>
@@ -108,9 +118,7 @@ import javax.annotation.Nullable;
  * @author Magesh Umasankar
  * @author <a href="mailto:bruce@callenish.com">Bruce Atherton</a>
  * @author <a href="mailto:levylambert@tiscali-dsl.de">Antoine Levy-Lambert</a>
- * @deprecated use {@code java.nio.file.DirectoryStream} and related classes
  */
-@Deprecated
 public class DirectoryScanner
 {
     /**
@@ -158,7 +166,7 @@ public class DirectoryScanner
     /**
      * The base directory to be scanned.
      */
-    private File basedir;
+    private Path basedir;
 
     /**
      * The patterns for the files to be included.
@@ -217,8 +225,6 @@ public class DirectoryScanner
 
     /**
      * Whether or not symbolic links should be followed.
-     *
-     * 
      */
     private boolean followSymlinks = true;
 
@@ -229,11 +235,6 @@ public class DirectoryScanner
     private ScanConductor scanConductor = null;
 
     /**
-     * The last ScanAction. We need to store this in the instance as the scan() method doesn't return
-     */
-    private ScanConductor.ScanAction scanAction = null;
-
-    /**
      * Sole constructor.
      */
     public DirectoryScanner()
@@ -259,6 +260,16 @@ public class DirectoryScanner
      */
     public void setBasedir( @Nonnull final File basedir )
     {
+        setBasedir( basedir.toPath() );
+    }
+
+    /**
+     * Sets the base directory to be scanned. This is the directory which is scanned recursively.
+     *
+     * @param basedir The base directory for scanning. Should not be <code>null</code>.
+     */
+    public void setBasedir( @Nonnull final Path basedir )
+    {
         this.basedir = basedir;
     }
 
@@ -269,7 +280,7 @@ public class DirectoryScanner
      */
     public File getBasedir()
     {
-        return basedir;
+        return basedir.toFile();
     }
 
     /**
@@ -363,6 +374,15 @@ public class DirectoryScanner
     }
 
     /**
+     * Set {@link EnforceExcludesOverIncludes} scan conductor for a faster scanning
+     * and generally no functional side effect.
+     */
+    public void setEnforceExcludesOverIncludes()
+    {
+        setScanConductor( new EnforceExcludesOverIncludes() );
+    }
+
+    /**
      * Scans the base directory for files which match at least one include pattern and don't match any exclude patterns.
      * If there are selectors then the files must pass muster there, as well.
      *
@@ -376,11 +396,11 @@ public class DirectoryScanner
         {
             throw new IllegalStateException( "No basedir set" );
         }
-        if ( !basedir.exists() )
+        if ( !Files.exists( basedir ) )
         {
             throw new IllegalStateException( "basedir " + basedir + " does not exist" );
         }
-        if ( !basedir.isDirectory() )
+        if ( !Files.isDirectory( basedir ) )
         {
             throw new IllegalStateException( "basedir " + basedir + " is not a directory" );
         }
@@ -388,42 +408,164 @@ public class DirectoryScanner
         setupDefaultFilters();
         setupMatchPatterns();
 
-        filesIncluded = new ArrayList<String>();
-        filesNotIncluded = new ArrayList<String>();
-        filesExcluded = new ArrayList<String>();
-        dirsIncluded = new ArrayList<String>();
-        dirsNotIncluded = new ArrayList<String>();
-        dirsExcluded = new ArrayList<String>();
-        scanAction = ScanConductor.ScanAction.CONTINUE;
+        if ( scanConductor instanceof ScannerAware ) // after the init
+        {
+            ( ( ScannerAware ) scanConductor ).setDirectoryScanner( this );
+        }
+
+        filesIncluded = new ArrayList<>();
+        filesNotIncluded = new ArrayList<>();
+        filesExcluded = new ArrayList<>();
+        dirsIncluded = new ArrayList<>();
+        dirsNotIncluded = new ArrayList<>();
+        dirsExcluded = new ArrayList<>();
+
+        doScan( basedir, followSymlinks ? EnumSet.of( FOLLOW_LINKS ) : EnumSet.noneOf( FileVisitOption.class ), true );
+    }
 
-        if ( isIncluded( "" ) )
+    private void doScan( final Path root, final Set<FileVisitOption> options, final boolean fast )
+    {
+        try
         {
-            if ( !isExcluded( "" ) )
+            Files.walkFileTree( root, options, Integer.MAX_VALUE, new SimpleFileVisitor<Path>()
             {
-                if ( scanConductor != null )
+                @Override
+                public FileVisitResult preVisitDirectory( final Path dir, final BasicFileAttributes attrs )
+                        throws IOException
+                {
+                    final String name = root.relativize( dir ).toString();
+                    if ( isIncluded( name ) )
+                    {
+                        if ( !isExcluded( name ) )
+                        {
+                            if ( scanConductor != null )
+                            {
+                                final ScanConductor.ScanAction scanAction = scanConductor.visitDirectory(
+                                        name, dir.toFile() );
+                                if ( ScanConductor.ScanAction.ABORT.equals( scanAction )
+                                        || ScanConductor.ScanAction.ABORT_DIRECTORY.equals( scanAction ) )
+                                {
+                                    return FileVisitResult.SKIP_SIBLINGS;
+                                }
+                                if ( ScanConductor.ScanAction.NO_RECURSE.equals( scanAction ) )
+                                {
+                                    return FileVisitResult.SKIP_SUBTREE;
+                                }
+                            }
+                            dirsIncluded.add( name );
+                        }
+                        else
+                        {
+                            dirsExcluded.add( name );
+                            if ( fast && !couldHoldIncluded( name ) )
+                            {
+                                return FileVisitResult.SKIP_SUBTREE;
+                            }
+                            if ( scanConductor != null )
+                            {
+                                final FileVisitResult result = toVisitResult( dir, name );
+                                if ( result != null )
+                                {
+                                    return result;
+                                }
+                            }
+                            // else continue to visit
+                        }
+                    }
+                    else
+                    {
+                        if ( fast && couldHoldIncluded( name ) )
+                        {
+                            if ( scanConductor != null )
+                            {
+                                final FileVisitResult result = toVisitResult( dir, name );
+                                if ( result != null )
+                                {
+                                    return result;
+                                }
+                            }
+                            dirsNotIncluded.add( name );
+                        }
+                        else if ( !fast )
+                        {
+                            final FileVisitResult result = toVisitResult( dir, name );
+                            if ( result != null )
+                            {
+                                return result;
+                            }
+                        }
+                    }
+                    return super.preVisitDirectory( dir, attrs );
+                }
+
+                @Override
+                public FileVisitResult visitFile( final Path file, final BasicFileAttributes attrs ) throws IOException
                 {
-                    scanAction = scanConductor.visitDirectory( "", basedir );
+                    final String name = root.relativize( file ).toString();
+                    if ( !followSymlinks && Files.isSymbolicLink( file ) )
+                    {
+                        final Path resolved = file.toRealPath( );
+                        if ( Files.isDirectory( resolved ) )
+                        {
+                            dirsIncluded.add( name );
+                            return FileVisitResult.SKIP_SUBTREE;
+                        }
+                    }
+                    if ( isIncluded( name ) )
+                    {
+                        if ( !isExcluded( name ) )
+                        {
+                            final ScanConductor.ScanAction scanAction;
+                            if ( scanConductor != null )
+                            {
+                                scanAction = scanConductor.visitFile( name, file.toFile() );
+                            }
+                            else
+                            {
+                                scanAction = null;
+                            }
+
+                            if ( ScanConductor.ScanAction.ABORT.equals( scanAction )
+                                    || ScanConductor.ScanAction.ABORT_DIRECTORY.equals( scanAction ) )
+                            {
+                                return FileVisitResult.SKIP_SIBLINGS;
+                            }
 
-                    if ( ScanConductor.ScanAction.ABORT.equals( scanAction )
-                        || ScanConductor.ScanAction.ABORT_DIRECTORY.equals( scanAction )
-                        || ScanConductor.ScanAction.NO_RECURSE.equals( scanAction ) )
+                            filesIncluded.add( name );
+                        }
+                        else
+                        {
+                            filesExcluded.add( name );
+                        }
+                    }
+                    else
                     {
-                        return;
+                        filesNotIncluded.add( name );
                     }
+                    return super.visitFile( file, attrs );
                 }
+            } );
+        }
+        catch ( final IOException e )
+        {
+            throw new IllegalStateException( e );
+        }
+    }
 
-                dirsIncluded.add( "" );
-            }
-            else
-            {
-                dirsExcluded.add( "" );
-            }
+    private FileVisitResult toVisitResult( final Path dir, final String name )
+    {
+        final ScanConductor.ScanAction scanAction = scanConductor.visitDirectory(
+                name, dir.toFile() );
+        if ( ScanConductor.ScanAction.ABORT.equals( scanAction )
+                || ScanConductor.ScanAction.ABORT_DIRECTORY.equals( scanAction ) )
+        {
+            return FileVisitResult.SKIP_SIBLINGS;
         }
-        else
+        if ( ScanConductor.ScanAction.NO_RECURSE.equals( scanAction ) )
         {
-            dirsNotIncluded.add( "" );
+            return FileVisitResult.SKIP_SUBTREE;
         }
-        scandir( basedir, "", true );
+        return null;
     }
 
     /**
@@ -523,15 +665,18 @@ public class DirectoryScanner
             return;
         }
 
-        final String[] excl = dirsExcluded.toArray( new String[dirsExcluded.size()] );
+        final String[] excl = dirsExcluded.toArray( new String[ 0 ] );
+
+        final String[] notIncl = dirsNotIncluded.toArray( new String[ 0 ] );
 
-        final String[] notIncl = dirsNotIncluded.toArray( new String[dirsNotIncluded.size()] );
+        final EnumSet<FileVisitOption> opts = followSymlinks
+                ? EnumSet.of( FOLLOW_LINKS ) : EnumSet.noneOf( FileVisitOption.class );
 
         for ( String anExcl : excl )
         {
             if ( !couldHoldIncluded( anExcl ) )
             {
-                scandir( new File( basedir, anExcl ), anExcl + File.separator, false );
+                doScan( basedir.resolve( anExcl ), opts, false );
             }
         }
 
@@ -539,7 +684,7 @@ public class DirectoryScanner
         {
             if ( !couldHoldIncluded( aNotIncl ) )
             {
-                scandir( new File( basedir, aNotIncl ), aNotIncl + File.separator, false );
+                doScan( basedir.resolve( aNotIncl ), opts, false );
             }
         }
 
@@ -547,207 +692,6 @@ public class DirectoryScanner
     }
 
     /**
-     * Scans the given directory for files and directories. Found files and directories are placed in their respective
-     * collections, based on the matching of includes, excludes, and the selectors. When a directory is found, it is
-     * scanned recursively.
-     *
-     * @param dir   The directory to scan. Must not be <code>null</code>.
-     * @param vpath The path relative to the base directory (needed to prevent problems with an absolute path when using
-     *              dir). Must not be <code>null</code>.
-     * @param fast  Whether or not this call is part of a fast scan.
-     * @see #filesIncluded
-     * @see #filesNotIncluded
-     * @see #filesExcluded
-     * @see #dirsIncluded
-     * @see #dirsNotIncluded
-     * @see #dirsExcluded
-     * @see #slowScan
-     */
-    void scandir( @Nonnull final File dir, @Nonnull final String vpath, final boolean fast )
-    {
-        String[] newfiles = dir.list();
-
-        if ( newfiles == null )
-        {
-            /*
-             * two reasons are mentioned in the API docs for File.list (1) dir is not a directory. This is impossible as
-             * we wouldn't get here in this case. (2) an IO error occurred (why doesn't it throw an exception then???)
-             */
-
-            /*
-             * [jdcasey] (2) is apparently happening to me, as this is killing one of my tests... this is affecting the
-             * assembly plugin, fwiw. I will initialize the newfiles array as zero-length for now. NOTE: I can't find
-             * the problematic code, as it appears to come from a native method in UnixFileSystem...
-             */
-            newfiles = new String[0];
-
-            // throw new IOException( "IO error scanning directory " + dir.getAbsolutePath() );
-        }
-
-        if ( !followSymlinks )
-        {
-            newfiles = doNotFollowSymbolicLinks( dir, vpath, newfiles );
-        }
-
-        for ( final String newfile : newfiles )
-        {
-            final String name = vpath + newfile;
-            final File file = new File( dir, newfile );
-            if ( file.isDirectory() )
-            {
-                if ( isIncluded( name ) )
-                {
-                    if ( !isExcluded( name ) )
-                    {
-                        if ( scanConductor != null )
-                        {
-                            scanAction = scanConductor.visitDirectory( name, file );
-
-                            if ( ScanConductor.ScanAction.ABORT.equals( scanAction )
-                                || ScanConductor.ScanAction.ABORT_DIRECTORY.equals( scanAction ) )
-                            {
-                                return;
-                            }
-                        }
-
-                        if ( !ScanConductor.ScanAction.NO_RECURSE.equals( scanAction ) )
-                        {
-                            dirsIncluded.add( name );
-                            if ( fast )
-                            {
-                                scandir( file, name + File.separator, fast );
-
-                                if ( ScanConductor.ScanAction.ABORT.equals( scanAction ) )
-                                {
-                                    return;
-                                }
-                            }
-                        }
-                        scanAction = null;
-
-                    }
-                    else
-                    {
-                        dirsExcluded.add( name );
-                        if ( fast && couldHoldIncluded( name ) )
-                        {
-                            scandir( file, name + File.separator, fast );
-                            if ( ScanConductor.ScanAction.ABORT.equals( scanAction ) )
-                            {
-                                return;
-                            }
-                            scanAction = null;
-                        }
-                    }
-                }
-                else
-                {
-                    if ( fast && couldHoldIncluded( name ) )
-                    {
-                        if ( scanConductor != null )
-                        {
-                            scanAction = scanConductor.visitDirectory( name, file );
-
-                            if ( ScanConductor.ScanAction.ABORT.equals( scanAction )
-                                || ScanConductor.ScanAction.ABORT_DIRECTORY.equals( scanAction ) )
-                            {
-                                return;
-                            }
-                        }
-                        if ( !ScanConductor.ScanAction.NO_RECURSE.equals( scanAction ) )
-                        {
-                            dirsNotIncluded.add( name );
-
-                            scandir( file, name + File.separator, fast );
-                            if ( ScanConductor.ScanAction.ABORT.equals( scanAction ) )
-                            {
-                                return;
-                            }
-                        }
-                        scanAction = null;
-                    }
-                }
-                if ( !fast )
-                {
-                    scandir( file, name + File.separator, fast );
-                    if ( ScanConductor.ScanAction.ABORT.equals( scanAction ) )
-                    {
-                        return;
-                    }
-                    scanAction = null;
-                }
-            }
-            else if ( file.isFile() )
-            {
-                if ( isIncluded( name ) )
-                {
-                    if ( !isExcluded( name ) )
-                    {
-                        if ( scanConductor != null )
-                        {
-                            scanAction = scanConductor.visitFile( name, file );
-                        }
-
-                        if ( ScanConductor.ScanAction.ABORT.equals( scanAction )
-                            || ScanConductor.ScanAction.ABORT_DIRECTORY.equals( scanAction ) )
-                        {
-                            return;
-                        }
-
-                        filesIncluded.add( name );
-                    }
-                    else
-                    {
-                        filesExcluded.add( name );
-                    }
-                }
-                else
-                {
-                    filesNotIncluded.add( name );
-                }
-            }
-        }
-    }
-
-    private String[] doNotFollowSymbolicLinks( final File dir, final String vpath, String[] newfiles )
-    {
-        final List<String> noLinks = new ArrayList<String>();
-        for ( final String newfile : newfiles )
-        {
-            try
-            {
-                if ( isSymbolicLink( dir, newfile ) )
-                {
-                    final String name = vpath + newfile;
-                    final File file = new File( dir, newfile );
-                    if ( file.isDirectory() )
-                    {
-                        dirsExcluded.add( name );
-                    }
-                    else
-                    {
-                        filesExcluded.add( name );
-                    }
-                }
-                else
-                {
-                    noLinks.add( newfile );
-                }
-            }
-            catch ( final IOException ioe )
-            {
-                final String msg =
-                    "IOException caught while checking " + "for links, couldn't get cannonical path!";
-                // will be caught and redirected to Ant's logging system
-                System.err.println( msg );
-                noLinks.add( newfile );
-            }
-        }
-        newfiles = noLinks.toArray( new String[noLinks.size()] );
-        return newfiles;
-    }
-
-    /**
      * Tests whether or not a name matches against at least one include pattern.
      *
      * @param name The name to match. Must not be <code>null</code>.
@@ -787,18 +731,16 @@ public class DirectoryScanner
      * Returns the names of the files which matched at least one of the include patterns and none of the exclude
      * patterns. The names are relative to the base directory.
      *
-     * @deprecated this method does not work correctly on Windows. 
      * @return the names of the files which matched at least one of the include patterns and none of the exclude
      *         patterns. May also contain symbolic links to files.
      */
-    @Deprecated
     public String[] getIncludedFiles()
     {
         if ( filesIncluded == null )
         {
             return new String[0];
         }
-        return filesIncluded.toArray( new String[filesIncluded.size()] );
+        return filesIncluded.toArray( new String[0] );
     }
 
     /**
@@ -891,21 +833,16 @@ public class DirectoryScanner
         excludes = newExcludes;
     }
 
-    /**
-     * Checks whether a given file is a symbolic link.
-     * <p>
-     * It doesn't really test for symbolic links but whether the canonical and absolute paths of the file are identical
-     * - this may lead to false positives on some platforms.
-     * </p>
-     *
-     * @param parent the parent directory of the file to test
-     * @param name   the name of the file to test.
-     * 
-     */
-    boolean isSymbolicLink( final File parent, final String name )
-        throws IOException
+    public MatchPatterns getExcludesPatterns()
+    {
+        setupDefaultFilters();
+        return excludesPatterns;
+    }
+
+    public MatchPatterns getIncludesPatterns()
     {
-        return Files.isSymbolicLink( parent.toPath() );
+        setupDefaultFilters();
+        return includesPatterns;
     }
 
     private void setupDefaultFilters()
diff --git a/src/main/java/org/apache/maven/shared/utils/io/MatchPattern.java b/src/main/java/org/apache/maven/shared/utils/io/MatchPattern.java
index 8abff42..25b35a7 100644
--- a/src/main/java/org/apache/maven/shared/utils/io/MatchPattern.java
+++ b/src/main/java/org/apache/maven/shared/utils/io/MatchPattern.java
@@ -33,9 +33,7 @@ import javax.annotation.Nonnull;
  * Significantly more efficient than using strings, since re-evaluation and re-tokenizing is avoided.
  *
  * @author Kristian Rosenvold
- * @deprecated use {@code java.nio.filejava.nio.file.DirectoryStream.Filter<T>} and related classes
  */
-@Deprecated
 public class MatchPattern
 {
     private final String source;
diff --git a/src/main/java/org/apache/maven/shared/utils/io/MatchPatterns.java b/src/main/java/org/apache/maven/shared/utils/io/MatchPatterns.java
index 693acb1..8e59f48 100644
--- a/src/main/java/org/apache/maven/shared/utils/io/MatchPatterns.java
+++ b/src/main/java/org/apache/maven/shared/utils/io/MatchPatterns.java
@@ -27,9 +27,7 @@ import javax.annotation.Nonnull;
  * A list of patterns to be matched
  *
  * @author Kristian Rosenvold
- * @deprecated use {@code java.nio.filejava.nio.file.DirectoryStream.Filter<T>} and related classes
  */
-@Deprecated
 public class MatchPatterns
 {
     private final MatchPattern[] patterns;
diff --git a/src/main/java/org/apache/maven/shared/utils/io/ScanConductor.java b/src/main/java/org/apache/maven/shared/utils/io/ScanConductor.java
index 08d7a1c..20c9a10 100644
--- a/src/main/java/org/apache/maven/shared/utils/io/ScanConductor.java
+++ b/src/main/java/org/apache/maven/shared/utils/io/ScanConductor.java
@@ -34,9 +34,7 @@ import java.io.File;
  *
  * @author <a href="mailto:struberg@apache.org">Mark Struberg</a>
  * 
- * @deprecated use {@code java.nio.file.Files.walkFileTree()} and related classes
  */
-@Deprecated
 public interface ScanConductor
 {
     /**
diff --git a/src/main/java/org/apache/maven/shared/utils/io/ScannerAware.java b/src/main/java/org/apache/maven/shared/utils/io/ScannerAware.java
new file mode 100644
index 0000000..8254203
--- /dev/null
+++ b/src/main/java/org/apache/maven/shared/utils/io/ScannerAware.java
@@ -0,0 +1,29 @@
+package org.apache.maven.shared.utils.io;
+
+/*
+ * 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.
+ */
+
+/**
+ * Enables a {@link ScanConductor} to access the {@link DirectoryScanner} configuration.
+ * It is set once the scanner is initialized.
+ */
+public interface ScannerAware
+{
+    void setDirectoryScanner( DirectoryScanner scanner );
+}
diff --git a/src/main/java/org/apache/maven/shared/utils/io/SelectorUtils.java b/src/main/java/org/apache/maven/shared/utils/io/SelectorUtils.java
index b716e7c..8f920da 100644
--- a/src/main/java/org/apache/maven/shared/utils/io/SelectorUtils.java
+++ b/src/main/java/org/apache/maven/shared/utils/io/SelectorUtils.java
@@ -38,10 +38,7 @@ import javax.annotation.Nonnull;
  *         <a href="mailto:ajkuiper@wxs.nl">ajkuiper@wxs.nl</a>
  * @author Magesh Umasankar
  * @author <a href="mailto:bruce@callenish.com">Bruce Atherton</a>
- * 
- * @deprecated use {@code java.nio.file.Files.walkFileTree()} and related classes
  */
-@Deprecated
 public final class SelectorUtils
 {
 
diff --git a/src/main/java/org/apache/maven/shared/utils/io/conductor/EnforceExcludesOverIncludes.java b/src/main/java/org/apache/maven/shared/utils/io/conductor/EnforceExcludesOverIncludes.java
new file mode 100644
index 0000000..47406bf
--- /dev/null
+++ b/src/main/java/org/apache/maven/shared/utils/io/conductor/EnforceExcludesOverIncludes.java
@@ -0,0 +1,58 @@
+package org.apache.maven.shared.utils.io.conductor;
+
+/*
+ * 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 org.apache.maven.shared.utils.io.DirectoryScanner;
+import org.apache.maven.shared.utils.io.MatchPatterns;
+import org.apache.maven.shared.utils.io.ScanConductor;
+import org.apache.maven.shared.utils.io.ScannerAware;
+
+import java.io.File;
+
+/**
+ * If an exclude is defined on a folder it will bypass the visit of the children
+ * even if some include can match children.
+ */
+public class EnforceExcludesOverIncludes implements ScanConductor, ScannerAware
+{
+    private MatchPatterns excludes;
+
+    @Override
+    public ScanAction visitDirectory( final String name, final File directory )
+    {
+        if ( excludes.matches( name, true ) )
+        {
+            return ScanAction.NO_RECURSE;
+        }
+        return ScanAction.CONTINUE;
+    }
+
+    @Override
+    public ScanAction visitFile( final String name, final File file )
+    {
+        return ScanAction.CONTINUE;
+    }
+
+    @Override
+    public void setDirectoryScanner( final DirectoryScanner scanner )
+    {
+        excludes = scanner.getExcludesPatterns();
+    }
+}
diff --git a/src/test/java/org/apache/maven/shared/utils/io/DirectoryScannerTest.java b/src/test/java/org/apache/maven/shared/utils/io/DirectoryScannerTest.java
index 0111006..4895639 100644
--- a/src/test/java/org/apache/maven/shared/utils/io/DirectoryScannerTest.java
+++ b/src/test/java/org/apache/maven/shared/utils/io/DirectoryScannerTest.java
@@ -162,7 +162,7 @@ public class DirectoryScannerTest
         ds.scan();
         List<String> included = Arrays.asList( ds.getIncludedFiles() );
         assertAlwaysIncluded( included );
-        assertEquals( 9, included.size() );
+        assertEquals( included.toString(), 9, included.size() );
         List<String> includedDirs = Arrays.asList( ds.getIncludedDirectories() );
         assertTrue( includedDirs.contains( "" ) );
         assertTrue( includedDirs.contains( "aRegularDir" ) );
@@ -243,14 +243,6 @@ public class DirectoryScannerTest
                 /* expExclDirs     */ NONE );
     }
 
-    public void testIsSymbolicLink()
-        throws IOException
-    {
-        File file = new File( "." );
-        DirectoryScanner ds = new DirectoryScanner();
-        ds.isSymbolicLink( file, "abc" );
-    }
-
     /**
      * Performs a scan and test for the given parameters if not null.
      */
diff --git a/src/test/java/org/apache/maven/shared/utils/io/conductor/EnforceExcludesOverIncludesTest.java b/src/test/java/org/apache/maven/shared/utils/io/conductor/EnforceExcludesOverIncludesTest.java
new file mode 100644
index 0000000..42259e5
--- /dev/null
+++ b/src/test/java/org/apache/maven/shared/utils/io/conductor/EnforceExcludesOverIncludesTest.java
@@ -0,0 +1,92 @@
+package org.apache.maven.shared.utils.io.conductor;
+
+/*
+ * 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 org.apache.maven.shared.utils.io.DirectoryScanner;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import static java.util.Arrays.asList;
+import static java.util.Collections.singletonList;
+import static org.junit.Assert.assertEquals;
+
+public class EnforceExcludesOverIncludesTest
+{
+    @Rule
+    public final TemporaryFolder folder = new TemporaryFolder();
+
+    @Test
+    public void dontVisitChildren() throws IOException
+    {
+        createFakeFiles();
+
+        DirectoryScanner scanner = new DirectoryScanner();
+        scanner.setScanConductor(new EnforceExcludesOverIncludes());
+        scanner.setExcludes( "**/target/**" );
+        scanner.setIncludes( "**" ); // files in target will match include but our conductor will bypass them anyway
+        scanner.setBasedir( folder.getRoot() );
+        scanner.scan();
+        assertResultEquals( asList( "bar", "sub/other" ), scanner.getIncludedFiles() );
+        assertResultEquals( singletonList( "target" ), scanner.getExcludedDirectories() );
+    }
+
+    @Test // we don't set the conductor to ensure we have a "control" test to compare to the other
+    public void controlTest() throws IOException
+    {
+        createFakeFiles();
+
+        DirectoryScanner scanner = new DirectoryScanner();
+        scanner.setExcludes( "**/target/**" );
+        scanner.setIncludes( "**" ); // files in target will match include but our conductor will bypass them anyway
+        scanner.setBasedir( folder.getRoot() );
+        scanner.scan();
+        assertResultEquals( asList( "bar", "sub/other" ), scanner.getIncludedFiles() );
+        assertResultEquals( asList( "target", "target/nested" ), scanner.getExcludedDirectories() );
+    }
+
+    private void createFakeFiles() throws IOException
+    {
+        touch(new File(folder.getRoot(), "bar"));
+        touch(new File(folder.getRoot(), "sub/other"));
+        touch(new File(folder.getRoot(), "target/foo"));
+        touch(new File(folder.getRoot(), "target/nested/dummy"));
+    }
+
+    private void assertResultEquals( final List<String> expected, final String[] actual )
+    {
+        final List<String> actualList = new ArrayList<>( asList( actual ) );
+        Collections.sort( actualList );
+        assertEquals( expected, actualList );
+    }
+
+    private void touch( final File file ) throws IOException
+    {
+        file.getParentFile().mkdirs();
+        new FileWriter( file ).close();
+    }
+}