/** * Licensed under the GNU LGPL v.2.1 or later. */ package info.freelibrary.util; import static java.nio.file.attribute.PosixFilePermission.GROUP_EXECUTE; import static java.nio.file.attribute.PosixFilePermission.GROUP_READ; import static java.nio.file.attribute.PosixFilePermission.GROUP_WRITE; import static java.nio.file.attribute.PosixFilePermission.OTHERS_EXECUTE; import static java.nio.file.attribute.PosixFilePermission.OTHERS_READ; import static java.nio.file.attribute.PosixFilePermission.OTHERS_WRITE; import static java.nio.file.attribute.PosixFilePermission.OWNER_EXECUTE; import static java.nio.file.attribute.PosixFilePermission.OWNER_READ; import static java.nio.file.attribute.PosixFilePermission.OWNER_WRITE; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.FilenameFilter; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; import java.net.URLConnection; import java.nio.channels.FileChannel; import java.nio.file.attribute.PosixFilePermission; import java.security.DigestInputStream; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.EnumSet; import java.util.Formatter; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.transform.TransformerException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.w3c.dom.Document; import org.w3c.dom.Element; /** * Utilities for working with files. * * @author <a href="mailto:ksclarke@ksclarke.io">Kevin S. Clarke</a> */ public class FileUtils { public static final String DATE_FORMAT = "yyyy-MM-dd HH:mm:ss Z"; private static final String DIR_TYPE = "dir"; private static final String FILE_TYPE = "file"; private static final String FILE_PATH = "path"; private static final String MODIFIED = "modified"; private static final Logger LOGGER = LoggerFactory.getLogger(FileUtils.class); /** * Constructor for the contained file utilities. */ private FileUtils() { } /** * Creates a string of XML that describes the supplied file or directory (and, possibly, all its subdirectories). * Includes absolute path, last modified time, read/write permissions, etc. By default, it only descends one * directory. * * @param aFilePath A file system path to be turned into as XML * @return Am XML string representation of the file structure * @throws FileNotFoundException If the supplied file or directory can not be found * @throws TransformerException If there is trouble with the XSL transformation */ public static String toXML(final String aFilePath) throws FileNotFoundException, TransformerException { return toXML(aFilePath, ".*"); } /** * Creates an <code>Element</code> that describes the supplied file or directory (and, possibly, all its * subdirectories). Includes absolute path, last modified time, read/write permissions, etc. By default, it only * descends one directory. * * @param aFilePath A file system path to turn into XML * @return An element representation of the file structure * @throws FileNotFoundException If the supplied file does not exist * @throws ParserConfigurationException If the default XML parser for the JRE isn't configured correctly */ public static Element toElement(final String aFilePath) throws FileNotFoundException, ParserConfigurationException { return toElement(aFilePath, ".*"); } /** * Creates an <code>Element</code> that describes the supplied file or directory (and, possibly, all its * subdirectories). Includes absolute path, last modified time, read/write permissions, etc. * * @param aFilePath A file system path to turn into XML * @param aBool A boolean indicating whether the XML should contain more than one level * @return An element representation of the file structure * @throws FileNotFoundException If the supplied file does not exist * @throws ParserConfigurationException If the default XML parser for the JRE isn't configured correctly */ public static Element toElement(final String aFilePath, final boolean aBool) throws FileNotFoundException, ParserConfigurationException { return toElement(aFilePath, ".*", aBool); } /** * Creates an <code>Document</code> that describes the supplied file or directory (and, possibly, all its * subdirectories). Includes absolute path, last modified time, read/write permissions, etc. By default, it only * descends one directory. * * @param aFilePath A file system path to turn into XML * @return A document representation of the file structure * @throws FileNotFoundException If the supplied file does not exist * @throws ParserConfigurationException If the default XML parser for the JRE isn't configured correctly */ public static Document toDocument(final String aFilePath) throws FileNotFoundException, ParserConfigurationException { return toDocument(aFilePath, ".*"); } /** * Creates a string of XML that describes the supplied file or directory (and all its subdirectories). Includes * absolute path, last modified time, read/write permissions, etc. * * @param aFilePath The file or directory to be returned as XML * @param aPattern A regular expression that file names must match * @return A string of XML describing the supplied file system path's structure * @throws FileNotFoundException If the supplied file or directory can not be found * @throws TransformerException If there is trouble with the XSL transformation */ public static String toXML(final String aFilePath, final String aPattern) throws FileNotFoundException, TransformerException { return toXML(aFilePath, aPattern, false); } /** * Creates a string of XML that describes the supplied file or directory (and, optionally, all its subdirectories). * Includes absolute path, last modified time, read/write permissions, etc. * * @param aFilePath The file or directory to be returned as XML * @param aDeepConversion Whether the subdirectories are included * @return A string of XML describing the supplied file system path's structure * @throws FileNotFoundException If the supplied file or directory can not be found * @throws TransformerException If there is trouble with the XSL transformation */ public static String toXML(final String aFilePath, final boolean aDeepConversion) throws FileNotFoundException, TransformerException { return toXML(aFilePath, ".*", aDeepConversion); } /** * Creates a string of XML that describes the supplied file or directory (and, optionally, all its subdirectories). * Includes absolute path, last modified time, read/write permissions, etc. * * @param aFilePath The file or directory to be returned as XML * @param aPattern A regular expression pattern to evaluate file matches against * @param aDeepTransformation Whether the subdirectories are included * @return A string of XML describing the supplied file system path's structure * @throws FileNotFoundException If the supplied file or directory can not be found * @throws TransformerException If there is trouble with the XSL transformation */ public static String toXML(final String aFilePath, final String aPattern, final boolean aDeepTransformation) throws FileNotFoundException, TransformerException { final Element element = toElement(aFilePath, aPattern, aDeepTransformation); return DOMUtils.toXML(element); } /** * Returns a Map representation of the supplied directory's structure. The map contains the file name as the key and * its path as the value. If a file with a name occurs more than once, multiple path values are returned for that * file name key. The map that is returned is unmodifiable. * * @param aFilePath The directory of which you'd like a file listing * @return An unmodifiable map representing the files in the file structure * @throws FileNotFoundException If the directory for the supplied file path does not exist */ public static Map<String, List<String>> toHashMap(final String aFilePath) throws FileNotFoundException { return toHashMap(aFilePath, null, (String[]) null); } /** * Returns a Map representation of the supplied directory's structure. The map contains the file name as the key and * its path as the value. If a file with a name occurs more than once, multiple path values are returned for that * file name key. The map that is returned is unmodifiable. * * @param aFilePath The directory of which you'd like a file listing * @param aPattern A regular expression pattern which the files must match to be returned * @return An unmodifiable map representing the files in the file structure * @throws FileNotFoundException If the directory for the supplied file path does not exist */ public static Map<String, List<String>> toHashMap(final String aFilePath, final String aPattern) throws FileNotFoundException { return toHashMap(aFilePath, aPattern, (String[]) null); } /** * Returns a Map representation of the supplied directory's structure. The map contains the file name as the key and * its path as the value. If a file with a name occurs more than once, multiple path values are returned for that * file name key. The map that is returned is unmodifiable. * * @param aFilePath The directory of which you'd like a file listing * @param aPattern A regular expression pattern which the files must match to be returned * @param aIgnoreList A list of directories into which we shouldn't descend * @return An unmodifiable map representing the files in the file structure * @throws FileNotFoundException If the directory for the supplied file path does not exist * @throws RuntimeException If a duplicate file path name is discovered */ public static Map<String, List<String>> toHashMap(final String aFilePath, final String aPattern, final String... aIgnoreList) throws RuntimeException, FileNotFoundException { final String filePattern = aPattern != null ? aPattern : ".*"; final RegexFileFilter filter = new RegexFileFilter(filePattern); final Map<String, List<String>> fileMap = new HashMap<>(); final File source = new File(aFilePath); for (final File file : listFiles(source, filter, true, aIgnoreList)) { final String fileName = file.getName(); final String filePath = file.getAbsolutePath(); if (fileMap.containsKey(fileName)) { final List<String> paths = fileMap.get(fileName); if (!paths.contains(filePath)) { paths.add(filePath); } else { throw new RuntimeException("Duplicate file path name"); } } else { final ArrayList<String> pathList = new ArrayList<>(); pathList.add(filePath); fileMap.put(fileName, pathList); } } return Collections.unmodifiableMap(fileMap); } /** * Returns an XML Element representing the file structure found at the supplied file system path. Files included in * the representation will match the supplied regular expression pattern. This method doesn't descend through the * directory structure. * * @param aFilePath The directory from which the structural representation should be built * @param aPattern A regular expression pattern which files included in the Element should match * @return An XML Element representation of the directory structure * @throws FileNotFoundException If the supplied directory isn't found * @throws ParserConfigurationException If the default XML parser for the JRE isn't configured correctly */ public static Element toElement(final String aFilePath, final String aPattern) throws FileNotFoundException, ParserConfigurationException { return toElement(aFilePath, aPattern, false); } /** * Returns an XML Element representing the file structure found at the supplied file system path. Files included in * the representation will match the supplied regular expression pattern. This method doesn't descend through the * directory structure. * * @param aFilePath The directory from which the structural representation should be built * @param aPattern A regular expression pattern which files included in the Element should match * @param aDeepTransformation Whether the conversion should descend through subdirectories * @return An XML Element representation of the directory structure * @throws FileNotFoundException If the supplied directory isn't found */ public static Element toElement(final String aFilePath, final String aPattern, final boolean aDeepTransformation) throws FileNotFoundException { final RegexFileFilter filter = new RegexFileFilter(aPattern); final File file = new File(aFilePath); if (file.exists() && file.canRead()) { return add(file, null, filter, aDeepTransformation); } throw new FileNotFoundException(aFilePath); } /** * Returns an XML Document representing the file structure found at the supplied file system path. Files included in * the representation will match the supplied regular expression pattern. This method doesn't descend through the * directory structure. * * @param aFilePath The directory from which the structural representation should be built * @param aPattern A regular expression pattern which files included in the Element should match * @return An XML Document representation of the directory structure * @throws FileNotFoundException If the supplied directory isn't found * @throws ParserConfigurationException If the default XML parser for the JRE isn't configured correctly */ public static Document toDocument(final String aFilePath, final String aPattern) throws FileNotFoundException, ParserConfigurationException { return toDocument(aFilePath, aPattern, false); } /** * Returns an XML Document representing the file structure found at the supplied file system path. Files included in * the representation will match the supplied regular expression pattern. This method doesn't descend through the * directory structure. * * @param aFilePath The directory from which the structural representation should be built * @param aPattern A regular expression pattern which files included in the Document should match * @param aDeepConversion Whether the conversion should descend through subdirectories * @return An XML Document representation of the directory structure * @throws FileNotFoundException If the supplied directory isn't found * @throws ParserConfigurationException If the default XML parser for the JRE isn't configured correctly */ public static Document toDocument(final String aFilePath, final String aPattern, final boolean aDeepConversion) throws FileNotFoundException, ParserConfigurationException { final Element element = toElement(aFilePath, aPattern, aDeepConversion); final Document document = element.getOwnerDocument(); document.appendChild(element); return document; } /** * Returns a Java <code>File</code> for the supplied file-based URL. * * @param aURL A URL that has a file protocol * @return A Java <code>File</code> for the supplied URL * @throws MalformedURLException If the supplied URL doesn't have a file protocol */ public static File toFile(final URL aURL) throws MalformedURLException { if (aURL.getProtocol().equals("file")) { return new File(aURL.toString().replace("file:", "")); } throw new MalformedURLException("Not a file URL"); } /** * An array of all the files in the supplied directory that match the supplied <code>FilenameFilter</code>. * * @param aDir A directory from which a file listing should be returned * @param aFilter A file name filter which returned files should match * @return An array of matching files * @throws FileNotFoundException If the supplied directory doesn't exist */ public static File[] listFiles(final File aDir, final FilenameFilter aFilter) throws FileNotFoundException { return listFiles(aDir, aFilter, false, (String[]) null); } /** * An array of all the files in the supplied directory that match the supplied <code>FilenameFilter</code>. * * @param aDir A directory from which a file listing should be returned * @param aFilter A file name filter which returned files should match * @param aDeepListing Whether we should descend through subdirectories * @return An array of matching files * @throws FileNotFoundException If the supplied directory doesn't exist */ public static File[] listFiles(final File aDir, final FilenameFilter aFilter, final boolean aDeepListing) throws FileNotFoundException { return listFiles(aDir, aFilter, aDeepListing, (String[]) null); } /** * An array of all the files in the supplied directory that match the supplied <code>FilenameFilter</code>. * Directories that match the supplied ignore list will not be included in the result. * * @param aDir A directory from which a file listing should be returned * @param aFilter A file name filter which returned files should match * @param aDeepListing Whether we should descend through subdirectories * @param aIgnoreList Directory names that should be ignored in the list. * @return An array of matching files * @throws FileNotFoundException If the supplied directory doesn't exist */ public static File[] listFiles(final File aDir, final FilenameFilter aFilter, final boolean aDeepListing, final String... aIgnoreList) throws FileNotFoundException { if (!aDir.exists()) { throw new FileNotFoundException(aDir.getAbsolutePath()); } if (aDir.isFile()) { if (aFilter.accept(aDir.getParentFile(), aDir.getName())) { return new File[] { aDir }; } else { return new File[0]; } } if (!aDeepListing) { return aDir.listFiles(aFilter); } else { final ArrayList<File> fileList = new ArrayList<>(); final String[] ignoreList; if (aIgnoreList == null) { ignoreList = new String[0]; } else { ignoreList = aIgnoreList; } for (final File file : aDir.listFiles()) { final String fileName = file.getName(); if (aFilter.accept(aDir, fileName)) { if (LOGGER.isDebugEnabled()) { LOGGER.debug("Match file: {}", file); } fileList.add(file); } if (file.isDirectory() && Arrays.binarySearch(ignoreList, fileName) < 0) { if (LOGGER.isDebugEnabled()) { LOGGER.debug("Descending into: {}", file); } final File[] files = listFiles(file, aFilter, aDeepListing); fileList.addAll(Arrays.asList(files)); } } return fileList.toArray(new File[fileList.size()]); } } /** * Return a file name without the dot extension. * * @param aFile The file name * @return The file name without the extension */ public static String stripExt(final File aFile) { return stripExt(aFile.getName()); } /** * Return a file name without the dot extension. * * @param aFileName The file name from which we want to strip the extension * @return The file name without the extension */ public static String stripExt(final String aFileName) { final int index = aFileName.lastIndexOf('.'); if (index != -1) { return aFileName.substring(0, index); } return aFileName; } /** * Returns a file extension (as delimited by a period). * * @param aFileName A file name from which to get the extension * @return The extension or an empty string if the file doesn't have an extension */ public static String getExt(final String aFileName) { final int index = aFileName.lastIndexOf('.'); if (index != -1) { return aFileName.substring(index + 1, aFileName.length()); } return ""; } /** * Gets the calculated size of a directory of files. * * @param aFile A file or directory from which to calculate size * @return The calculated size of the supplied directory or file */ public static long getSize(final File aFile) { long size = 0; if (aFile != null && aFile.exists()) { if (aFile.isDirectory()) { for (final File file : aFile.listFiles()) { size += getSize(file); } } else { size += aFile.length(); } } return size; } /** * Deletes a directory and all its children. * * @param aDir A directory to delete * @return True if file was successfully deleted; else, false */ public static boolean delete(final File aDir) { if (aDir.exists() && aDir.listFiles() != null) { for (final File file : aDir.listFiles()) { if (file.isDirectory()) { if (!delete(file)) { if (LOGGER.isErrorEnabled()) { LOGGER.error("Unable to delete: " + file); } } } else { if (!file.delete()) { if (LOGGER.isErrorEnabled()) { LOGGER.error("Unable to delete: " + file); } } } } } else if (LOGGER.isDebugEnabled() && aDir.listFiles() == null) { LOGGER.debug("Trying to delete '{}' but there was a problem", aDir); } return aDir.delete(); } /** * Copies a file or directory from one place to another. This copies a file to a file or a directory to a directory. * It does not copy a file to a directory. * * @param aFromFile A file or directory source * @param aToFile A file or directory destination * @throws IOException If there is an exception copying the files or directories */ public static void copy(final File aFromFile, final File aToFile) throws IOException { if (aFromFile.isDirectory() && aToFile.isFile() || aFromFile.isFile() && aToFile.isDirectory()) { throw new IOException("Can't copy file to directory or directory to file"); } if (aFromFile.isDirectory()) { if (!aToFile.exists() && !aToFile.mkdirs()) { throw new RuntimeException("Unable to create new directory: " + aToFile.getAbsolutePath()); } for (final File file : aFromFile.listFiles()) { copy(file, new File(aToFile, file.getName())); } } else { copyFile(aFromFile, aToFile); } } /** * Returns a human readable size from a large number of bytes. * * @param aByteCount A large number of bytes * @return A human readable size */ public static String sizeFromBytes(final long aByteCount) { return sizeFromBytes(aByteCount, false); } /** * Returns a human readable size from a large number of bytes. You can specify that the human readable size use an * abbreviated label (e.g., GB or MB). * * @param aByteCount A large number of bytes * @param aAbbreviatedLabel Whether the label should be abbreviated * @return A human readable size */ public static String sizeFromBytes(final long aByteCount, final boolean aAbbreviatedLabel) { long count; if ((count = aByteCount / 1073741824) > 0) { return count + (aAbbreviatedLabel ? " GB" : " gigabytes"); } else if ((count = aByteCount / 1048576) > 0) { return count + (aAbbreviatedLabel ? " MB" : " megabytes"); } else if ((count = aByteCount / 1024) > 0) { return count + (aAbbreviatedLabel ? " KB" : " kilobytes"); } return count + (aAbbreviatedLabel ? " B" : " bytes"); } /** * Get a hash for a supplied file. * * @param aFile A file to get a hash for * @param aAlgorithm A hash algorithm supported by <code>MessageDigest</code> * @return A hash string * @throws NoSuchAlgorithmException If the supplied algorithm isn't supported * @throws IOException If there is trouble reading the file */ public static String hash(final File aFile, final String aAlgorithm) throws NoSuchAlgorithmException, IOException { final MessageDigest md = MessageDigest.getInstance(aAlgorithm); final FileInputStream inStream = new FileInputStream(aFile); final DigestInputStream mdStream = new DigestInputStream(inStream, md); final byte[] bytes = new byte[8192]; int bytesRead = 0; while (bytesRead != -1) { bytesRead = mdStream.read(bytes); } final Formatter formatter = new Formatter(); for (final byte bite : md.digest()) { formatter.format("%02x", bite); } IOUtils.closeQuietly(mdStream); final String hash = formatter.toString(); formatter.close(); return hash; } /** * Gets the MIME type of the supplied file URL. It gets MIME type from {@link URLConnection}'s file name map. * * @param aFileUrl A file-based URL * @return The MIME-type name for the supplied file * @throws IOException If there is trouble reading the MIME type */ public static String getMimeType(final String aFileUrl) throws IOException { return URLConnection.getFileNameMap().getContentTypeFor(aFileUrl); } /** * Tests that the supplied directory exists and can be used according to the supplied permissions string (e.g., * 'rwx'). * * @param aDirName A name of a directory on the file system * @param aPermString A string representing the desired permissions of the directory * @return True if the directory is okay to be used; else, false */ public static boolean dirIsUseable(final String aDirName, final String aPermString) { final File dir = new File(aDirName); // NullPointerException if null if (!dir.exists() && !dir.mkdirs()) { return false; } // NullPointerException if aPermString is null if (aPermString.contains("r") && !dir.canRead()) { return false; } if (aPermString.contains("w") && !dir.canWrite()) { return false; } if (aPermString.contains("x") && !dir.canExecute()) { return false; } return true; } /** * Converts an file permissions mode integer to a PosixFilePermission set. * * @param aMode An integer permissions mode. * @return A PosixFilePermission set */ public static Set<PosixFilePermission> convertToPermissionsSet(final int aMode) { final Set<PosixFilePermission> result = EnumSet.noneOf(PosixFilePermission.class); if (isSet(aMode, 0400)) { result.add(OWNER_READ); } if (isSet(aMode, 0200)) { result.add(OWNER_WRITE); } if (isSet(aMode, 0100)) { result.add(OWNER_EXECUTE); } if (isSet(aMode, 040)) { result.add(GROUP_READ); } if (isSet(aMode, 020)) { result.add(GROUP_WRITE); } if (isSet(aMode, 010)) { result.add(GROUP_EXECUTE); } if (isSet(aMode, 04)) { result.add(OTHERS_READ); } if (isSet(aMode, 02)) { result.add(OTHERS_WRITE); } if (isSet(aMode, 01)) { result.add(OTHERS_EXECUTE); } return result; } /** * Convert a PosixFilePermission set to an integer permissions mode. * * @param aPermSet A PosixFilePermission set * @return A permissions mode integer */ public static int convertToInt(final Set<PosixFilePermission> aPermSet) { int result = 0; if (aPermSet.contains(OWNER_READ)) { result = result | 0400; } if (aPermSet.contains(OWNER_WRITE)) { result = result | 0200; } if (aPermSet.contains(OWNER_EXECUTE)) { result = result | 0100; } if (aPermSet.contains(GROUP_READ)) { result = result | 040; } if (aPermSet.contains(GROUP_WRITE)) { result = result | 020; } if (aPermSet.contains(GROUP_EXECUTE)) { result = result | 010; } if (aPermSet.contains(OTHERS_READ)) { result = result | 04; } if (aPermSet.contains(OTHERS_WRITE)) { result = result | 02; } if (aPermSet.contains(OTHERS_EXECUTE)) { result = result | 01; } return result; } private static boolean isSet(final int mode, final int testbit) { return (mode & testbit) == testbit; } /** * Copies a non-directory file from one location to another. * * @param aSourceFile A file to copy * @param aDestFile A destination location for the copied file * @return True if the copy was successful; else, false * @throws IOException If there is a problem copying the file */ private static boolean copyFile(final File aSourceFile, final File aDestFile) throws IOException { FileOutputStream outputStream = null; FileInputStream inputStream = null; boolean success = true; // destructive copy if (aDestFile.exists() && !aDestFile.delete()) { success = false; } if (success && !aDestFile.createNewFile()) { if (LOGGER.isWarnEnabled()) { LOGGER.warn("Failed to create: " + aDestFile.getAbsolutePath()); } success = false; } if (success) { try { outputStream = new FileOutputStream(aDestFile); inputStream = new FileInputStream(aSourceFile); final FileChannel source = inputStream.getChannel(); outputStream.getChannel().transferFrom(source, 0, source.size()); } finally { IOUtils.closeQuietly(outputStream); IOUtils.closeQuietly(inputStream); } } if (success && aDestFile.exists() && aSourceFile.canRead()) { success = aDestFile.setReadable(true, true); } if (success && aDestFile.exists() && aSourceFile.canWrite()) { success = aDestFile.setWritable(true, true); } if (success && aDestFile.exists() && aSourceFile.canExecute()) { success = aDestFile.setExecutable(true, true); } if (!success && !aDestFile.delete() && LOGGER.isWarnEnabled()) { LOGGER.warn("Unable to delete: " + aDestFile.getAbsolutePath()); } return success; } private static Element add(final File aFile, final Element aParent, final RegexFileFilter aFilter, final boolean aDeepAdd) throws FileNotFoundException { final Element element; final String tagName; if (aFile.isDirectory()) { tagName = DIR_TYPE; } else { tagName = FILE_TYPE; } if (aParent == null) { try { final DocumentBuilderFactory f = DocumentBuilderFactory.newInstance(); final Document doc = f.newDocumentBuilder().newDocument(); element = doc.createElement(tagName); } catch (final ParserConfigurationException details) { throw new RuntimeException(details); } } else { element = aParent.getOwnerDocument().createElement(tagName); aParent.appendChild(element); } copyMetadata(aFile, element); if (aFile.isDirectory()) { if (aDeepAdd) { final File[] dirs = listFiles(aFile, new RegexDirFilter(".*")); Arrays.sort(dirs); // Consistency to make testing easier for (final File dir : dirs) { element.appendChild(add(dir, element, aFilter, aDeepAdd)); } } else if (aFilter.toString().equals(".*")) { final Document doc = element.getOwnerDocument(); final File[] dirs = listFiles(aFile, new RegexDirFilter(".*")); Arrays.sort(dirs); // Consistency to make testing easier for (final File dir : dirs) { final Element dirElem = doc.createElement(DIR_TYPE); element.appendChild(dirElem); copyMetadata(dir, dirElem); } } final File[] files = listFiles(aFile, aFilter); // Provide some consistency in what's returned to make testing easier Arrays.sort(files); for (final File file : files) { element.appendChild(add(file, element, aFilter, aDeepAdd)); } } return element; } /** * Copy file metadata into the supplied file element. * * @param aFile A file to extract metadata from * @param aElement A destination element for the file metadata */ private static void copyMetadata(final File aFile, final Element aElement) { final SimpleDateFormat formatter = new SimpleDateFormat(DATE_FORMAT); final StringBuilder permissions = new StringBuilder(); final Date date = new Date(aFile.lastModified()); aElement.setAttribute(FILE_PATH, aFile.getAbsolutePath()); aElement.setAttribute(MODIFIED, formatter.format(date)); if (aFile.canRead()) { permissions.append('r'); } if (aFile.canWrite()) { permissions.append('w'); } aElement.setAttribute("permissions", permissions.toString()); } }