/*
* #%L
* Nazgul Project: nazgul-core-quickstart-api
* %%
* Copyright (C) 2010 - 2017 jGuru Europe AB
* %%
* Licensed under the jGuru Europe AB license (the "License"), based
* on Apache License, Version 2.0; you may not use this file except
* in compliance with the License.
*
* You may obtain a copy of the License at
*
* http://www.jguru.se/licenses/jguruCorporateSourceLicense-2.0.txt
*
* 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.
* #L%
*
*/
package se.jguru.nazgul.core.quickstart.api;
import org.apache.commons.lang3.Validate;
import org.apache.maven.model.Model;
import org.apache.maven.model.io.xpp3.MavenXpp3Reader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import se.jguru.nazgul.core.quickstart.model.SimpleArtifact;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.SortedMap;
import java.util.TreeMap;
/**
* A suite of utility algorithms for use with Files and related structures.
*
* @author <a href="mailto:lj@jguru.se">Lennart Jörelid</a>, jGuru Europe AB
*/
public final class FileUtils {
// Our log
private static final Logger log = LoggerFactory.getLogger(FileUtils.class.getName());
/**
* The line ending string.
*/
public static final String LINE_ENDING = System.getProperty("line.separator");
/**
* The file separator ("/" on unix, "\" on windows).
*/
public static final String FILE_SEPARATOR = System.getProperty("file.separator");
/**
* A FileFilter which accepts directories only.
*/
public static final FileFilter DIRECTORY_FILTER = new FileFilter() {
@Override
public boolean accept(final File pathname) {
System.getProperties();
return pathname.exists() && pathname.isDirectory();
}
};
/**
* A FileFilter which accepts files only.
*/
public static final FileFilter FILE_FILTER = new FileFilter() {
@Override
public boolean accept(final File pathname) {
return pathname.exists() && pathname.isFile();
}
};
/**
* A FileFilter which accepts Maven module directories only (i.e. directories containing a 'pom.xml' file).
*/
public static final FileFilter MODULE_NAME_FILTER = new FileFilter() {
@Override
public boolean accept(final File moduleCandidate) {
if (moduleCandidate.exists() && moduleCandidate.isDirectory()) {
return FILE_FILTER.accept(new File(moduleCandidate, "pom.xml"));
}
// Not a module directory/name.
return false;
}
};
/**
* A FileFilter which accepts nonexistent or empty directories.
* For this FileFilter, an "empty" directory implies that no directories or files are present within a successful
* candidate. All files except those whose names starts with "." are considered files.
*/
public static final FileFilter NONEXISTENT_OR_EMPTY_DIRECTORY_FILTER = new FileFilter() {
@Override
public boolean accept(final File candidate) {
boolean okCandidate = !candidate.exists();
if (!okCandidate && DIRECTORY_FILTER.accept(candidate)) {
final File[] childFiles = candidate.listFiles();
if (childFiles != null && childFiles.length != 0) {
for (File current : childFiles) {
if (!current.getName().startsWith(".")) {
return false;
}
}
}
// All seems well.
okCandidate = true;
}
// All done.
return okCandidate;
}
};
/**
* A list containing file suffixes of files normally containing character data, implying that the
* content of such files can normally be manipulated using TokenParsers.
*/
public static final List<String> CHARDATA_FILE_SUFFIXES = Arrays.asList("txt", "text", "xml", "xsd", "properties",
"apt", "log", "md", "readme", "csv", "tab", "odt", "1st", "java", "jsp", "js", "jsf", "c", "cpp", "html",
"css", "js", "less", "scss");
/**
* A FileFilter which identifies file types normally holding character data content.
*/
public static final FileFilter CHARACTER_DATAFILE_FILTER = new FileFilter() {
/**
* Accepts aFile if it is a file whose suffix is found in the CHARDATA_FILE_SUFFIXES list.
*
* @param aFile The non-null File to check.
* @return {@code true} if the supplied aFile is a file whose suffix is found in
* the {@code CHARDATA_FILE_SUFFIXES} list.
*/
@Override
public boolean accept(final File aFile) {
if (FILE_FILTER.accept(aFile)) {
// Find the file suffix to compare with.
final String fileName = aFile.getName();
final int suffixDelimiterIndex = fileName.lastIndexOf(".");
final String fileSuffix = "" + (suffixDelimiterIndex == -1
? fileName
: fileName.substring(suffixDelimiterIndex + 1)).trim().toLowerCase();
// All done.
return CHARDATA_FILE_SUFFIXES.contains(fileSuffix);
}
// not recognized as a CharacterData-based file.
return false;
}
};
// Internal state
private static MavenXpp3Reader pomReader = new MavenXpp3Reader();
/*
* Hide constructor for utility classes.
*/
private FileUtils() {
// Do nothing
}
/**
* Gets the canonical path of the supplied (non-null) File.
*
* @param fileOrDirectory A non-null File object.
* @return The canonical path to the supplied fileOrDirectory.
*/
public static String getCanonicalPath(final File fileOrDirectory) {
// Check sanity
Validate.notNull(fileOrDirectory, "Cannot handle null or empty fileOrDirectory argument.");
try {
return fileOrDirectory.getCanonicalPath();
} catch (IOException e) {
throw new IllegalArgumentException("Could not acquire canonical path for [" + fileOrDirectory + "]", e);
}
}
/**
* Creates all intermediary directories from the rootDirectory to the leafDirectory
* given by the relativePath supplied.
*
* @param rootDirectory The existent or nonexistent rootDirectory.
* @param relativePath The relative path of directories to make.
* @return The directory just made.
* @throws java.lang.IllegalArgumentException if the leafDirectory (or any of its intermediary directories)
* could not be created.
*/
public static File makeDirectory(final File rootDirectory, final String relativePath)
throws IllegalArgumentException {
// Check sanity
Validate.notNull(rootDirectory, "Cannot handle null rootDirectory argument.");
Validate.notNull(relativePath, "Cannot handle null relativePath argument.");
final File toReturn = relativePath.isEmpty() ? rootDirectory : new File(rootDirectory, relativePath);
Validate.isTrue(!toReturn.exists() || (toReturn.exists() && toReturn.isDirectory()),
"Insane state. [" + getCanonicalPath(toReturn) + "] exists but is not a Directory.");
// Delegate and return
if (toReturn.mkdirs()) {
return toReturn;
}
// Should we fail?
if (!exists(toReturn, true)) {
throw new IllegalArgumentException("Could not create path to [" + getCanonicalPath(toReturn)
+ "] fully. Check filesystem; state may be broken.");
}
// Seems we are OK anyways.
return toReturn;
}
/**
* Acquires a Maven Model from the supplied POM file.
*
* @param aPomFile A non-null pom.xml File.
* @return The Maven model converted from the supplied aPomFile.
*/
public static Model getPomModel(final File aPomFile) {
// Check sanity
Validate.notNull(aPomFile, "Cannot handle null aPomFile argument.");
Validate.isTrue(FILE_FILTER.accept(aPomFile), "File [" + getCanonicalPath(aPomFile)
+ "] must exist and be a File.");
try {
return pomReader.read(new FileReader(aPomFile));
} catch (Exception e) {
throw new IllegalArgumentException("Could not read POM file [" + getCanonicalPath(aPomFile) + "]", e);
}
}
/**
* Given a directory within a Maven project reactor, list the names of all subdirectories
* containing a 'pom.xml' file. These directories typically correspond to module names within a
* reactor POM found in the supplied reactorDirectory.
*
* @param reactorDirectory An existing directory.
* @return The names of all subdirectories to the provided reactorDirectory where a file called "pom.xml" exists.
* The names of these directories should typically be used as module names within a reactor pom located within
* the supplied reactorDirectory. No validation of any found pom.xml files (consistency, well-formed-ness etc.)
* are done within this method.
*/
public static List<String> getModuleNames(final File reactorDirectory) {
// Check sanity
Validate.notNull(reactorDirectory, "Cannot handle null or empty reactorDirectory argument.");
Validate.isTrue(DIRECTORY_FILTER.accept(reactorDirectory), "reactorDirectory argument ["
+ getCanonicalPath(reactorDirectory) + "] must refer to an existing directory.");
final List<String> toReturn = new ArrayList<>();
for (File current : reactorDirectory.listFiles(MODULE_NAME_FILTER)) {
toReturn.add(current.getName());
}
// All done.
return toReturn;
}
/**
* Extracts a SimpleArtifact from a Maven model.
*
* @param aModel The Maven Model from which to extract the SimpleArtifact data.
* @return A SimpleArtifact wrapping the data found in the supplied aModel.
*/
public static SimpleArtifact getSimpleArtifact(final Model aModel) {
// Check sanity
Validate.notNull(aModel, "Cannot handle null aModel argument.");
String version = aModel.getVersion();
if (version == null) {
Validate.notNull(aModel.getParent(), "A Model requires either a version or a non-null Parent definition.");
version = aModel.getParent().getVersion();
}
// All done.
return new SimpleArtifact(aModel.getGroupId(), aModel.getArtifactId(), version);
}
/**
* Identifies if the supplied fileOrDir exists.
*
* @param fileOrDir A non-null File.
* @param isDirectory if {@code true}, the fileOrDir is assumed to be a directory - and otherwise a file.
* @return {@code true} if the supplied fileOrDir exists and is of the type indicated by {@code isDirectory}.
*/
public static boolean exists(final File fileOrDir, final boolean isDirectory) {
Validate.notNull(fileOrDir, "Cannot handle null fileOrDir argument.");
return isDirectory ? DIRECTORY_FILTER.accept(fileOrDir) : FILE_FILTER.accept(fileOrDir);
}
/**
* Writes the supplied data to the given File.
*
* @param aFile The non-null File to write data to.
* @param data The non-null data string to write.
*/
public static void writeFile(final File aFile, final String data) {
// Check sanity
Validate.notNull(aFile, "Cannot handle null aFile argument.");
Validate.notNull(data, "Cannot handle null data argument.");
final File dirForFile = aFile.getParentFile();
if (dirForFile == null || !(dirForFile.exists() && dirForFile.isDirectory())) {
throw new IllegalArgumentException("Cannot write file [" + FileUtils.getCanonicalPath(aFile)
+ "], since its parent is not an existing directory.");
}
// All seems sane. Write the file.
try (BufferedWriter writer = new BufferedWriter(new FileWriter(aFile))) {
writer.write(data);
writer.flush();
} catch (IOException e) {
log.warn("Could not write data to [" + getCanonicalPath(aFile) + "]", e);
}
}
/**
* Reads all (text) data from the supplied File, returning it as a String.
* All line feeds are converted to {@code System.getProperty("line.separator")}.
*
* @param aFile The non-null File to read data from.
* @return The content of the supplied File.
*/
public static String readFile(final File aFile) {
// Check sanity
Validate.notNull(aFile, "Cannot handle null aFile argument.");
Validate.isTrue(FILE_FILTER.accept(aFile), "File [" + getCanonicalPath(aFile)
+ "] must exist and be a (text) file.");
try {
return readFully(new FileInputStream(aFile), getCanonicalPath(aFile));
} catch (FileNotFoundException e) {
// This should never happen
throw new IllegalArgumentException("Could not read file", e);
}
}
/**
* Reads all (text) data from the supplied resource URL, returning it as a String.
* All line feeds are converted to {@code System.getProperty("line.separator")}.
*
* @param resourceURL The non-empty resource URL to read data from.
* @return The content of the supplied resource URL.
*/
public static String readFile(final String resourceURL) {
// Check sanity
Validate.notEmpty(resourceURL, "Cannot handle null resourceURL argument.");
// Peel off any initial '/' chars
final String effectiveURL = resourceURL.startsWith("/") ? resourceURL.substring(1) : resourceURL;
final InputStream in = Thread.currentThread().getContextClassLoader().getResourceAsStream(effectiveURL);
if (in == null) {
throw new IllegalArgumentException("No file (or stream) found for resource [" + effectiveURL + "].");
}
// All done.
return readFully(in, resourceURL);
}
/**
* Maps the relative path of all files found under the supplied aDirectory to the files themselves.
* Note that this operation consumes considerable amounts of resources (memory buffers) when mapping large file
* trees. Therefore, it is wise to confine the aDirectory to smaller file trees.
*
* @param aDirectory A directory under which all Files are mapped.
* @return A non-null SortedMap relating the relative paths of all Files found under the supplied aDirectory
* to the Files themselves.
*/
public static SortedMap<String, File> listFilesRecursively(final File aDirectory) {
// Check sanity
Validate.notNull(aDirectory, "Cannot handle null aDirectory argument.");
Validate.isTrue(DIRECTORY_FILTER.accept(aDirectory),
"Argument aDirectory must point to an existing directory. Got [" + getCanonicalPath(aDirectory) + "]");
final SortedMap<String, File> toReturn = new TreeMap<>();
populate(toReturn, aDirectory, aDirectory);
// All done.
return toReturn;
}
//
// Private helpers
//
/**
* Reads all data from the supplied InputStream, assumed to point to a character-based stream.
* All line feeds are converted to {@code System.getProperty("line.separator")}.
*
* @param stream The non-null stream to read fully. The stream is closed before returning from this method,
* irrespective of the results of reading the stream.
* @param desc A description of the stream. Used within an Exception message should an IOException
* occur while reading the stream data.
* @return The fully read text data.
*/
@SuppressWarnings("all")
private static String readFully(final InputStream stream, final String desc) {
final StringBuilder result = new StringBuilder();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(stream))) {
for (String aLine = reader.readLine(); aLine != null; aLine = reader.readLine()) {
result.append(aLine).append(LINE_ENDING);
}
} catch (IOException e) {
throw new IllegalArgumentException("Could not read data from [" + desc + "]", e);
} finally {
try {
// Close the original stream.
stream.close();
} catch (IOException e) {
throw new IllegalArgumentException("Could not close original stream for [" + desc + "]", e);
}
}
// All done.
return result.toString();
}
/**
* Populates the provided SortedMap with the relative path of each File found under the rootDirectory,
*
* @param toPopulate The SortedMap to populate.
* @param currentDirectory The current directory.
* @param rootDirectory The root directory of the structure to populate, used to calculate the relative
* path of the files found within the currentDirectory (i.e. the keys within the
* toPopulate SortedMap).
*/
private static void populate(final SortedMap<String, File> toPopulate,
final File currentDirectory,
final File rootDirectory) {
// Calculate the relative path
final String rootDirPath = getCanonicalPath(rootDirectory);
final String currentDirPath = getCanonicalPath(currentDirectory);
final String tmp = currentDirPath.substring(currentDirPath.indexOf(rootDirPath)
+ rootDirPath.length()
+ (rootDirPath.equals(currentDirPath) ? 0 : 1));
final String prefix = tmp.length() == 0 ? "" : tmp + "/";
// Map all files in the current directory
for (File current : currentDirectory.listFiles(FILE_FILTER)) {
final File existingFile = toPopulate.put(prefix + current.getName(), current);
Validate.isTrue(existingFile == null, "Already mapped file at path [" + getCanonicalPath(current) + "]");
}
// Recurse
for (File current : currentDirectory.listFiles(DIRECTORY_FILTER)) {
populate(toPopulate, current, rootDirectory);
}
}
}