001/*
002 *  Copyright 2020 Anyware Services
003 *
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016package org.ametys.core.util.path;
017
018import java.io.File;
019import java.io.FileNotFoundException;
020import java.io.IOException;
021import java.nio.file.Files;
022import java.nio.file.Path;
023import java.nio.file.StandardCopyOption;
024import java.util.ArrayList;
025import java.util.List;
026import java.util.function.Predicate;
027import java.util.stream.Collectors;
028import java.util.stream.Stream;
029
030import org.apache.commons.io.FileExistsException;
031
032/**
033 * General path manipulation utilities.
034 */
035public final class PathUtils
036{
037    private PathUtils()
038    {
039        // utility class
040    }
041    
042    /**
043      * Copies a whole directory to a new location preserving the file dates.
044      * <p>
045      * This method copies the specified directory and all its child
046      * directories and files to the specified destination.
047      * The destination is the new location and name of the directory.
048      * <p>
049      * The destination directory is created if it does not exist.
050      * If the destination directory did exist, then this method merges
051      * the source with the destination, with the source taking precedence.
052      * <p>
053      * <strong>Note:</strong> This method tries to preserve the files' last
054      * modified date/times using {@link File#setLastModified(long)}, however
055      * it is not guaranteed that those operations will succeed.
056      * If the modification operation fails, no indication is provided.
057      *
058      * @param srcDir  an existing directory to copy, must not be {@code null}
059      * @param destDir the new directory, must not be {@code null}
060      *
061      * @throws NullPointerException if source or destination is {@code null}
062      * @throws IOException          if source or destination is invalid
063      * @throws IOException          if an IO error occurs during copying
064      */
065    public static void copyDirectory(final Path srcDir, final Path destDir) throws IOException
066    {
067        copyDirectory(srcDir, destDir, true);
068    }
069
070    /**
071     * Copies a whole directory to a new location.
072     * <p>
073     * This method copies the contents of the specified source directory
074     * to within the specified destination directory.
075     * <p>
076     * The destination directory is created if it does not exist.
077     * If the destination directory did exist, then this method merges
078     * the source with the destination, with the source taking precedence.
079     * <p>
080     * <strong>Note:</strong> Setting <code>preserveFileDate</code> to
081     * {@code true} tries to preserve the files' last modified
082     * date/times using {@link File#setLastModified(long)}, however it is
083     * not guaranteed that those operations will succeed.
084     * If the modification operation fails, no indication is provided.
085     *
086     * @param srcDir           an existing directory to copy, must not be {@code null}
087     * @param destDir          the new directory, must not be {@code null}
088     * @param preserveFileDate true if the file date of the copy
089     *                         should be the same as the original
090     *
091     * @throws NullPointerException if source or destination is {@code null}
092     * @throws IOException          if source or destination is invalid
093     * @throws IOException          if an IO error occurs during copying
094     */
095    public static void copyDirectory(final Path srcDir, final Path destDir, final boolean preserveFileDate) throws IOException 
096    {
097        copyDirectory(srcDir, destDir, null, preserveFileDate);
098    }
099
100    /**
101     * Copies a filtered directory to a new location preserving the file dates.
102     * <p>
103     * This method copies the contents of the specified source directory
104     * to within the specified destination directory.
105     * <p>
106     * The destination directory is created if it does not exist.
107     * If the destination directory did exist, then this method merges
108     * the source with the destination, with the source taking precedence.
109     * <p>
110     * <strong>Note:</strong> This method tries to preserve the files' last
111     * modified date/times using {@link File#setLastModified(long)}, however
112     * it is not guaranteed that those operations will succeed.
113     * If the modification operation fails, no indication is provided.
114     * </p>
115     * <h3>Example: Copy directories only</h3>
116     * <pre>
117     *  // only copy the directory structure
118     *  FileUtils.copyDirectory(srcDir, destDir, Files::isDirectory);
119     *  </pre>
120     *
121     * @param srcDir  an existing directory to copy, must not be {@code null}
122     * @param destDir the new directory, must not be {@code null}
123     * @param filter  the filter to apply, null means copy all directories and files
124     *                should be the same as the original
125     *
126     * @throws NullPointerException if source or destination is {@code null}
127     * @throws IOException          if source or destination is invalid
128     * @throws IOException          if an IO error occurs during copying
129     */
130    public static void copyDirectory(final Path srcDir, final Path destDir, final Predicate<Path> filter) throws IOException 
131    {
132        copyDirectory(srcDir, destDir, filter, true);
133    }
134    
135    /**
136     * Copies a filtered directory to a new location.
137     * <p>
138     * This method copies the contents of the specified source directory
139     * to within the specified destination directory.
140     * <p>
141     * The destination directory is created if it does not exist.
142     * If the destination directory did exist, then this method merges
143     * the source with the destination, with the source taking precedence.
144     * <p>
145     * <strong>Note:</strong> Setting <code>preserveFileDate</code> to
146     * {@code true} tries to preserve the files' last modified
147     * date/times using {@link File#setLastModified(long)}, however it is
148     * not guaranteed that those operations will succeed.
149     * If the modification operation fails, no indication is provided.
150     * </p>
151     * <h3>Example: Copy directories only</h3>
152     * <pre>
153     *  // only copy the directory structure
154     *  FileUtils.copyDirectory(srcDir, destDir, DirectoryFileFilter.DIRECTORY, false);
155     *  </pre>
156     *
157     * <h3>Example: Copy directories and txt files</h3>
158     * <pre>
159     *  // Create a filter for ".txt" files
160     *  IOFileFilter txtSuffixFilter = FileFilterUtils.suffixFileFilter(".txt");
161     *  IOFileFilter txtFiles = FileFilterUtils.andFileFilter(FileFileFilter.FILE, txtSuffixFilter);
162     *
163     *  // Create a filter for either directories or ".txt" files
164     *  FileFilter filter = FileFilterUtils.orFileFilter(DirectoryFileFilter.DIRECTORY, txtFiles);
165     *
166     *  // Copy using the filter
167     *  FileUtils.copyDirectory(srcDir, destDir, filter, false);
168     *  </pre>
169     *
170     * @param srcDir           an existing directory to copy, must not be {@code null}
171     * @param destDir          the new directory, must not be {@code null}
172     * @param filter           the filter to apply, null means copy all directories and files
173     * @param preserveFileDate true if the file date of the copy
174     *                         should be the same as the original
175     *
176     * @throws NullPointerException if source or destination is {@code null}
177     * @throws IOException          if source or destination is invalid
178     * @throws IOException          if an IO error occurs during copying
179     */
180    public static void copyDirectory(final Path srcDir, final Path destDir, final Predicate<Path> filter, final boolean preserveFileDate) throws IOException 
181    {
182        checkFileRequirements(srcDir, destDir);
183        if (!Files.isDirectory(srcDir)) 
184        {
185            throw new IOException("Source '" + srcDir + "' exists but is not a directory");
186        }
187        if (srcDir.toAbsolutePath().equals(destDir.toAbsolutePath())) 
188        {
189            throw new IOException("Source '" + srcDir + "' and destination '" + destDir + "' are the same");
190        }
191
192        // Cater for destination being directory within the source directory (see IO-141)
193        List<Path> exclusionList = null;
194        if (destDir.getFileSystem() == srcDir.getFileSystem() 
195                && destDir.toAbsolutePath().startsWith(srcDir.toAbsolutePath())) 
196        {
197            List<Path> srcFiles;
198            try (Stream<Path> walk = Files.list(srcDir))
199            {
200                if (filter != null)
201                {
202                    srcFiles = walk.filter(filter).collect(Collectors.toList());
203                }
204                else
205                {
206                    srcFiles = walk.collect(Collectors.toList());
207                }
208            }
209
210            if (!srcFiles.isEmpty()) 
211            {
212                exclusionList = new ArrayList<>(srcFiles.size());
213                for (final Path srcFile : srcFiles) 
214                {
215                    final Path copiedFile = destDir.resolve(srcFile.getFileName());
216                    exclusionList.add(copiedFile.toAbsolutePath());
217                }
218            }
219        }
220        doCopyDirectory(srcDir, destDir, filter, preserveFileDate, exclusionList);
221    }
222
223    /**
224     * checks requirements for file copy
225     * @param src the source file
226     * @param dest the destination
227     * @throws FileNotFoundException if the destination does not exist
228     */
229    private static void checkFileRequirements(final Path src, final Path dest) throws FileNotFoundException 
230    {
231        if (src == null) 
232        {
233            throw new NullPointerException("Source must not be null");
234        }
235        if (dest == null) 
236        {
237            throw new NullPointerException("Destination must not be null");
238        }
239        if (!Files.exists(src)) 
240        {
241            throw new FileNotFoundException("Source '" + src + "' does not exist");
242        }
243    }
244
245    /**
246     * Internal copy directory method.
247     *
248     * @param srcDir           the validated source directory, must not be {@code null}
249     * @param destDir          the validated destination directory, must not be {@code null}
250     * @param filter           the filter to apply, null means copy all directories and files
251     * @param preserveFileDate whether to preserve the file date
252     * @param exclusionList    List of files and directories to exclude from the copy, may be null
253     * @throws IOException if an error occurs
254     */
255    private static void doCopyDirectory(final Path srcDir, final Path destDir, final Predicate<Path> filter, final boolean preserveFileDate, final List<Path> exclusionList) throws IOException 
256    {
257        // recurse
258        List<Path> srcFiles;
259        try (Stream<Path> walk = Files.list(srcDir))
260        {
261            if (filter != null)
262            {
263                srcFiles = walk.filter(filter).collect(Collectors.toList());
264            }
265            else
266            {
267                srcFiles = walk.collect(Collectors.toList());
268            }
269        }
270
271        if (Files.exists(destDir)) 
272        {
273            if (!Files.isDirectory(destDir)) 
274            {
275                throw new IOException("Destination '" + destDir + "' exists but is not a directory");
276            }
277        } 
278        else 
279        {
280            Path createdDirectories = Files.createDirectories(destDir);
281            if (!Files.isDirectory(createdDirectories)) 
282            {
283                throw new IOException("Destination '" + destDir + "' directory cannot be created");
284            }
285        }
286        if (!Files.isWritable(destDir))
287        {
288            throw new IOException("Destination '" + destDir + "' cannot be written to");
289        }
290        for (final Path srcFile : srcFiles) 
291        {
292            final Path dstFile = destDir.resolve(srcFile.getFileName().toString());
293            if (exclusionList == null || !exclusionList.contains(srcFile.toAbsolutePath())) 
294            {
295                if (Files.isDirectory(srcFile)) 
296                {
297                    doCopyDirectory(srcFile, dstFile, filter, preserveFileDate, exclusionList);
298                } 
299                else 
300                {
301                    doCopyFile(srcFile, dstFile, preserveFileDate);
302                }
303            }
304        }
305
306        // Do this last, as the above has probably affected directory metadata
307        if (preserveFileDate) 
308        {
309            Files.setLastModifiedTime(destDir, Files.getLastModifiedTime(srcDir));
310        }
311    }
312    
313    /**
314     * Internal copy file method.
315     * This caches the original file length, and throws an IOException
316     * if the output file length is different from the current input file length.
317     * So it may fail if the file changes size.
318     * It may also fail with "IllegalArgumentException: Negative size" if the input file is truncated part way
319     * through copying the data and the new file size is less than the current position.
320     *
321     * @param srcFile          the validated source file, must not be {@code null}
322     * @param destFile         the validated destination file, must not be {@code null}
323     * @param preserveFileDate whether to preserve the file date
324     * @throws IOException              if an error occurs
325     * @throws IOException              if the output file length is not the same as the input file length after the
326     * copy completes
327     * @throws IllegalArgumentException "Negative size" if the file is truncated so that the size is less than the
328     * position
329     */
330    private static void doCopyFile(final Path srcFile, final Path destFile, final boolean preserveFileDate) throws IOException 
331    {
332        if (Files.exists(destFile) && Files.isDirectory(destFile)) 
333        {
334            throw new IOException("Destination '" + destFile + "' exists but is a directory");
335        }
336
337        Files.copy(srcFile, destFile, StandardCopyOption.REPLACE_EXISTING);
338
339        final long srcLen = Files.size(srcFile); // TODO See IO-386
340        final long dstLen = Files.size(destFile); // TODO See IO-386
341        if (srcLen != dstLen) 
342        {
343            throw new IOException("Failed to copy full contents from '" + srcFile + "' to '" + destFile + "' Expected length: " + srcLen + " Actual: " + dstLen);
344        }
345        if (preserveFileDate) 
346        {
347            Files.setLastModifiedTime(destFile, Files.getLastModifiedTime(srcFile));
348        }
349    }
350
351    /**
352     * Moves a file.
353     * <p>
354     * When the destination file is on another file system, do a "copy and delete".
355     *
356     * @param srcFile  the file to be moved
357     * @param destFile the destination file
358     * @throws NullPointerException if source or destination is {@code null}
359     * @throws FileExistsException  if the destination file exists
360     * @throws IOException          if source or destination is invalid
361     * @throws IOException          if an IO error occurs moving the file
362     * @since 1.4
363     */
364    public static void moveFile(final Path srcFile, final Path destFile) throws IOException 
365    {
366        if (srcFile == null) 
367        {
368            throw new NullPointerException("Source must not be null");
369        }
370        if (destFile == null) 
371        {
372            throw new NullPointerException("Destination must not be null");
373        }
374        if (!Files.exists(srcFile)) 
375        {
376            throw new FileNotFoundException("Source '" + srcFile + "' does not exist");
377        }
378        if (Files.isDirectory(srcFile)) 
379        {
380            throw new IOException("Source '" + srcFile + "' is a directory");
381        }
382        if (Files.exists(destFile)) 
383        {
384            throw new FileExistsException("Destination '" + destFile + "' already exists");
385        }
386        if (Files.isDirectory(destFile)) 
387        {
388            throw new IOException("Destination '" + destFile + "' is a directory");
389        }
390        
391        try
392        {
393            Files.move(srcFile, destFile);
394        }
395        catch (IOException e)
396        {
397            copyFile(srcFile, destFile);
398            if (!Files.deleteIfExists(srcFile)) 
399            {
400                deleteQuietly(destFile);
401                throw new IOException("Failed to delete original file '" + srcFile + "' after copy to '" + destFile + "'");
402            }
403        }
404    }
405    
406    /**
407     * Moves a file or directory to the destination directory.
408     * <p>
409     * When the destination is on another file system, do a "copy and delete".
410     *
411     * @param src           the file or directory to be moved
412     * @param destDir       the destination directory
413     * @param createDestDir If {@code true} create the destination directory,
414     *                      otherwise if {@code false} throw an IOException
415     * @throws NullPointerException if source or destination is {@code null}
416     * @throws FileExistsException  if the directory or file exists in the destination directory
417     * @throws IOException          if source or destination is invalid
418     * @throws IOException          if an IO error occurs moving the file
419     */
420    public static void moveToDirectory(final Path src, final Path destDir, final boolean createDestDir) throws IOException 
421    {
422        if (src == null) 
423        {
424            throw new NullPointerException("Source must not be null");
425        }
426        if (destDir == null) 
427        {
428            throw new NullPointerException("Destination must not be null");
429        }
430        if (!Files.exists(src)) 
431        {
432            throw new FileNotFoundException("Source '" + src + "' does not exist");
433        }
434        if (Files.isDirectory(src)) 
435        {
436            moveDirectoryToDirectory(src, destDir, createDestDir);
437        } 
438        else 
439        {
440            moveFileToDirectory(src, destDir, createDestDir);
441        }
442    }
443
444    /**
445     * Moves a file to a directory.
446     *
447     * @param srcFile       the file to be moved
448     * @param destDir       the destination file
449     * @param createDestDir If {@code true} create the destination directory,
450     *                      otherwise if {@code false} throw an IOException
451     * @throws NullPointerException if source or destination is {@code null}
452     * @throws FileExistsException  if the destination file exists
453     * @throws IOException          if source or destination is invalid
454     * @throws IOException          if an IO error occurs moving the file
455     * @since 1.4
456     */
457    public static void moveFileToDirectory(final Path srcFile, final Path destDir, final boolean createDestDir) throws IOException 
458    {
459        if (srcFile == null) 
460        {
461            throw new NullPointerException("Source must not be null");
462        }
463        if (destDir == null) 
464        {
465            throw new NullPointerException("Destination directory must not be null");
466        }
467        if (!Files.exists(destDir) && createDestDir) 
468        {
469            Files.createDirectories(destDir);
470        }
471        if (!Files.exists(destDir)) 
472        {
473            throw new FileNotFoundException("Destination directory '" + destDir + "' does not exist [createDestDir=" + createDestDir + "]");
474        }
475        if (!Files.isDirectory(destDir)) 
476        {
477            throw new IOException("Destination '" + destDir + "' is not a directory");
478        }
479        moveFile(srcFile, destDir.resolve(srcFile.getFileName().toString()));
480    }
481
482    
483    /**
484     * Moves a directory to another directory.
485     *
486     * @param src           the file to be moved
487     * @param destDir       the destination file
488     * @param createDestDir If {@code true} create the destination directory,
489     *                      otherwise if {@code false} throw an IOException
490     * @throws NullPointerException if source or destination is {@code null}
491     * @throws FileExistsException  if the directory exists in the destination directory
492     * @throws IOException          if source or destination is invalid
493     * @throws IOException          if an IO error occurs moving the file
494     */
495    public static void moveDirectoryToDirectory(final Path src, final Path destDir, final boolean createDestDir) throws IOException 
496    {
497        if (src == null) 
498        {
499            throw new NullPointerException("Source must not be null");
500        }
501        if (destDir == null) 
502        {
503            throw new NullPointerException("Destination directory must not be null");
504        }
505        if (!Files.exists(destDir) && createDestDir) 
506        {
507            Files.createDirectories(destDir);
508        }
509        if (!Files.exists(destDir)) 
510        {
511            throw new FileNotFoundException("Destination directory '" + destDir + "' does not exist [createDestDir=" + createDestDir + "]");
512        }
513        if (!Files.isDirectory(destDir)) 
514        {
515            throw new IOException("Destination '" + destDir + "' is not a directory");
516        }
517        moveDirectory(src, destDir.resolve(src.getFileName().toString()));
518    }
519    
520    /**
521     * Moves a directory.
522     * <p>
523     * When the destination directory is on another file system, do a "copy and delete".
524     *
525     * @param srcDir  the directory to be moved
526     * @param destDir the destination directory
527     * @throws NullPointerException if source or destination is {@code null}
528     * @throws FileExistsException  if the destination directory exists
529     * @throws IOException          if source or destination is invalid
530     * @throws IOException          if an IO error occurs moving the file
531     */
532    public static void moveDirectory(final Path srcDir, final Path destDir) throws IOException 
533    {
534        if (srcDir == null) 
535        {
536            throw new NullPointerException("Source must not be null");
537        }
538        if (destDir == null) 
539        {
540            throw new NullPointerException("Destination must not be null");
541        }
542        if (!Files.exists(srcDir)) 
543        {
544            throw new FileNotFoundException("Source '" + srcDir + "' does not exist");
545        }
546        if (!Files.isDirectory(srcDir)) 
547        {
548            throw new IOException("Source '" + srcDir + "' is not a directory");
549        }
550        if (Files.exists(destDir)) 
551        {
552            throw new FileExistsException("Destination '" + destDir + "' already exists");
553        }
554        
555        try
556        {
557            Files.move(srcDir, destDir);
558        }
559        catch (IOException e)
560        {
561            if (destDir.toAbsolutePath().toString().startsWith(srcDir.toAbsolutePath().toString() + File.separator)) 
562            {
563                throw new IOException("Cannot move directory: " + srcDir + " to a subdirectory of itself: " + destDir);
564            }
565            copyDirectory(srcDir, destDir);
566            deleteDirectory(srcDir);
567            if (Files.exists(srcDir)) 
568            {
569                throw new IOException("Failed to delete original directory '" + srcDir + "' after copy to '" + destDir + "'");
570            }
571        }
572    }
573
574    
575    /**
576     * Deletes a file, never throwing an exception. If file is a directory, delete it and all sub-directories.
577     * <p>
578     * The difference between File.delete() and this method are:
579     * <ul>
580     * <li>A directory to be deleted does not have to be empty.</li>
581     * <li>No exceptions are thrown when a file or directory cannot be deleted.</li>
582     * </ul>
583     *
584     * @param file file or directory to delete, can be {@code null}
585     * @return {@code true} if the file or directory was deleted, otherwise
586     * {@code false}
587     */
588    public static boolean deleteQuietly(final Path file) 
589    {
590        if (file == null) 
591        {
592            return false;
593        }
594        try 
595        {
596            if (Files.isDirectory(file)) 
597            {
598                cleanDirectory(file);
599            }
600        } 
601        catch (final Exception ignored) 
602        {
603            // ignored
604        }
605
606        try 
607        {
608            return Files.deleteIfExists(file);
609        } 
610        catch (final Exception ignored) 
611        {
612            return false;
613        }
614    }
615    
616    /**
617     * Cleans a directory without deleting it.
618     *
619     * @param directory directory to clean
620     * @throws IOException              in case cleaning is unsuccessful
621     * @throws IllegalArgumentException if {@code directory} does not exist or is not a directory
622     */
623    public static void cleanDirectory(final Path directory) throws IOException 
624    {
625        final Path[] files = verifiedListFiles(directory);
626
627        IOException exception = null;
628        for (final Path file : files) 
629        {
630            try 
631            {
632                forceDelete(file);
633            } 
634            catch (final IOException ioe) 
635            {
636                exception = ioe;
637            }
638        }
639
640        if (null != exception) 
641        {
642            throw exception;
643        }
644    }
645
646    /**
647     * Lists files in a directory, asserting that the supplied directory satisfies exists and is a directory
648     * @param directory The directory to list
649     * @return The files in the directory, never null.
650     * @throws IOException if an I/O error occurs
651     */
652    private static Path[] verifiedListFiles(final Path directory) throws IOException 
653    {
654        if (!Files.exists(directory)) 
655        {
656            final String message = directory + " does not exist";
657            throw new IllegalArgumentException(message);
658        }
659
660        if (!Files.isDirectory(directory)) 
661        {
662            final String message = directory + " is not a directory";
663            throw new IllegalArgumentException(message);
664        }
665        
666        try (Stream<Path> s = Files.list(directory))
667        {
668            List<Path> collect = s.collect(Collectors.toList());
669            return collect.toArray(new Path[collect.size()]);
670        }
671    }
672
673    /**
674     * Deletes a file. If file is a directory, delete it and all sub-directories.
675     * <p>
676     * The difference between File.delete() and this method are:
677     * <ul>
678     * <li>A directory to be deleted does not have to be empty.</li>
679     * <li>You get exceptions when a file or directory cannot be deleted.
680     * (java.io.File methods returns a boolean)</li>
681     * </ul>
682     *
683     * @param file file or directory to delete, must not be {@code null}
684     * @throws NullPointerException  if the directory is {@code null}
685     * @throws FileNotFoundException if the file was not found
686     * @throws IOException           in case deletion is unsuccessful
687     */
688    public static void forceDelete(final Path file) throws IOException 
689    {
690        if (Files.isDirectory(file)) 
691        {
692            deleteDirectory(file);
693        } 
694        else 
695        {
696            final boolean filePresent = Files.exists(file);
697            if (!Files.deleteIfExists(file)) 
698            {
699                if (!filePresent) 
700                {
701                    throw new FileNotFoundException("File does not exist: " + file);
702                }
703                final String message = "Unable to delete file: " + file;
704                throw new IOException(message);
705            }
706        }
707    }
708    
709    /**
710     * Deletes a directory recursively.
711     *
712     * @param directory directory to delete
713     * @throws IOException              in case deletion is unsuccessful
714     * @throws IllegalArgumentException if {@code directory} does not exist or is not a directory
715     */
716    public static void deleteDirectory(final Path directory) throws IOException 
717    {
718        if (!Files.exists(directory)) 
719        {
720            return;
721        }
722
723        if (!isSymlink(directory)) 
724        {
725            cleanDirectory(directory);
726        }
727
728        if (!Files.deleteIfExists(directory)) 
729        {
730            final String message = "Unable to delete directory " + directory + ".";
731            throw new IOException(message);
732        }
733    }
734    
735    /**
736     * Determines whether the specified file is a Symbolic Link rather than an actual file.
737     * <p>
738     * Will not return true if there is a Symbolic Link anywhere in the path,
739     * only if the specific file is.
740     * <p>
741     * When using jdk1.7, this method delegates to {@code boolean java.nio.file.Files.isSymbolicLink(Path path)}
742     *
743     * <p>
744     * For code that runs on Java 1.7 or later, use the following method instead:
745     * <br>
746     * {@code boolean java.nio.file.Files.isSymbolicLink(Path path)}
747     * @param file the file to check
748     * @return true if the file is a Symbolic Link
749     * @throws IOException if an IO error occurs while checking the file
750     */
751    public static boolean isSymlink(final Path file) throws IOException 
752    {
753        if (file == null) 
754        {
755            throw new NullPointerException("File must not be null");
756        }
757        return Files.isSymbolicLink(file);
758    }
759    
760    /**
761     * Copies a file to a new location preserving the file date.
762     * <p>
763     * This method copies the contents of the specified source file to the
764     * specified destination file. The directory holding the destination file is
765     * created if it does not exist. If the destination file exists, then this
766     * method will overwrite it.
767     * <p>
768     * <strong>Note:</strong> This method tries to preserve the file's last
769     * modified date/times using {@link File#setLastModified(long)}, however
770     * it is not guaranteed that the operation will succeed.
771     * If the modification operation fails, no indication is provided.
772     *
773     * @param srcFile  an existing file to copy, must not be {@code null}
774     * @param destFile the new file, must not be {@code null}
775     *
776     * @throws NullPointerException if source or destination is {@code null}
777     * @throws IOException          if source or destination is invalid
778     * @throws IOException          if an IO error occurs during copying
779     * @throws IOException          if the output file length is not the same as the input file length after the copy
780     * completes
781     * @see #copyFile(Path, Path, boolean)
782     */
783    public static void copyFile(final Path srcFile, final Path destFile) throws IOException 
784    {
785        copyFile(srcFile, destFile, true);
786    }
787
788    /**
789     * Copies a file to a new location.
790     * <p>
791     * This method copies the contents of the specified source file
792     * to the specified destination file.
793     * The directory holding the destination file is created if it does not exist.
794     * If the destination file exists, then this method will overwrite it.
795     * <p>
796     * <strong>Note:</strong> Setting <code>preserveFileDate</code> to
797     * {@code true} tries to preserve the file's last modified
798     * date/times using {@link File#setLastModified(long)}, however it is
799     * not guaranteed that the operation will succeed.
800     * If the modification operation fails, no indication is provided.
801     *
802     * @param srcFile          an existing file to copy, must not be {@code null}
803     * @param destFile         the new file, must not be {@code null}
804     * @param preserveFileDate true if the file date of the copy
805     *                         should be the same as the original
806     *
807     * @throws NullPointerException if source or destination is {@code null}
808     * @throws IOException          if source or destination is invalid
809     * @throws IOException          if an IO error occurs during copying
810     * @throws IOException          if the output file length is not the same as the input file length after the copy
811     * completes
812     * @see #doCopyFile(Path, Path, boolean)
813     */
814    public static void copyFile(final Path srcFile, final Path destFile, final boolean preserveFileDate) throws IOException 
815    {
816        checkFileRequirements(srcFile, destFile);
817        if (Files.isDirectory(srcFile)) 
818        {
819            throw new IOException("Source '" + srcFile + "' exists but is a directory");
820        }
821        if (srcFile.toAbsolutePath().equals(destFile.toAbsolutePath())) 
822        {
823            throw new IOException("Source '" + srcFile + "' and destination '" + destFile + "' are the same");
824        }
825        
826        final Path parentFile = destFile.getParent();
827        if (parentFile != null) 
828        {
829            Files.createDirectories(parentFile);
830            if (!Files.isDirectory(parentFile)) 
831            {
832                throw new IOException("Destination '" + parentFile + "' directory cannot be created");
833            }
834        }
835        if (Files.exists(destFile) && !Files.isWritable(destFile)) 
836        {
837            throw new IOException("Destination '" + destFile + "' exists but is read-only");
838        }
839        doCopyFile(srcFile, destFile, preserveFileDate);
840    }
841}