com.helger.commons.io.file.PathHelper Maven / Gradle / Ivy
Show all versions of ph-commons Show documentation
/*
* Copyright (C) 2014-2024 Philip Helger (www.helger.com)
* philip[at]helger[dot]com
*
* Licensed 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.
*/
package com.helger.commons.io.file;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Reader;
import java.io.UncheckedIOException;
import java.io.Writer;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.Charset;
import java.nio.file.FileSystemLoopException;
import java.nio.file.FileVisitOption;
import java.nio.file.FileVisitResult;
import java.nio.file.FileVisitor;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.EnumSet;
import java.util.Set;
import java.util.function.Predicate;
import javax.annotation.Nonnegative;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.helger.commons.ValueEnforcer;
import com.helger.commons.annotation.PresentForCodeCoverage;
import com.helger.commons.annotation.ReturnsMutableCopy;
import com.helger.commons.collection.impl.CommonsArrayList;
import com.helger.commons.collection.impl.ICommonsList;
import com.helger.commons.io.EAppend;
import com.helger.commons.io.stream.NonBlockingBufferedInputStream;
import com.helger.commons.io.stream.NonBlockingBufferedOutputStream;
import com.helger.commons.io.stream.NonBlockingBufferedReader;
import com.helger.commons.io.stream.NonBlockingBufferedWriter;
import com.helger.commons.io.stream.StreamHelper;
import com.helger.commons.state.EChange;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
/**
* Miscellaneous file utility methods.
*
* @author Philip Helger
*/
@Immutable
public final class PathHelper
{
private static final Logger LOGGER = LoggerFactory.getLogger (PathHelper.class);
@PresentForCodeCoverage
private static final PathHelper INSTANCE = new PathHelper ();
private PathHelper ()
{}
/**
* Check if the passed file can read and write. If the file already exists,
* the file itself is checked. If the file does not exist, the parent
* directory
*
* @param aFile
* The file to be checked. May be null
.
* @return true
if the file can be read and written
*/
public static boolean canReadAndWriteFile (@Nullable final Path aFile)
{
if (aFile == null)
return false;
// The Files API seem to be slow
return FileHelper.canReadAndWriteFile (aFile.toFile ());
// if (Files.isRegularFile (aFile))
// {
// // Path exists
// if (!Files.isReadable (aFile) || !Files.isWritable (aFile))
// return false;
// }
// else
// {
// // Path does not exist (yet)
// // Check parent directory
// final Path aParentDir = aFile.getParent ();
// if (aParentDir == null ||
// !Files.isDirectory (aParentDir) ||
// !Files.isReadable (aParentDir) ||
// !Files.isWritable (aParentDir))
// return false;
// }
// return true;
}
@Nonnull
public static EChange ensureParentDirectoryIsPresent (@Nonnull final Path aFile)
{
ValueEnforcer.notNull (aFile, "Path");
// The Files API seem to be slow
return FileHelper.ensureParentDirectoryIsPresent (aFile.toFile ());
// // If the file has no parent, it is located in the root...
// final Path aParent = aFile.getParent ();
// if (aParent == null || Files.exists (aParent))
// {
// if (aParent != null && !Files.isDirectory (aParent))
// LOGGER.warn ("Parent object specified is not a directory: '" + aParent
// + "'");
// return EChange.UNCHANGED;
// }
//
// // Now try to create the directory
// final FileIOError aError = PathOperations.createDirRecursive (aParent);
// if (aError.isFailure ())
// throw new IllegalStateException ("Failed to create parent of " +
// aFile.toAbsolutePath () + ": " + aError);
//
// // Check again if it exists, to be 100% sure :)
// if (!Files.exists (aParent))
// throw new IllegalStateException ("Parent of " + aFile.toAbsolutePath () +
// " is still not existing!");
// return EChange.CHANGED;
}
/**
* Get the canonical file of the passed file, if the file is not
* null
.
*
* @param aFile
* The file to get the canonical path from. May be null
.
* @return null
if the passed file is null
.
* @throws IOException
* If an I/O error occurs, which is possible because the construction
* of the canonical pathname may require filesystem queries
*/
@Nullable
public static Path getCanonicalFile (@Nullable final Path aFile) throws IOException
{
return aFile == null ? null : aFile.toRealPath ();
}
/**
* Get the canonical file of the passed file, if the file is not
* null
. In case of an {@link IOException}, null
is
* returned.
*
* @param aFile
* The file to get the canonical path from. May be null
.
* @return null
if the passed file is null
or an
* exception occurred.
*/
@Nullable
public static Path getCanonicalFileOrNull (@Nullable final Path aFile)
{
if (aFile != null)
try
{
return aFile.toRealPath ();
}
catch (final IOException ex)
{
// fall through
}
return null;
}
/**
* Get the canonical path of the passed file, if the file is not
* null
.
*
* @param aFile
* The file to get the canonical path from. May be null
.
* @return null
if the passed file is null
.
* @throws IOException
* If an I/O error occurs, which is possible because the construction
* of the canonical pathname may require filesystem queries
*/
@Nullable
public static String getCanonicalPath (@Nullable final Path aFile) throws IOException
{
// Note: getCanonicalPath may be a bottleneck on Unix based file systems!
return aFile == null ? null : aFile.toRealPath ().toString ();
}
/**
* Get the canonical path of the passed file, if the file is not
* null
. In case of an {@link IOException}, null
is
* returned.
*
* @param aFile
* The file to get the canonical path from. May be null
.
* @return null
if the passed file is null
.
*/
@Nullable
public static String getCanonicalPathOrNull (@Nullable final Path aFile)
{
if (aFile != null)
try
{
// Note: toRealPath may be a bottleneck on Unix based file
// systems!
return aFile.toRealPath ().toString ();
}
catch (final IOException ex)
{
// fall through
}
return null;
}
/**
* Check if the searched directory is a parent object of the start directory
*
* @param aSearchDirectory
* The directory to be searched. May not be null
.
* @param aStartDirectory
* The directory where the search starts. May not be null
.
* @return true
if the search directory is a parent of the start
* directory, false
otherwise.
* @see #getCanonicalFile(Path)
*/
@SuppressFBWarnings ("IL_INFINITE_LOOP")
public static boolean isParentDirectory (@Nonnull final Path aSearchDirectory, @Nonnull final Path aStartDirectory)
{
ValueEnforcer.notNull (aSearchDirectory, "SearchDirectory");
ValueEnforcer.notNull (aStartDirectory, "StartDirectory");
Path aRealSearchDirectory = aSearchDirectory.toAbsolutePath ();
Path aRealStartDirectory = aStartDirectory.toAbsolutePath ();
try
{
aRealSearchDirectory = getCanonicalFile (aRealSearchDirectory);
aRealStartDirectory = getCanonicalFile (aRealStartDirectory);
}
catch (final IOException ex)
{
// ignore
}
if (aRealSearchDirectory.toFile ().isDirectory ())
{
Path aParent = aRealStartDirectory;
while (aParent != null)
{
if (aParent.equals (aRealSearchDirectory))
return true;
aParent = aParent.getParent ();
}
}
return false;
}
@Nullable
public static InputStream getInputStream (@Nonnull final Path aFile)
{
ValueEnforcer.notNull (aFile, "Path");
try
{
return Files.newInputStream (aFile);
}
catch (final IOException ex)
{
return null;
}
}
@Nullable
public static NonBlockingBufferedInputStream getBufferedInputStream (@Nonnull final Path aFile)
{
ValueEnforcer.notNull (aFile, "File");
final InputStream aIS = getInputStream (aFile);
if (aIS == null)
return null;
return new NonBlockingBufferedInputStream (aIS);
}
@Nullable
public static Reader getReader (@Nonnull final Path aFile, @Nonnull final Charset aCharset)
{
ValueEnforcer.notNull (aFile, "Path");
ValueEnforcer.notNull (aCharset, "Charset");
return StreamHelper.createReader (getInputStream (aFile), aCharset);
}
@Nullable
public static NonBlockingBufferedReader getBufferedReader (@Nonnull final Path aFile, @Nonnull final Charset aCharset)
{
ValueEnforcer.notNull (aFile, "Path");
ValueEnforcer.notNull (aCharset, "Charset");
final Reader aReader = getReader (aFile, aCharset);
if (aReader == null)
return null;
return new NonBlockingBufferedReader (aReader);
}
/**
* Get an output stream for writing to a file.
*
* @param aFile
* The file to write to. May not be null
.
* @return null
if the file could not be opened
*/
@Nullable
public static OutputStream getOutputStream (@Nonnull final Path aFile)
{
return getOutputStream (aFile, EAppend.DEFAULT);
}
/**
* Get an output stream for writing to a file.
*
* @param aFile
* The file to write to. May not be null
.
* @param eAppend
* Appending mode. May not be null
.
* @return null
if the file could not be opened
*/
@Nullable
public static OutputStream getOutputStream (@Nonnull final Path aFile, @Nonnull final EAppend eAppend)
{
ValueEnforcer.notNull (aFile, "Path");
ValueEnforcer.notNull (eAppend, "Append");
// OK, parent is present and writable
try
{
return Files.newOutputStream (aFile, eAppend.getAsOpenOptions ());
}
catch (final IOException ex)
{
return null;
}
}
@Nullable
public static NonBlockingBufferedOutputStream getBufferedOutputStream (@Nonnull final Path aFile)
{
return getBufferedOutputStream (aFile, EAppend.DEFAULT);
}
@Nullable
public static NonBlockingBufferedOutputStream getBufferedOutputStream (@Nonnull final Path aFile,
@Nonnull final EAppend eAppend)
{
ValueEnforcer.notNull (aFile, "File");
ValueEnforcer.notNull (eAppend, "Append");
final OutputStream aOS = getOutputStream (aFile, eAppend);
if (aOS == null)
return null;
return new NonBlockingBufferedOutputStream (aOS);
}
@Nullable
public static Writer getWriter (@Nonnull final Path aFile,
@Nonnull final EAppend eAppend,
@Nonnull final Charset aCharset)
{
ValueEnforcer.notNull (aFile, "Path");
ValueEnforcer.notNull (aCharset, "Charset");
return StreamHelper.createWriter (getOutputStream (aFile, eAppend), aCharset);
}
@Nullable
public static NonBlockingBufferedWriter getBufferedWriter (@Nonnull final Path aFile,
@Nonnull final EAppend eAppend,
@Nonnull final Charset aCharset)
{
ValueEnforcer.notNull (aFile, "Path");
ValueEnforcer.notNull (aCharset, "Charset");
final Writer aWriter = getWriter (aFile, eAppend, aCharset);
if (aWriter == null)
return null;
return new NonBlockingBufferedWriter (aWriter);
}
/**
* Returns true
if the first file is newer than the second file.
* Returns true
if the first file exists and the second file does
* not exist. Returns false
if the first file is older than the
* second file. Returns false
if the first file does not exists
* but the second does. Returns false
if none of the files exist.
*
* @param aFile1
* First file. May not be null
.
* @param aFile2
* Second file. May not be null
.
* @return true
if the first file is newer than the second file,
* false
otherwise.
*/
public static boolean isFileNewer (@Nonnull final Path aFile1, @Nonnull final Path aFile2)
{
ValueEnforcer.notNull (aFile1, "File1");
ValueEnforcer.notNull (aFile2, "aFile2");
// The Files API seem to be slow
return FileHelper.isFileNewer (aFile1.toFile (), aFile2.toFile ());
// // Compare with the same file?
// if (aFile1.equals (aFile2))
// return false;
//
// // if the first file does not exists, always false
// if (!Files.exists (aFile1))
// return false;
//
// // first file exists, but second file does not
// if (!Files.exists (aFile2))
// return true;
//
// try
// {
// // both exist, compare file times
// return Files.getLastModifiedTime (aFile1).compareTo
// (Files.getLastModifiedTime (aFile2)) > 0;
// }
// catch (final IOException ex)
// {
// throw new UncheckedIOException (ex);
// }
}
/**
* Returns the number of files and directories contained in the passed
* directory excluding the system internal directories.
*
* @param aDirectory
* The directory to check. May not be null
and must be a
* directory.
* @return A non-negative number of objects in that directory.
* @see FilenameHelper#isSystemInternalDirectory(CharSequence)
*/
@Nonnegative
public static int getDirectoryObjectCount (@Nonnull final Path aDirectory)
{
ValueEnforcer.notNull (aDirectory, "Directory");
ValueEnforcer.isTrue (aDirectory.toFile ().isDirectory (), "Passed object is not a directory: " + aDirectory);
int ret = 0;
for (final Path aChild : getDirectoryContent (aDirectory))
if (!FilenameHelper.isSystemInternalDirectory (aChild))
ret++;
return ret;
}
@Nonnull
public static Path walkFileTree (@Nonnull final Path aStart,
@Nonnegative final int nMaxDepth,
@Nonnull final FileVisitor super Path> aVisitor)
{
return walkFileTree (aStart, EnumSet.noneOf (FileVisitOption.class), nMaxDepth, aVisitor);
}
/**
* Walks a file tree.
*
* This method walks a file tree rooted at a given starting file. The file
* tree traversal is depth-first with the given {@link FileVisitor}
* invoked for each file encountered. File tree traversal completes when all
* accessible files in the tree have been visited, or a visit method returns a
* result of {@link FileVisitResult#TERMINATE TERMINATE}. Where a visit method
* terminates due an {@code IOException}, an uncaught error, or runtime
* exception, then the traversal is terminated and the error or exception is
* propagated to the caller of this method.
*
* For each file encountered this method attempts to read its
* {@link java.nio.file.attribute.BasicFileAttributes}. If the file is not a
* directory then the {@link FileVisitor#visitFile visitFile} method is
* invoked with the file attributes. If the file attributes cannot be read,
* due to an I/O exception, then the {@link FileVisitor#visitFileFailed
* visitFileFailed} method is invoked with the I/O exception.
*
* Where the file is a directory, and the directory could not be opened, then
* the {@code visitFileFailed} method is invoked with the I/O exception, after
* which, the file tree walk continues, by default, at the next
* sibling of the directory.
*
* Where the directory is opened successfully, then the entries in the
* directory, and their descendants are visited. When all entries
* have been visited, or an I/O error occurs during iteration of the
* directory, then the directory is closed and the visitor's
* {@link FileVisitor#postVisitDirectory postVisitDirectory} method is
* invoked. The file tree walk then continues, by default, at the next
* sibling of the directory.
*
* By default, symbolic links are not automatically followed by this method.
* If the {@code options} parameter contains the
* {@link FileVisitOption#FOLLOW_LINKS FOLLOW_LINKS} option then symbolic
* links are followed. When following links, and the attributes of the target
* cannot be read, then this method attempts to get the
* {@code BasicFileAttributes} of the link. If they can be read then the
* {@code visitFile} method is invoked with the attributes of the link
* (otherwise the {@code visitFileFailed} method is invoked as specified
* above).
*
* If the {@code options} parameter contains the
* {@link FileVisitOption#FOLLOW_LINKS FOLLOW_LINKS} option then this method
* keeps track of directories visited so that cycles can be detected. A cycle
* arises when there is an entry in a directory that is an ancestor of the
* directory. Cycle detection is done by recording the
* {@link java.nio.file.attribute.BasicFileAttributes#fileKey file-key} of
* directories, or if file keys are not available, by invoking the
* {@link Files#isSameFile} method to test if a directory is the same file as
* an ancestor. When a cycle is detected it is treated as an I/O error, and
* the {@link FileVisitor#visitFileFailed visitFileFailed} method is invoked
* with an instance of {@link FileSystemLoopException}.
*
* The {@code maxDepth} parameter is the maximum number of levels of
* directories to visit. A value of {@code 0} means that only the starting
* file is visited, unless denied by the security manager. A value of
* {@link Integer#MAX_VALUE MAX_VALUE} may be used to indicate that all levels
* should be visited. The {@code visitFile} method is invoked for all files,
* including directories, encountered at {@code maxDepth}, unless the basic
* file attributes cannot be read, in which case the {@code
* visitFileFailed} method is invoked.
*
* If a visitor returns a result of {@code null} then {@code
* NullPointerException} is thrown.
*
* When a security manager is installed and it denies access to a file (or
* directory), then it is ignored and the visitor is not invoked for that file
* (or directory).
*
* @param aStart
* the starting file
* @param aOptions
* options to configure the traversal
* @param nMaxDepth
* the maximum number of directory levels to visit
* @param aVisitor
* the file visitor to invoke for each file
* @return the starting file
* @throws UncheckedIOException
* if an I/O error is thrown by a visitor method
*/
@Nonnull
public static Path walkFileTree (@Nonnull final Path aStart,
@Nonnull final Set aOptions,
@Nonnegative final int nMaxDepth,
@Nonnull final FileVisitor super Path> aVisitor)
{
try
{
return Files.walkFileTree (aStart, aOptions, nMaxDepth, aVisitor);
}
catch (final IOException ex)
{
throw new UncheckedIOException (ex);
}
}
@Nonnull
@ReturnsMutableCopy
private static ICommonsList _getDirectoryContent (@Nonnull final Path aDirectory,
@Nullable final Predicate super Path> aPathFilter)
{
final ICommonsList ret = new CommonsArrayList <> ();
walkFileTree (aDirectory, 1, new SimpleFileVisitor ()
{
@Override
public FileVisitResult visitFile (final Path aCurFile, final BasicFileAttributes attrs) throws IOException
{
if (aPathFilter == null || aPathFilter.test (aCurFile))
ret.add (aCurFile);
return FileVisitResult.CONTINUE;
}
});
if (ret.isEmpty ())
{
// No content returned
if (aPathFilter == null)
{
// Try some diagnosis...
final File aDirectoryFile = aDirectory.toFile ();
if (!aDirectoryFile.isDirectory ())
{
LOGGER.warn ("Cannot list non-directory: " + aDirectory.toAbsolutePath ());
}
else
if (!Files.isExecutable (aDirectory))
{
// If this happens, the resulting Path objects are neither files nor
// directories (isFile() and isDirectory() both return false!!)
LOGGER.warn ("Existing directory is missing the listing permission: " + aDirectory.toAbsolutePath ());
}
else
if (!Files.isReadable (aDirectory))
{
LOGGER.warn ("Cannot list directory because of no read-rights: " + aDirectory.toAbsolutePath ());
}
else
if (!aDirectoryFile.exists ())
{
LOGGER.warn ("Cannot list non-existing: " + aDirectory.toAbsolutePath ());
}
}
}
else
{
if (!Files.isExecutable (aDirectory))
{
// If this happens, the resulting Path objects are neither files nor
// directories (isFile() and isDirectory() both return false!!)
LOGGER.warn ("Directory is missing the listing permission: " + aDirectory.toAbsolutePath ());
}
}
return ret;
}
/**
* This is a replacement for Path.listFiles()
doing some
* additional checks on permissions. The order of the returned files is
* undefined. "." and ".." are not contained.
*
* @param aDirectory
* The directory to be listed. May not be null
.
* @return Never null
.
*/
@Nonnull
@ReturnsMutableCopy
public static ICommonsList getDirectoryContent (@Nonnull final Path aDirectory)
{
ValueEnforcer.notNull (aDirectory, "Directory");
return _getDirectoryContent (aDirectory, null);
}
/**
* This is a replacement for Path.listFiles(FilenameFilter)
doing
* some additional checks on permissions. The order of the returned files is
* undefined. "." and ".." are not contained.
*
* @param aDirectory
* The directory to be listed. May not be null
.
* @param aPathFilter
* The path filter to be used. May not be null
.
* @return Never null
.
*/
@Nonnull
@ReturnsMutableCopy
public static ICommonsList getDirectoryContent (@Nonnull final Path aDirectory,
@Nullable final Predicate super Path> aPathFilter)
{
ValueEnforcer.notNull (aDirectory, "Directory");
return _getDirectoryContent (aDirectory, aPathFilter);
}
@Nullable
public static URL getAsURL (@Nonnull final Path aPath)
{
ValueEnforcer.notNull (aPath, "Path");
try
{
return aPath.toUri ().toURL ();
}
catch (final MalformedURLException ex)
{
LOGGER.warn ("Failed to convert path to URL: " + aPath, ex);
return null;
}
}
}