Add PathUtils.createParentDirectories(Path, LinkOption,
FileAttribute<?>...)
MODIFY
src/changes/changes.xml
src/changes/changes.xml
@@ -320,6 +320,9 @@ The <action> type attribute can be add,update,fix,remove.
<action dev="ggregory" type="add" due-to="Gary Gregory">
Add CharsetDecoders.
</action>
+ <action dev="ggregory" type="add" due-to="Gary Gregory">
+ Add PathUtils.createParentDirectories(Path, LinkOption, FileAttribute...).
+ </action>
<!-- UPDATE -->
<action dev="ggregory" type="add" due-to="Gary Gregory">
Update FileEntry to use FileTime instead of long for file time stamps.

MODIFY
src/main/java/org/apache/commons/io/file/PathUtils.java
src/main/java/org/apache/commons/io/file/PathUtils.java
public final class PathUtils {
/**
- * Private worker/holder that computes and tracks relative path names and their equality. We reuse the sorted relative lists when comparing directories.
+ * Private worker/holder that computes and tracks relative path names and their equality. We reuse the sorted relative
+ * lists when comparing directories.
*/
private static class RelativeSortedPaths {
@@ -176,6 +177,13 @@ private RelativeSortedPaths(final Path dir1, final Path dir2, final int maxDepth
*/
public static final LinkOption[] NOFOLLOW_LINK_OPTION_ARRAY = {LinkOption.NOFOLLOW_LINKS};
+ /**
+ * A LinkOption used to follow link in this class, the inverse of {@link LinkOption#NOFOLLOW_LINKS}.
+ *
+ * @since 2.12.0
+ */
+ public static final LinkOption FOLLOW_LINKS = null;
+
/**
* Empty {@link OpenOption} array.
*/
@@ -335,10 +343,29 @@ public static PathCounters countDirectoryAsBigInteger(final Path directory) thro
* @since 2.9.0
*/
public static Path createParentDirectories(final Path path, final FileAttribute<?>... attrs) throws IOException {
- final Path parent = getParent(path);
+ return createParentDirectories(path, LinkOption.NOFOLLOW_LINKS, attrs);
+ }
+
+ /**
+ * Creates the parent directories for the given {@code path}.
+ *
+ * @param path The path to a file (or directory).
+ * @param linkOption A {@link LinkOption} or null.
+ * @param attrs An optional list of file attributes to set atomically when creating the directories.
+ * @return The Path for the {@code path}'s parent directory or null if the given path has no parent.
+ * @throws IOException if an I/O error occurs.
+ * @since 2.12.0
+ */
+ public static Path createParentDirectories(final Path path, final LinkOption linkOption, final FileAttribute<?>... attrs) throws IOException {
+ Path parent = getParent(path);
+ parent = linkOption == LinkOption.NOFOLLOW_LINKS ? parent : readIfSymbolicLink(parent);
return parent == null ? null : Files.createDirectories(parent, attrs);
}
+ private static Path readIfSymbolicLink(Path path) throws IOException {
+ return Files.isSymbolicLink(path) ? Files.readSymbolicLink(path) : path;
+ }
+
/**
* Gets the current directory.
*
@@ -536,8 +563,8 @@ public static PathCounters deleteFile(final Path file, final LinkOption[] linkOp
}
/**
- * Compares the file sets of two Paths to determine if they are equal or not while considering file contents. The comparison includes all files in all
- * sub-directories.
+ * Compares the file sets of two Paths to determine if they are equal or not while considering file contents. The
+ * comparison includes all files in all sub-directories.
*
* @param path1 The first directory.
* @param path2 The second directory.
@@ -549,8 +576,8 @@ public static boolean directoryAndFileContentEquals(final Path path1, final Path
}
/**
- * Compares the file sets of two Paths to determine if they are equal or not while considering file contents. The comparison includes all files in all
- * sub-directories.
+ * Compares the file sets of two Paths to determine if they are equal or not while considering file contents. The
+ * comparison includes all files in all sub-directories.
*
* @param path1 The first directory.
* @param path2 The second directory.
@@ -593,8 +620,8 @@ public static boolean directoryAndFileContentEquals(final Path path1, final Path
}
/**
- * Compares the file sets of two Paths to determine if they are equal or not without considering file contents. The comparison includes all files in all
- * sub-directories.
+ * Compares the file sets of two Paths to determine if they are equal or not without considering file contents. The
+ * comparison includes all files in all sub-directories.
*
* @param path1 The first directory.
* @param path2 The second directory.
@@ -606,8 +633,8 @@ public static boolean directoryContentEquals(final Path path1, final Path path2)
}
/**
- * Compares the file sets of two Paths to determine if they are equal or not without considering file contents. The comparison includes all files in all
- * sub-directories.
+ * Compares the file sets of two Paths to determine if they are equal or not without considering file contents. The
+ * comparison includes all files in all sub-directories.
*
* @param path1 The first directory.
* @param path2 The second directory.
@@ -701,8 +728,8 @@ public static boolean fileContentEquals(final Path path1, final Path path2, fina
/**
* <p>
- * Applies an {@link IOFileFilter} to the provided {@link File} objects. The resulting array is a subset of the original file list that matches the provided
- * filter.
+ * Applies an {@link IOFileFilter} to the provided {@link File} objects. The resulting array is a subset of the original
+ * file list that matches the provided filter.
* </p>
*
* <p>
@@ -819,10 +846,10 @@ public static Path getTempDirectory() {
*
* @param path the path to the file.
* @param options options indicating how to handle symbolic links
- * @return {@code true} if the file is a directory; {@code false} if the path is null, the file does not exist, is not a directory, or it cannot be
- * determined if the file is a directory or not.
- * @throws SecurityException In the case of the default provider, and a security manager is installed, the {@link SecurityManager#checkRead(String)
- * checkRead} method is invoked to check read access to the directory.
+ * @return {@code true} if the file is a directory; {@code false} if the path is null, the file does not exist, is not a
+ * directory, or it cannot be determined if the file is a directory or not.
+ * @throws SecurityException In the case of the default provider, and a security manager is installed, the
+ * {@link SecurityManager#checkRead(String) checkRead} method is invoked to check read access to the directory.
* @since 2.9.0
*/
public static boolean isDirectory(final Path path, final LinkOption... options) {
@@ -845,10 +872,11 @@ public static boolean isEmpty(final Path path) throws IOException {
*
* @param directory the directory to query.
* @return whether the directory is empty.
- * @throws NotDirectoryException if the file could not otherwise be opened because it is not a directory <i>(optional specific exception)</i>.
+ * @throws NotDirectoryException if the file could not otherwise be opened because it is not a directory <i>(optional
+ * specific exception)</i>.
* @throws IOException if an I/O error occurs.
- * @throws SecurityException In the case of the default provider, and a security manager is installed, the {@link SecurityManager#checkRead(String)
- * checkRead} method is invoked to check read access to the directory.
+ * @throws SecurityException In the case of the default provider, and a security manager is installed, the
+ * {@link SecurityManager#checkRead(String) checkRead} method is invoked to check read access to the directory.
*/
public static boolean isEmptyDirectory(final Path directory) throws IOException {
try (DirectoryStream<Path> directoryStream = Files.newDirectoryStream(directory)) {
@@ -862,8 +890,8 @@ public static boolean isEmptyDirectory(final Path directory) throws IOException
* @param file the file to query.
* @return whether the file is empty.
* @throws IOException if an I/O error occurs.
- * @throws SecurityException In the case of the default provider, and a security manager is installed, its {@link SecurityManager#checkRead(String)
- * checkRead} method denies read access to the file.
+ * @throws SecurityException In the case of the default provider, and a security manager is installed, its
+ * {@link SecurityManager#checkRead(String) checkRead} method denies read access to the file.
*/
public static boolean isEmptyFile(final Path file) throws IOException {
return Files.size(file) <= 0;
@@ -1025,10 +1053,10 @@ public static boolean isPosix(final Path test, final LinkOption... options) {
*
* @param path the path to the file.
* @param options options indicating how to handle symbolic links.
- * @return {@code true} if the file is a regular file; {@code false} if the path is null, the file does not exist, is not a directory, or it cannot be
- * determined if the file is a regular file or not.
- * @throws SecurityException In the case of the default provider, and a security manager is installed, the {@link SecurityManager#checkRead(String)
- * checkRead} method is invoked to check read access to the directory.
+ * @return {@code true} if the file is a regular file; {@code false} if the path is null, the file does not exist, is
+ * not a directory, or it cannot be determined if the file is a regular file or not.
+ * @throws SecurityException In the case of the default provider, and a security manager is installed, the
+ * {@link SecurityManager#checkRead(String) checkRead} method is invoked to check read access to the directory.
* @since 2.9.0
*/
public static boolean isRegularFile(final Path path, final LinkOption... options) {
@@ -1048,7 +1076,8 @@ public static boolean isRegularFile(final Path path, final LinkOption... options
}
/**
- * Creates a new OutputStream by opening or creating a file, returning an output stream that may be used to write bytes to the file.
+ * Creates a new OutputStream by opening or creating a file, returning an output stream that may be used to write bytes
+ * to the file.
*
* @param path the Path.
* @param append Whether or not to append.
@@ -1086,8 +1115,8 @@ private static boolean overrideReadOnly(final DeleteOption... deleteOptions) {
}
/**
- * Reads the BasicFileAttributes from the given path. Returns null instead of throwing {@link UnsupportedOperationException}. Throws
- * {@link UncheckedIOExceptions} instead of {@link IOException}.
+ * Reads the BasicFileAttributes from the given path. Returns null instead of throwing
+ * {@link UnsupportedOperationException}. Throws {@link UncheckedIOExceptions} instead of {@link IOException}.
*
* @param <A> The {@code BasicFileAttributes} type
* @param path The Path to test.
@@ -1123,7 +1152,8 @@ public static BasicFileAttributes readBasicFileAttributes(final Path path) throw
}
/**
- * Reads the BasicFileAttributes from the given path. Returns null instead of throwing {@link UnsupportedOperationException}.
+ * Reads the BasicFileAttributes from the given path. Returns null instead of throwing
+ * {@link UnsupportedOperationException}.
*
* @param path the path to read.
* @param options options indicating how to handle symbolic links.
@@ -1135,7 +1165,8 @@ public static BasicFileAttributes readBasicFileAttributes(final Path path, final
}
/**
- * Reads the BasicFileAttributes from the given path. Returns null instead of throwing {@link UnsupportedOperationException}.
+ * Reads the BasicFileAttributes from the given path. Returns null instead of throwing
+ * {@link UnsupportedOperationException}.
*
* @param path the path to read.
* @return the path attributes.
@@ -1149,7 +1180,8 @@ public static BasicFileAttributes readBasicFileAttributesUnchecked(final Path pa
}
/**
- * Reads the DosFileAttributes from the given path. Returns null instead of throwing {@link UnsupportedOperationException}.
+ * Reads the DosFileAttributes from the given path. Returns null instead of throwing
+ * {@link UnsupportedOperationException}.
*
* @param path the path to read.
* @param options options indicating how to handle symbolic links.
@@ -1161,7 +1193,8 @@ public static DosFileAttributes readDosFileAttributes(final Path path, final Lin
}
/**
- * Reads the PosixFileAttributes or DosFileAttributes from the given path. Returns null instead of throwing {@link UnsupportedOperationException}.
+ * Reads the PosixFileAttributes or DosFileAttributes from the given path. Returns null instead of throwing
+ * {@link UnsupportedOperationException}.
*
* @param path The Path to read.
* @param options options indicating how to handle symbolic links.
@@ -1174,7 +1207,8 @@ public static BasicFileAttributes readOsFileAttributes(final Path path, final Li
}
/**
- * Reads the PosixFileAttributes from the given path. Returns null instead of throwing {@link UnsupportedOperationException}.
+ * Reads the PosixFileAttributes from the given path. Returns null instead of throwing
+ * {@link UnsupportedOperationException}.
*
* @param path The Path to read.
* @param options options indicating how to handle symbolic links.
@@ -1217,7 +1251,8 @@ public static String readString(final Path path, final Charset charset) throws I
}
/**
- * Throws an {@link IllegalArgumentException} if the file is not writable. This provides a more precise exception message than a plain access denied.
+ * Throws an {@link IllegalArgumentException} if the file is not writable. This provides a more precise exception
+ * message than a plain access denied.
*
* @param file The file to test.
* @param name The parameter name to use in the exception message.
@@ -1407,11 +1442,11 @@ public static Path setReadOnly(final Path path, final boolean readOnly, final Li
}
/**
- * Returns the size of the given file or directory. If the provided {@link Path} is a regular file, then the file's size is returned. If the argument is a
- * directory, then the size of the directory is calculated recursively.
+ * Returns the size of the given file or directory. If the provided {@link Path} is a regular file, then the file's size
+ * is returned. If the argument is a directory, then the size of the directory is calculated recursively.
* <p>
- * Note that overflow is not detected, and the return value may be negative if overflow occurs. See {@link #sizeOfAsBigInteger(Path)} for an alternative
- * method that does not overflow.
+ * Note that overflow is not detected, and the return value may be negative if overflow occurs. See
+ * {@link #sizeOfAsBigInteger(Path)} for an alternative method that does not overflow.
* </p>
*
* @param path the regular file or directory to return the size of, must not be {@code null}.
@@ -1427,8 +1462,8 @@ public static long sizeOf(final Path path) throws IOException {
}
/**
- * Returns the size of the given file or directory. If the provided {@link Path} is a regular file, then the file's size is returned. If the argument is a
- * directory, then the size of the directory is calculated recursively.
+ * Returns the size of the given file or directory. If the provided {@link Path} is a regular file, then the file's size
+ * is returned. If the argument is a directory, then the size of the directory is calculated recursively.
*
* @param path the regular file or directory to return the size of (must not be {@code null}).
* @return the length of the file, or recursive size of the directory, provided (in bytes).
@@ -1445,12 +1480,13 @@ public static BigInteger sizeOfAsBigInteger(final Path path) throws IOException
/**
* Counts the size of a directory recursively (sum of the size of all files).
* <p>
- * Note that overflow is not detected, and the return value may be negative if overflow occurs. See {@link #sizeOfDirectoryAsBigInteger(Path)} for an
- * alternative method that does not overflow.
+ * Note that overflow is not detected, and the return value may be negative if overflow occurs. See
+ * {@link #sizeOfDirectoryAsBigInteger(Path)} for an alternative method that does not overflow.
* </p>
*
* @param directory directory to inspect, must not be {@code null}.
- * @return size of directory in bytes, 0 if directory is security restricted, a negative number when the real total is greater than {@link Long#MAX_VALUE}.
+ * @return size of directory in bytes, 0 if directory is security restricted, a negative number when the real total is
+ * greater than {@link Long#MAX_VALUE}.
* @throws NullPointerException if the directory is {@code null}.
* @throws IOException if an I/O error occurs.
* @since 2.12.0
@@ -1556,7 +1592,8 @@ public static BigInteger sizeOfDirectoryAsBigInteger(final Path directory) throw
/**
* Waits for the file system to propagate a file creation, with a timeout.
* <p>
- * This method repeatedly tests {@link Files#exists(Path,LinkOption...)} until it returns true up to the maximum time given.
+ * This method repeatedly tests {@link Files#exists(Path,LinkOption...)} until it returns true up to the maximum time
+ * given.
* </p>
*
* @param file the file to check, must not be {@code null}.

MODIFY
src/test/java/org/apache/commons/io/file/PathUtilsTest.java
src/test/java/org/apache/commons/io/file/PathUtilsTest.java
@@ -180,7 +180,15 @@ public void testCreateDirectoriesNew() throws IOException {
}
@Test
- public void testCreateDirectoriesWithClashingSymlink() throws IOException {
+ public void testCreateDirectoriesSymlink() throws IOException {
+ final Path symlinkedDir = createTempSymlinkedRelativeDir();
+ final String leafDirName = "child";
+ final Path newDirFollowed = PathUtils.createParentDirectories(symlinkedDir.resolve(leafDirName), PathUtils.FOLLOW_LINKS);
+ assertEquals(Files.readSymbolicLink(symlinkedDir), newDirFollowed);
+ }
+
+ @Test
+ public void testCreateDirectoriesSymlinkClashing() throws IOException {
final Path symlinkedDir = createTempSymlinkedRelativeDir();
assertThrowsExactly(FileAlreadyExistsException.class, () -> PathUtils.createParentDirectories(symlinkedDir.resolve("child")));
}
@@ -274,6 +282,13 @@ public void testNewOutputStreamNewFileInsideExistingSymlinkedDir() throws IOExce
assertThrowsExactly(FileAlreadyExistsException.class, () -> PathUtils.newOutputStream(file, false));
}
+ @Test
+ public void testNewOutputStreamNewFileInsideExistingSymlinkedDirFollow() throws IOException {
+ final Path symlinkDir = createTempSymlinkedRelativeDir();
+ final Path file = symlinkDir.resolve("test.txt");
+ assertThrowsExactly(FileAlreadyExistsException.class, () -> PathUtils.newOutputStream(file, false));
+ }
+
@Test
public void testReadAttributesPosix() throws IOException {
boolean isPosix;

Add PathUtils.createParentDirectories(Path, LinkOption,
FileAttribute<?>...)