[MCLEAN-93] Support junctions on NTFS (#10)

diff --git a/src/main/java/org/apache/maven/plugins/clean/Cleaner.java b/src/main/java/org/apache/maven/plugins/clean/Cleaner.java
index 70bfe2e..9f5c2fb 100644
--- a/src/main/java/org/apache/maven/plugins/clean/Cleaner.java
+++ b/src/main/java/org/apache/maven/plugins/clean/Cleaner.java
@@ -24,8 +24,10 @@
 import java.lang.reflect.Method;
 import java.lang.reflect.Proxy;
 import java.nio.file.Files;
+import java.nio.file.LinkOption;
 import java.nio.file.Path;
 import java.nio.file.StandardCopyOption;
+import java.nio.file.attribute.BasicFileAttributes;
 import java.util.ArrayDeque;
 import java.util.Deque;
 
@@ -215,9 +217,8 @@
 
         if (isDirectory) {
             if (selector == null || selector.couldHoldSelected(pathname)) {
-                final boolean isSymlink = Files.isSymbolicLink(file.toPath());
-                File canonical = followSymlinks ? file : file.getCanonicalFile();
-                if (followSymlinks || !isSymlink) {
+                if (followSymlinks || !isSymbolicLink(file.toPath())) {
+                    File canonical = followSymlinks ? file : file.getCanonicalFile();
                     String[] filenames = canonical.list();
                     if (filenames != null) {
                         String prefix = pathname.length() > 0 ? pathname + File.separatorChar : "";
@@ -254,6 +255,13 @@
         return result;
     }
 
+    private boolean isSymbolicLink(Path path) throws IOException {
+        BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS);
+        return attrs.isSymbolicLink()
+                // MCLEAN-93: NTFS junctions have isDirectory() and isOther() attributes set
+                || (attrs.isDirectory() && attrs.isOther());
+    }
+
     /**
      * Deletes the specified file, directory. If the path denotes a symlink, only the link is removed, its target is
      * left untouched.
diff --git a/src/test/java/org/apache/maven/plugins/clean/CleanMojoTest.java b/src/test/java/org/apache/maven/plugins/clean/CleanMojoTest.java
index 48fa782..f82e615 100644
--- a/src/test/java/org/apache/maven/plugins/clean/CleanMojoTest.java
+++ b/src/test/java/org/apache/maven/plugins/clean/CleanMojoTest.java
@@ -18,15 +18,22 @@
  */
 package org.apache.maven.plugins.clean;
 
+import java.io.ByteArrayOutputStream;
 import java.io.File;
+import java.io.IOException;
 import java.io.RandomAccessFile;
 import java.nio.channels.FileChannel;
 import java.nio.channels.FileLock;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Collections;
 
 import org.apache.maven.plugin.MojoExecutionException;
 import org.apache.maven.plugin.testing.AbstractMojoTestCase;
 
 import static org.apache.commons.io.FileUtils.copyDirectory;
+import static org.codehaus.plexus.util.IOUtil.copy;
 
 /**
  * Test the clean mojo.
@@ -205,7 +212,7 @@
      */
     public void testCleanLockedFile() throws Exception {
         if (!System.getProperty("os.name").toLowerCase().contains("windows")) {
-            assertTrue("Ignored this test on none Windows based systems", true);
+            assertTrue("Ignored this test on non Windows based systems", true);
             return;
         }
 
@@ -239,7 +246,7 @@
      */
     public void testCleanLockedFileWithNoError() throws Exception {
         if (!System.getProperty("os.name").toLowerCase().contains("windows")) {
-            assertTrue("Ignored this test on none Windows based systems", true);
+            assertTrue("Ignore this test on non Windows based systems", true);
             return;
         }
 
@@ -265,6 +272,90 @@
     }
 
     /**
+     * Test the followLink option with windows junctions
+     * @throws Exception
+     */
+    public void testFollowLinksWithWindowsJunction() throws Exception {
+        if (!System.getProperty("os.name").toLowerCase().contains("windows")) {
+            assertTrue("Ignore this test on non Windows based systems", true);
+            return;
+        }
+
+        testSymlink((link, target) -> {
+            Process process = new ProcessBuilder()
+                    .directory(link.getParent().toFile())
+                    .command("cmd", "/c", "mklink", "/j", link.getFileName().toString(), target.toString())
+                    .start();
+            process.waitFor();
+            ByteArrayOutputStream baos = new ByteArrayOutputStream();
+            copy(process.getInputStream(), baos);
+            copy(process.getErrorStream(), baos);
+            if (!Files.exists(link)) {
+                throw new IOException("Unable to create junction: " + baos);
+            }
+        });
+    }
+
+    /**
+     * Test the followLink option with sym link
+     * @throws Exception
+     */
+    public void testFollowLinksWithSymLinkOnPosix() throws Exception {
+        if (System.getProperty("os.name").toLowerCase().contains("windows")) {
+            assertTrue("Ignore this test on Windows based systems", true);
+            return;
+        }
+
+        testSymlink((link, target) -> {
+            try {
+                Files.createSymbolicLink(link, target);
+            } catch (IOException e) {
+                throw new IOException("Unable to create symbolic link", e);
+            }
+        });
+    }
+
+    @FunctionalInterface
+    interface LinkCreator {
+        void createLink(Path link, Path target) throws Exception;
+    }
+
+    private void testSymlink(LinkCreator linkCreator) throws Exception {
+        Cleaner cleaner = new Cleaner(null, null, false, null, null);
+        Path testDir = Paths.get("target/test-classes/unit/test-dir").toAbsolutePath();
+        Path dirWithLnk = testDir.resolve("dir");
+        Path orgDir = testDir.resolve("org-dir");
+        Path jctDir = dirWithLnk.resolve("jct-dir");
+        Path file = orgDir.resolve("file.txt");
+
+        // create directories, links and file
+        Files.createDirectories(dirWithLnk);
+        Files.createDirectories(orgDir);
+        Files.write(file, Collections.singleton("Hello world"));
+        linkCreator.createLink(jctDir, orgDir);
+        // delete
+        cleaner.delete(dirWithLnk.toFile(), null, false, true, false);
+        // verify
+        assertTrue(Files.exists(file));
+        assertFalse(Files.exists(jctDir));
+        assertTrue(Files.exists(orgDir));
+        assertFalse(Files.exists(dirWithLnk));
+
+        // create directories, links and file
+        Files.createDirectories(dirWithLnk);
+        Files.createDirectories(orgDir);
+        Files.write(file, Collections.singleton("Hello world"));
+        linkCreator.createLink(jctDir, orgDir);
+        // delete
+        cleaner.delete(dirWithLnk.toFile(), null, true, true, false);
+        // verify
+        assertFalse(Files.exists(file));
+        assertFalse(Files.exists(jctDir));
+        assertTrue(Files.exists(orgDir));
+        assertFalse(Files.exists(dirWithLnk));
+    }
+
+    /**
      * @param dir a dir or a file
      * @return true if a file/dir exists, false otherwise
      */