/* * The MIT License (MIT) * * Copyright (c) 2014-2015 Sri Harsha Chilakapati * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ package com.shc.utils; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.Reader; import java.io.Writer; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.nio.file.FileSystem; import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.jar.JarEntry; import java.util.jar.JarFile; import java.util.stream.Collectors; /** * <p> A FilePath is handle to a file that can be both an external file besides the JAR, or has an absolute path from * the root of the filesystem or is a resource packed in some JAR in the classpath. This class uses some tricks to find * the other properties of the resources like is that a directory or a file, it's size and also whether it exists. To * construct a FilePath, use either {@link #getExternalFile(String)} or {@link #getResourceFile(String)} methods.</p> * * <pre> * FilePath resource = FilePath.getResourceFile("resources/test.png"); * FilePath external = FilePath.getExternalFile("C:/Windows/explorer.exe"); * </pre> * * <p> Once you create a FilePath, you can derive new files if that is a directory, or you can get the parent path of a * path. You can also get a list of files in a directory denoted by a FilePath instance. To read from a file, or to * write to a file, you can get an {@link InputStream} or an {@link OutputStream} by using {@link #getInputStream()} and * {@link #getOutputStream()} methods.</p> * * <p> For external files, this class uses the {@link Files} class of Java NIO package. For resources, there are two * ways. If the resource is accessed from an IDE, it uses a {@link File} instance with the URI returned by the class * loader's {@link ClassLoader#getResource(String)} method. If the resource is accessed from a running JAR, then an * instance of {@link JarFile} is created using reflection, and it's entries are used to access the attributes.</p> * * @author Sri Harsha Chilakapati */ public class FilePath { /** * The UNIX style path separator ({@code /}) character. All the path strings are converted to this character when * creating an instance with the constructor. */ public static final char SEPARATOR = '/'; // The path of the resource, and the type private String path; private Type type; /** * Constructs an instance of FilePath by taking a path string, and a type. * * @param path The path string of the path * @param type The type of the file, one of {@link Type#EXTERNAL} or {@link Type#RESOURCE}. */ private FilePath(String path, Type type) { this.path = path.replaceAll("\\\\", "" + SEPARATOR).replaceAll("/+", "" + SEPARATOR).trim(); this.type = type; if (type == Type.RESOURCE && this.path.startsWith("/")) this.path = this.path.replaceFirst("/", ""); } /** * This method creates a {@link FilePath} instance that handles a path to an external file or directory. The path * can be either an absolute path, or a relative path. In case of the relative path, the files are expected to be in * project root folder when running through an IDE, and besides the JAR file when running from an executable JAR. * * @param path The path string that specifies the location of the file or directory. * * @return The FilePath instance that can be used to handle an external file. */ public static FilePath getExternalFile(String path) { return new FilePath(path, Type.EXTERNAL); } /** * This method creates a {@link FilePath} instance that handles a path to an internal file or directory, which is * present in the classpath. This path is always absolute, and starts with the root of classpath, which will be the * source directory in case of running from the IDE, or from the root of the JAR file when running from executable * JAR file. * * @param path The path string that specifies the location of the file or directory. * * @return The FilePath instance that can be used to handle a resource file, which will be packed into the JAR. */ public static FilePath getResourceFile(String path) { return new FilePath(path, Type.RESOURCE); } /** * Gets the path string of this FilePath instance. This path will be the same as the one that you used to create * this object, except that it will be changed to use the separator defined by {@link #SEPARATOR} which works in all * the platforms. * * @return The path string of this FilePath instance. */ public String getPath() { if (path.trim().endsWith("" + SEPARATOR)) return path.trim().substring(0, path.lastIndexOf(SEPARATOR)); return path; } /** * Gets the absolute path string of this FilePath instance. The root of the path will be the root of the filesystem * if this path is an external file, or the root of the classpath if this is a JAR file resource. * * @return The absolute path string of this FilePath instance. */ public String getAbsolutePath() { if (type == Type.EXTERNAL) // For external files, use the File object to get the absolute path return new File(path).getAbsolutePath().replaceAll("\\\\", "" + SEPARATOR) .replaceAll("/+", "" + SEPARATOR).trim(); else // For resource files, the path is always absolute, just return it return path; } /** * Gets the type of file this FilePath instance resolves to. * * @return One of {@link Type#EXTERNAL} or {@link Type#RESOURCE}. */ public Type getType() { return type; } /** * Checks if the file resolved to from this FilePath instance actually exists, or whether it is a hypothetical one. * * @return True if the file exists, or else False. */ public boolean exists() { if (getType() == Type.EXTERNAL) return Files.exists(Paths.get(path)); else try { return existsInJar() || FilePath.class.getClassLoader().getResource(path) != null; } catch (IOException e) { throw new RuntimeException(e.getCause()); } } /** * This method finds whether this path exists in the executable JAR file. * * @return True if the file exists, else false. * * @throws IOException If an error occurs when accessing the JAR file. */ private boolean existsInJar() throws IOException { boolean exists = false; // Get the code source as a file File jarFile = new File(FilePath.class.getProtectionDomain().getCodeSource().getLocation().getPath().replaceAll("%20", " ")); if (jarFile.isFile()) { // If this is a file, we are sure that we are running from a JAR file. JarFile jar = new JarFile(jarFile); exists = jar.stream().filter(e -> e.getName().startsWith(path)).count() > 0; jar.close(); } return exists; } /** * Checks if this FilePath is a file, or a directory. * * @return True if a file, else false. */ public boolean isFile() { return exists() && !isDirectory(); } /** * Checks if this FilePath is a directory, or a file. * * @return True if a directory, else false. */ public boolean isDirectory() { if (!exists()) return false; if (getType() == Type.EXTERNAL) return Files.isDirectory(Paths.get(path)); else { boolean isDirectory = false; try { // Try to get the code source as a file File file = new File(FilePath.class.getProtectionDomain().getCodeSource().getLocation().getPath().replaceAll("%20", " ")); if (file.isFile()) { // If the source is a JAR file, then this is a directory if it has more entries that starts with // the path of this resource. JarFile jarFile = new JarFile(file); isDirectory = jarFile.stream().filter(e -> e.getName().startsWith(path)).count() > 1; jarFile.close(); } else { // Now that we know that our code base is a directory, we should be running from the IDE URL url = FilePath.class.getClassLoader().getResource(path); if (url == null) { // A guess, if the url is not found, search the code source for this path (Shouldn't be run) isDirectory = new File(file, path).isDirectory(); } else { final Map<String, String> env = new HashMap<>(); final String[] array = url.toURI().toString().split("!"); Path path; if (array[0].startsWith("jar") || array[0].startsWith("zip")) { // The requested file is from a JAR file in the classpath, so create a // new FileSystem to resolve it. final FileSystem fs = FileSystems.newFileSystem(URI.create(array[0]), env); path = fs.getPath(array[1]); isDirectory = Files.isDirectory(path); fs.close(); } else { // The requested file is from the directory of the project path = Paths.get(url.toURI()); isDirectory = Files.isDirectory(path); } } } } catch (Exception e) { throw new RuntimeException(e.getCause()); } return isDirectory; } } /** * Gets an {@link InputStream} that can be used to read data from this file path. * * @return An input stream that allows you to read from this FilePath. * * @throws IOException If an I/O error occurs while creating an input stream. */ public InputStream getInputStream() throws IOException { if (isDirectory()) throw new RuntimeException("Cannot read from a directory."); if (!exists()) throw new RuntimeException("Cannot read from a non existing file."); InputStream inputStream = null; switch (type) { case EXTERNAL: inputStream = Files.newInputStream(Paths.get(path)); break; case RESOURCE: inputStream = FilePath.class.getClassLoader().getResourceAsStream(path); } return inputStream; } /** * Gets an {@link OutputStream} that can be used to write data to this file path. Note that data can only be written * to external file paths, resources will generate a RuntimeException. * * @return An output stream that allows you to write into this FilePath. * * @throws IOException If an I/O error occurs. * @throws RuntimeException If the path is a directory, or if the path is a resource. */ public OutputStream getOutputStream() throws IOException { if (type == Type.RESOURCE) throw new RuntimeException("Cannot write to a resource file."); if (isDirectory()) throw new RuntimeException("Cannot write to a directory."); return Files.newOutputStream(Paths.get(path)); } /** * Gets a {@link Reader} that can be used to read the data from this file path. * * @return An InputStreamReader instance that reads from the input stream of this FilePath. * * @throws IOException If an I/O error occurs. * @throws RuntimeException If the path is a directory, or if the path does not exist. */ public Reader getReader() throws IOException { return new InputStreamReader(getInputStream()); } /** * Gets a {@link Writer} that can be used to write data into this file path. * * @return An OutputStreamWriter instance that writes data into the output stream of this FilePath. * * @throws IOException If an I/O error occurs. * @throws RuntimeException If the path is a directory, or if the path does not exist. */ public Writer getWriter() throws IOException { return new OutputStreamWriter(getOutputStream()); } /** * Copies the contents of this FilePath into another FilePath replacing the destination contents. * * @param path The destination FilePath where to copy the contents of this FilePath. * * @throws IOException If an I/O error occurs. * @throws RuntimeException If either this or the destination are directories, or if this path doesn't exist. */ public void copyTo(FilePath path) throws IOException { boolean thisIsDirectory = this.isDirectory(); boolean pathIsDirectory = path.isDirectory(); if (thisIsDirectory && !pathIsDirectory) throw new RuntimeException("Cannot copy a directory into a file."); if (!thisIsDirectory && pathIsDirectory) throw new RuntimeException("Cannot copy a file into a directory."); if (!exists()) throw new RuntimeException("Cannot copy a non existing file."); byte[] buffer = new byte[1024]; int length; try (InputStream inputStream = getInputStream(); OutputStream outputStream = path.getOutputStream()) { while ((length = inputStream.read(buffer)) > 0) { outputStream.write(buffer, 0, length); } inputStream.close(); outputStream.close(); } } /** * Moves this path to another path, overwriting the destination if something exists there already. * * @param path The destination path to move the contents of this path into. * * @throws IOException If an I/O error occurs. * @throws RuntimeException If either the source or the destinations are resources. */ public void moveTo(FilePath path) throws IOException { if (getType() == Type.RESOURCE || path.getType() == Type.RESOURCE) throw new RuntimeException("Cannot move resource files!"); Files.move(Paths.get(this.path), Paths.get(path.getPath())); // Change this path this.path = path.path; } /** * Creates the directories represented by this path if any does not exist. * * @throws IOException If an I/O error occurs. * @throws RuntimeException If this path is a resource. */ public void mkdirs() throws IOException { if (getType() == Type.RESOURCE) throw new RuntimeException("Cannot create resource directories!"); if (isFile() && !exists()) getParent().mkdirs(); else Files.createDirectories(Paths.get(path)); } /** * Creates an empty file at the path resolved by this FilePath instance. * * @throws IOException If an I/O error occurs. * @throws RuntimeException If this path is a resource, or is a directory. */ public void createFile() throws IOException { if (getType() == Type.RESOURCE) throw new RuntimeException("Cannot create resource files!"); if (isDirectory()) throw new RuntimeException("Cannot convert a directory to a file"); Files.createFile(Paths.get(path)); } /** * Gets a new FilePath that represents the parent directory of this FilePath. * * @return The parent directory of this FilePath. */ public FilePath getParent() { String[] parts = path.split("" + SEPARATOR); String path = parts[0]; for (int i = 1; i < parts.length - 1; i++) path += SEPARATOR + parts[i] + SEPARATOR; return new FilePath(path + SEPARATOR, type); } /** * Gets a new FilePath that represents a file that is a child of this path. * * @param path The path of the child, relative to this path. * * @return The FilePath instance of the child. * * @throws RuntimeException If this path is not a directory, or if it does not exist. */ public FilePath getChild(String path) { if (!isDirectory()) throw new RuntimeException("Cannot get a child for a file."); if (!exists()) throw new RuntimeException("Cannot get a child for a non existing directory."); return new FilePath(this.path + SEPARATOR + path, type); } /** * Deletes the file resolved by this FilePath instance. * * @return True if the attempt is successful or false otherwise. * * @throws IOException If an I/O error occurs. * @throws RuntimeException If this file is a resource, or if this doesn't exist. */ public boolean delete() throws IOException { if (getType() == Type.RESOURCE) throw new RuntimeException("Cannot delete resource files."); if (!exists()) throw new RuntimeException("Cannot delete non existing file."); return Files.deleteIfExists(Paths.get(path)); } /** * Marks this FilePath to be deleted on JVM exit. * * @throws RuntimeException If this path is a resource. */ public void deleteOnExit() { if (getType() == Type.RESOURCE) throw new RuntimeException("Cannot delete resource files."); new File(path).deleteOnExit(); } /** * Returns the extension of the file represented by this FilePath. * * @return The extension of the file without the leading dot. */ public String getExtension() { String[] parts = getPath().split("\\.(?=[^\\.]+$)"); return parts.length > 1 ? parts[1] : ""; } /** * Returns the file name of the file represented by this FilePath. * * @return The filename of this FilePath, along with the extension. */ public String getName() { String path = this.path; if (path.endsWith("" + SEPARATOR)) path = path.substring(0, path.length() - 1); return path.substring(path.lastIndexOf(SEPARATOR) + 1); } /** * Returns the file name of this file represented by this FilePath without it's extension. * * @return The filename of this FilePath, without the extension. */ public String getNameWithoutExtension() { return getName().replaceAll("\\." + getExtension(), ""); } /** * Returns the file size of this path in number of bytes. In case of a directory, the size will be the sum of the * sizes of all it's children. If this file path does not exist, a value of {@code -1} is returned. * * @return The size of the file in bytes. */ public long sizeInBytes() { if (!exists()) return -1; try { if (getType() == Type.EXTERNAL) return Files.size(Paths.get(path)); else return calculateResourceSize(); } catch (IOException e) { throw new RuntimeException(e.getCause()); } } /** * This method calculates the size of the resource file or the directory. If this path is a directory, then the size * is the sum of all the files in this directory. Otherwise the size is calculated either by retrieving the JarEntry * of the resource, or by delegating it to the Files class with a path, in case of running from the IDE. * * @return The size of the FilePath, in bytes. * * @throws IOException If an error occurred while accessing the file path. */ private long calculateResourceSize() throws IOException { long size = 0; if (isDirectory()) { // If this path is a directory, then the size is the sum of all the files in it. List<FilePath> files = listFiles(); for (FilePath path : files) size += path.sizeInBytes(); } else { // Otherwise, try to find the location of the executable JAR File jarFile = new File(FilePath.class.getProtectionDomain().getCodeSource().getLocation().getPath().replaceAll("%20", " ")); // If this is a JAR file, we are running through an executable JAR. Construct a JarFile instance // and use that instance to read the entries from the JAR. if (jarFile.isFile()) { JarFile jar = new JarFile(jarFile); // Collect all the entries whose name equals with the path List<JarEntry> entries = jar.stream().filter(e -> e.getName().equals(path)) .collect(Collectors.toList()); size = entries.get(0).getSize(); jar.close(); } else { try { // This is a resource, but the file is requested when running from an IDE. We can safely delegate // the work to the Files class now. URL url = FilePath.class.getClassLoader().getResource(path); if (url != null) size = Files.size(Paths.get(url.toURI())); } catch (URISyntaxException e) { throw new RuntimeException(e.getCause()); } } } return size; } /** * Returns a list of FilePaths for the children of this directory. * * @return An un-modifiable list of FilePaths for all the children of this directory. * * @throws IOException If an I/O error occurs. * @throws RuntimeException If this is not a directory, of if this doesn't exist. */ public List<FilePath> listFiles() throws IOException { if (!isDirectory()) throw new RuntimeException("Cannot list files in a path which is not a directory."); if (!exists()) throw new RuntimeException("Cannot list files in a non existing directory."); List<FilePath> list = new ArrayList<>(); if (getType() == Type.EXTERNAL) { File file = new File(path); File[] children = file.listFiles(); if (children != null) for (File child : children) list.add(new FilePath(path + SEPARATOR + child.getPath().replace(file.getPath(), ""), getType())); } else { File file = new File(FilePath.class.getProtectionDomain().getCodeSource().getLocation().getPath().replaceAll("%20", " ")); if (file.isFile()) { JarFile jarFile = new JarFile(file); jarFile.stream().filter(e -> e.getName().startsWith(path)).forEach(p -> list.add(new FilePath(p.getName(), getType()))); jarFile.close(); } else { try { URL url = FilePath.class.getClassLoader().getResource(path); if (url != null) { Files.list(Paths.get(url.toURI())).forEach(p -> list.add(new FilePath(path + SEPARATOR + p.toFile().getName(), getType()))); } } catch (URISyntaxException e) { throw new RuntimeException(e.getCause()); } } } return Collections.unmodifiableList(list); } @Override public int hashCode() { int result = getPath().hashCode(); result = 31 * result + getType().hashCode(); result = 31 * result + (isDirectory() ? 1 : 0); result = 31 * result + (exists() ? 1 : 0); result = 31 * result + (int) (sizeInBytes() ^ (sizeInBytes() >>> 32)); return result; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; FilePath filePath = (FilePath) o; return isDirectory() == filePath.isDirectory() && exists() == filePath.exists() && sizeInBytes() == filePath.sizeInBytes() && getPath().equals(filePath.getPath()) && getType() == filePath.getType(); } @Override public String toString() { return "FilePath{" + "path='" + path + '\'' + ", name='" + getName() + "'" + ", extension='" + getExtension() + "'" + ", type=" + type + ", isDirectory=" + isDirectory() + ", exists=" + exists() + ", size=" + sizeInBytes() + '}'; } /** * Describes the type of the FilePath. A path can only be either {@link #EXTERNAL} or {@link #RESOURCE}. */ public enum Type { EXTERNAL, RESOURCE } }