[IO-807] Copy symlinks, not the files the symlinks point to (#558)

* update test to reflect desired behavior of copying broken symlinks

* [IO-807] copy symbolic links as links

* pmd

* typo

* add test for unbroken link
diff --git a/src/main/java/org/apache/commons/io/FileUtils.java b/src/main/java/org/apache/commons/io/FileUtils.java
index 2b3a554..5a26fc2 100644
--- a/src/main/java/org/apache/commons/io/FileUtils.java
+++ b/src/main/java/org/apache/commons/io/FileUtils.java
@@ -57,6 +57,7 @@
 import java.time.chrono.ChronoLocalDateTime;
 import java.time.chrono.ChronoZonedDateTime;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Date;
@@ -297,7 +298,9 @@
             if (file.exists()) {
                 throw new IllegalArgumentException("Parameter '" + name + "' is not a file: " + file);
             }
-            throw new FileNotFoundException("Source '" + file + "' does not exist");
+            if (!Files.isSymbolicLink(file.toPath())) {
+                throw new FileNotFoundException("Source '" + file + "' does not exist");
+            }
         }
     }
 
@@ -504,6 +507,13 @@
      * not guaranteed that the operation will succeed. If the modification operation fails, it falls back to
      * {@link File#setLastModified(long)}. If that fails, the method throws IOException.
      * </p>
+     * <p>
+     * Symbolic links in the source directory are copied to new symbolic links in the destination
+     * directory that point to the original target. The target of the link is not copied unless
+     * it is also under the source directory. Even if it is under the source directory, the new symbolic
+     * link in the destination points to the original target in the source directory, not to the
+     * newly created copy of the target.
+     * </p>
      *
      * @param srcDir an existing directory to copy, must not be {@code null}.
      * @param destDir the new directory, must not be {@code null}.
@@ -816,7 +826,7 @@
      * @param srcFile an existing file to copy, must not be {@code null}.
      * @param destFile the new file, must not be {@code null}.
      * @param preserveFileDate true if the file date of the copy should be the same as the original.
-     * @param copyOptions options specifying how the copy should be done, for example {@link StandardCopyOption}..
+     * @param copyOptions options specifying how the copy should be done, for example {@link StandardCopyOption}.
      * @throws NullPointerException if any of the given {@link File}s are {@code null}.
      * @throws FileNotFoundException if the source does not exist.
      * @throws IllegalArgumentException if source is not a file.
@@ -825,7 +835,7 @@
      * @see #copyFileToDirectory(File, File, boolean)
      * @since 2.8.0
      */
-    public static void copyFile(final File srcFile, final File destFile, final boolean preserveFileDate, final CopyOption... copyOptions) throws IOException {
+    public static void copyFile(final File srcFile, final File destFile, final boolean preserveFileDate, CopyOption... copyOptions) throws IOException {
         Objects.requireNonNull(destFile, "destination");
         checkFileExists(srcFile, "srcFile");
         requireCanonicalPathsNotEquals(srcFile, destFile);
@@ -834,10 +844,18 @@
             checkFileExists(destFile, "destFile");
             requireCanWrite(destFile, "destFile");
         }
+
+        final boolean isSymLink = Files.isSymbolicLink(srcFile.toPath());
+        if (isSymLink && !Arrays.asList(copyOptions).contains(LinkOption.NOFOLLOW_LINKS)) {
+            final List<CopyOption> list = new ArrayList<CopyOption>(Arrays.asList(copyOptions));
+            list.add(LinkOption.NOFOLLOW_LINKS);
+            copyOptions = list.toArray(new CopyOption[0]);
+        }
+
         Files.copy(srcFile.toPath(), destFile.toPath(), copyOptions);
 
         // On Windows, the last modified time is copied by default.
-        if (preserveFileDate && !setTimes(srcFile, destFile)) {
+        if (preserveFileDate && !isSymLink && !setTimes(srcFile, destFile)) {
             throw new IOException("Cannot set the file time.");
         }
     }
diff --git a/src/test/java/org/apache/commons/io/FileUtilsTest.java b/src/test/java/org/apache/commons/io/FileUtilsTest.java
index e7b3202..71811ac 100644
--- a/src/test/java/org/apache/commons/io/FileUtilsTest.java
+++ b/src/test/java/org/apache/commons/io/FileUtilsTest.java
@@ -757,6 +757,33 @@
         assertTrue(FileUtils.contentEqualsIgnoreEOL(file1, file2, null));
     }
 
+    @Test
+    public void testCopyDirectory_symLink() throws IOException {
+        // Make a file
+        final File sourceDirectory = new File(tempDirFile, "source_directory");
+        sourceDirectory.mkdir();
+        final File targetFile = new File(sourceDirectory, "hello.txt");
+        FileUtils.writeStringToFile(targetFile, "HELLO WORLD", "UTF8");
+
+        // Make a symlink to the file
+        final Path targetPath = targetFile.toPath();
+        final Path linkPath = sourceDirectory.toPath().resolve("linkfile");
+        Files.createSymbolicLink(linkPath, targetPath);
+        assumeTrue(Files.isSymbolicLink(linkPath), () -> "Expected a symlink here: " + linkPath);
+        assumeTrue(Files.exists(linkPath));
+        assumeTrue(Files.exists(linkPath, LinkOption.NOFOLLOW_LINKS));
+
+        // Now copy sourceDirectory, including the broken link, to another directory
+        final File destination = new File(tempDirFile, "destination");
+        FileUtils.copyDirectory(sourceDirectory, destination);
+        assertTrue(destination.exists());
+        final Path copiedSymlink = new File(destination, "linkfile").toPath();
+
+        // test for the existence of the copied symbolic link as a link
+        assertTrue(Files.isSymbolicLink(copiedSymlink));
+        assertTrue(Files.exists(copiedSymlink));
+    }
+
     /**
      * Tests IO-807.
      */
@@ -783,12 +810,16 @@
 
         // Now copy sourceDirectory, including the broken link, to another directory
         final File destination = new File(tempDirFile, "destination");
-        final FileNotFoundException thrown = assertThrows(
-                FileNotFoundException.class,
-                () -> FileUtils.copyDirectory(sourceDirectory, destination),
-                "ignored broken link"
-        );
-        assertTrue(thrown.getMessage().contains("linkfile' does not exist"));
+        FileUtils.copyDirectory(sourceDirectory, destination);
+        assertTrue(destination.exists());
+        final Path copiedBrokenSymlink = new File(destination, "linkfile").toPath();
+
+        // test for the existence of the copied symbolic link as a link
+        assertTrue(Files.isSymbolicLink(copiedBrokenSymlink));
+
+        // shouldn't be able to read through to the source of the link.
+        // If we can, then the link points somewhere other than the deleted file
+        assertFalse(Files.exists(copiedBrokenSymlink));
     }
 
     @Test