All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.scijava.util.FileUtils Maven / Gradle / Ivy

Go to download

SciJava Common is a shared library for SciJava software. It provides a plugin framework, with an extensible mechanism for service discovery, backed by its own annotation processor, so that plugins can be loaded dynamically. It is used by downstream projects in the SciJava ecosystem, such as ImageJ and SCIFIO.

There is a newer version: 2.99.0
Show newest version
/*
 * #%L
 * SciJava Common shared library for SciJava software.
 * %%
 * Copyright (C) 2009 - 2016 Board of Regents of the University of
 * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
 * Institute of Molecular Cell Biology and Genetics.
 * %%
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * 
 * 1. Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 * #L%
 */

// File path shortening code adapted from:
// from: http://www.rgagnon.com/javadetails/java-0661.html

package org.scijava.util;

import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.net.JarURLConnection;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Useful methods for working with file paths.
 * 
 * @author Johannes Schindelin
 * @author Curtis Rueden
 * @author Grant Harris
 */
public final class FileUtils {

	public static final int DEFAULT_SHORTENER_THRESHOLD = 4;
	public static final String SHORTENER_BACKSLASH_REGEX = "\\\\";
	public static final String SHORTENER_SLASH_REGEX = "/";
	public static final String SHORTENER_BACKSLASH = "\\";
	public static final String SHORTENER_SLASH = "/";
	public static final String SHORTENER_ELLIPSE = "...";

	/** A regular expression to match filenames containing version information. */
	private static final Pattern VERSION_PATTERN = buildVersionPattern();

	private FileUtils() {
		// prevent instantiation of utility class
	}

	/**
	 * Gets the absolute path to the given file, with the directory separator
	 * standardized to forward slash, like most platforms use.
	 * 
	 * @param file The file whose path will be obtained and standardized.
	 * @return The file's standardized absolute path.
	 */
	public static String getPath(final File file) {
		final String path = file.getAbsolutePath();
		final String slash = System.getProperty("file.separator");
		return getPath(path, slash);
	}

	/**
	 * Gets a standardized path based on the given one, with the directory
	 * separator standardized from the specific separator to forward slash, like
	 * most platforms use.
	 * 
	 * @param path The path to standardize.
	 * @param separator The directory separator to be standardized.
	 * @return The standardized path.
	 */
	public static String getPath(final String path, final String separator) {
		// NB: Standardize directory separator (i.e., avoid Windows nonsense!).
		return path.replaceAll(Pattern.quote(separator), "/");
	}

	/**
	 * Extracts the file extension from a file.
	 * 
	 * @param file the file object
	 * @return the file extension (excluding the dot), or the empty string when
	 *         the file name does not contain dots
	 */
	public static String getExtension(final File file) {
		final String name = file.getName();
		final int dot = name.lastIndexOf('.');
		if (dot < 0) return "";
		return name.substring(dot + 1);
	}

	/**
	 * Extracts the file extension from a file path.
	 * 
	 * @param path the path to the file (relative or absolute)
	 * @return the file extension (excluding the dot), or the empty string when
	 *         the file name does not contain dots
	 */
	public static String getExtension(final String path) {
		return getExtension(new File(path));
	}

	/** Gets the {@link Date} of the file's last modification. */
	public static Date getModifiedTime(final File file) {
		final long modifiedTime = file.lastModified();
		final Calendar c = Calendar.getInstance();
		c.setTimeInMillis(modifiedTime);
		return c.getTime();
	}

	/**
	 * Reads the contents of the given file into a new byte array.
	 * 
	 * @see DigestUtils#string(byte[]) To convert a byte array to a string.
	 * @throws IOException If the file cannot be read.
	 */
	public static byte[] readFile(final File file) throws IOException {
		final long length = file.length();
		if (length > Integer.MAX_VALUE) {
			throw new IllegalArgumentException("File too large");
		}
		final DataInputStream dis = new DataInputStream(new FileInputStream(file));
		final byte[] bytes = new byte[(int) length];
		dis.readFully(bytes);
		dis.close();
		return bytes;
	}

	/**
	 * Writes the given byte array to the specified file.
	 * 
	 * @see DigestUtils#bytes(String) To convert a string to a byte array.
	 * @throws IOException If the file cannot be written.
	 */
	public static void writeFile(final File file, final byte[] bytes)
		throws IOException
	{
		final FileOutputStream out = new FileOutputStream(file);
		try {
			out.write(bytes);
		}
		finally {
			out.close();
		}
	}

	public static String stripFilenameVersion(final String filename) {
		final Matcher matcher = VERSION_PATTERN.matcher(filename);
		if (!matcher.matches()) return filename;
		return matcher.group(1) + matcher.group(5);
	}

	/**
	 * Lists all versions of a given (possibly versioned) file name.
	 * 
	 * @param directory the directory to scan
	 * @param filename the file name to use
	 * @return the list of matches
	 */
	public static File[] getAllVersions(final File directory,
		final String filename)
	{
		final Matcher matcher = VERSION_PATTERN.matcher(filename);
		if (!matcher.matches()) {
			final File file = new File(directory, filename);
			return file.exists() ? new File[] { file } : null;
		}
		final String baseName = matcher.group(1);
		final String classifier = matcher.group(6);
		return directory.listFiles(new FilenameFilter() {

			@Override
			public boolean accept(final File dir, final String name) {
				if (!name.startsWith(baseName)) return false;
				final Matcher matcher2 = VERSION_PATTERN.matcher(name);
				return matcher2.matches() && baseName.equals(matcher2.group(1)) &&
						equals(classifier, matcher2.group(6));
			}

			private boolean equals(final String a, final String b) {
				if (a == null) {
					return b == null;
				}
				return a.equals(b);
			}
		});
	}

	/**
	 * Converts the given {@link URL} to its corresponding {@link File}.
	 * 

* This method is similar to calling {@code new File(url.toURI())} except that * it also handles "jar:file:" URLs, returning the path to the JAR file. *

* * @param url The URL to convert. * @return A file path suitable for use with e.g. {@link FileInputStream} * @throws IllegalArgumentException if the URL does not correspond to a file. */ public static File urlToFile(final URL url) { return url == null ? null : urlToFile(url.toString()); } /** * Converts the given URL string to its corresponding {@link File}. * * @param url The URL to convert. * @return A file path suitable for use with e.g. {@link FileInputStream} * @throws IllegalArgumentException if the URL does not correspond to a file. */ public static File urlToFile(final String url) { String path = url; if (path.startsWith("jar:")) { // remove "jar:" prefix and "!/" suffix final int index = path.indexOf("!/"); path = path.substring(4, index); } try { if (PlatformUtils.isWindows() && path.matches("file:[A-Za-z]:.*")) { path = "file:/" + path.substring(5); } return new File(new URL(path).toURI()); } catch (final MalformedURLException e) { // NB: URL is not completely well-formed. } catch (final URISyntaxException e) { // NB: URL is not completely well-formed. } if (path.startsWith("file:")) { // pass through the URL as-is, minus "file:" prefix path = path.substring(5); return new File(path); } throw new IllegalArgumentException("Invalid URL: " + url); } /** * Shortens the path to a maximum of 4 path elements. * * @param path the path to the file (relative or absolute) * @return shortened path */ public static String shortenPath(final String path) { return shortenPath(path, DEFAULT_SHORTENER_THRESHOLD); } /** * Shortens the path based on the given maximum number of path elements. E.g., * "C:/1/2/test.txt" returns "C:/1/.../test.txt" if threshold is 1. * * @param path the path to the file (relative or absolute) * @param threshold the number of directories to keep unshortened * @return shortened path */ public static String shortenPath(final String path, final int threshold) { String regex = SHORTENER_BACKSLASH_REGEX; String sep = SHORTENER_BACKSLASH; if (path.indexOf("/") > 0) { regex = SHORTENER_SLASH_REGEX; sep = SHORTENER_SLASH; } String pathtemp[] = path.split(regex); // remove empty elements int elem = 0; { final String newtemp[] = new String[pathtemp.length]; int j = 0; for (int i = 0; i < pathtemp.length; i++) { if (!pathtemp[i].equals("")) { newtemp[j++] = pathtemp[i]; elem++; } } pathtemp = newtemp; } if (elem > threshold) { final StringBuilder sb = new StringBuilder(); int index = 0; // drive or protocol final int pos2dots = path.indexOf(":"); if (pos2dots > 0) { // case c:\ c:/ etc. sb.append(path.substring(0, pos2dots + 2)); index++; // case http:// ftp:// etc. if (path.indexOf(":/") > 0 && pathtemp[0].length() > 2) { sb.append(SHORTENER_SLASH); } } else { final boolean isUNC = path.substring(0, 2).equals(SHORTENER_BACKSLASH_REGEX); if (isUNC) { sb.append(SHORTENER_BACKSLASH).append(SHORTENER_BACKSLASH); } } for (; index <= threshold; index++) { sb.append(pathtemp[index]).append(sep); } if (index == (elem - 1)) { sb.append(pathtemp[elem - 1]); } else { sb.append(SHORTENER_ELLIPSE).append(sep).append(pathtemp[elem - 1]); } return sb.toString(); } return path; } /** * Compacts a path into a given number of characters. The result is similar to * the Win32 API PathCompactPathExA. * * @param path the path to the file (relative or absolute) * @param limit the number of characters to which the path should be limited * @return shortened path */ public static String limitPath(final String path, final int limit) { if (path.length() <= limit) return path; final char shortPathArray[] = new char[limit]; final char pathArray[] = path.toCharArray(); final char ellipseArray[] = SHORTENER_ELLIPSE.toCharArray(); final int pathindex = pathArray.length - 1; final int shortpathindex = limit - 1; // fill the array from the end int i = 0; for (; i < limit; i++) { if (pathArray[pathindex - i] != '/' && pathArray[pathindex - i] != '\\') { shortPathArray[shortpathindex - i] = pathArray[pathindex - i]; } else { break; } } // check how much space is left final int free = limit - i; if (free < SHORTENER_ELLIPSE.length()) { // fill the beginning with ellipse for (int j = 0; j < ellipseArray.length; j++) { shortPathArray[j] = ellipseArray[j]; } } else { // fill the beginning with path and leave room for the ellipse int j = 0; for (; j + ellipseArray.length < free; j++) { shortPathArray[j] = pathArray[j]; } // ... add the ellipse for (int k = 0; j + k < free; k++) { shortPathArray[j + k] = ellipseArray[k]; } } return new String(shortPathArray); } /** * Creates a temporary directory. *

* Since there is no atomic operation to do that, we create a temporary file, * delete it and create a directory in its place. To avoid race conditions, we * use the optimistic approach: if the directory cannot be created, we try to * obtain a new temporary file rather than erroring out. *

*

* It is the caller's responsibility to make sure that the directory is * deleted; see {@link #deleteRecursively(File)}. *

* * @param prefix The prefix string to be used in generating the file's name; * see {@link File#createTempFile(String, String, File)} * @param suffix The suffix string to be used in generating the file's name; * see {@link File#createTempFile(String, String, File)} * @return An abstract pathname denoting a newly-created empty directory * @throws IOException */ public static File createTemporaryDirectory(final String prefix, final String suffix) throws IOException { return createTemporaryDirectory(prefix, suffix, null); } /** * Creates a temporary directory. *

* Since there is no atomic operation to do that, we create a temporary file, * delete it and create a directory in its place. To avoid race conditions, we * use the optimistic approach: if the directory cannot be created, we try to * obtain a new temporary file rather than erroring out. *

*

* It is the caller's responsibility to make sure that the directory is * deleted; see {@link #deleteRecursively(File)}. *

* * @param prefix The prefix string to be used in generating the file's name; * see {@link File#createTempFile(String, String, File)} * @param suffix The suffix string to be used in generating the file's name; * see {@link File#createTempFile(String, String, File)} * @param directory The directory in which the file is to be created, or null * if the default temporary-file directory is to be used * @return: An abstract pathname denoting a newly-created empty directory * @throws IOException */ public static File createTemporaryDirectory(final String prefix, final String suffix, final File directory) throws IOException { for (int counter = 0; counter < 10; counter++) { final File file = File.createTempFile(prefix, suffix, directory); if (!file.delete()) { throw new IOException("Could not delete file " + file); } // in case of a race condition, just try again if (file.mkdir()) return file; } throw new IOException( "Could not create temporary directory (too many race conditions?)"); } /** * Deletes a directory recursively. * * @param directory The directory to delete. * @return whether it succeeded (see also {@link File#delete()}) */ public static boolean deleteRecursively(final File directory) { if (directory == null) return true; final File[] list = directory.listFiles(); if (list == null) return true; for (final File file : list) { if (file.isFile()) { if (!file.delete()) return false; } else if (file.isDirectory()) { if (!deleteRecursively(file)) return false; } } return directory.delete(); } /** * Recursively lists the contents of the referenced directory. Directories are * excluded from the result. Supported protocols include {@code file} and * {@code jar}. * * @param directory The directory whose contents should be listed. * @return A collection of {@link URL}s representing the directory's contents. * @see #listContents(URL, boolean, boolean) */ public static Collection listContents(final URL directory) { return listContents(directory, true, true); } /** * Lists all contents of the referenced directory. Supported protocols include * {@code file} and {@code jar}. * * @param directory The directory whose contents should be listed. * @param recurse Whether to list contents recursively, as opposed to only the * directory's direct contents. * @param filesOnly Whether to exclude directories in the resulting collection * of contents. * @return A collection of {@link URL}s representing the directory's contents. */ public static Collection listContents(final URL directory, final boolean recurse, final boolean filesOnly) { return appendContents(new ArrayList(), directory, recurse, filesOnly); } /** * Recursively adds contents from the referenced directory to an existing * collection. Directories are excluded from the result. Supported protocols * include {@code file} and {@code jar}. * * @param result The collection to which contents should be added. * @param directory The directory whose contents should be listed. * @return A collection of {@link URL}s representing the directory's contents. * @see #appendContents(Collection, URL, boolean, boolean) */ public static Collection appendContents(final Collection result, final URL directory) { return appendContents(result, directory, true, true); } /** * Add contents from the referenced directory to an existing collection. * Supported protocols include {@code file} and {@code jar}. * * @param result The collection to which contents should be added. * @param directory The directory whose contents should be listed. * @param recurse Whether to append contents recursively, as opposed to only * the directory's direct contents. * @param filesOnly Whether to exclude directories in the resulting collection * of contents. * @return A collection of {@link URL}s representing the directory's contents. */ public static Collection appendContents(final Collection result, final URL directory, final boolean recurse, final boolean filesOnly) { if (directory == null) return result; // nothing to append final String protocol = directory.getProtocol(); if (protocol.equals("file")) { final File dir = urlToFile(directory); final File[] list = dir.listFiles(); if (list != null) { for (final File file : list) { try { if (!filesOnly || file.isFile()) { result.add(file.toURI().toURL()); } if (recurse && file.isDirectory()) { appendContents(result, file.toURI().toURL(), recurse, filesOnly); } } catch (final MalformedURLException e) { e.printStackTrace(); } } } } else if (protocol.equals("jar")) { try { final String url = directory.toString(); final int bang = url.indexOf("!/"); if (bang < 0) return result; final String prefix = url.substring(bang + 2); final String baseURL = url.substring(0, bang + 2); final JarURLConnection connection = (JarURLConnection) new URL(baseURL).openConnection(); final JarFile jar = connection.getJarFile(); for (final JarEntry entry : new IteratorPlus<>(jar.entries())) { final String urlEncoded = new URI(null, null, entry.getName(), null).toString(); if (urlEncoded.length() > prefix.length() && // omit directory itself urlEncoded.startsWith(prefix)) { if (filesOnly && urlEncoded.endsWith("/")) { // URL is directory; exclude it continue; } if (!recurse) { // check whether this URL is a *direct* child of the directory final int slash = urlEncoded.indexOf("/", prefix.length()); if (slash >= 0 && slash != urlEncoded.length() - 1) { // not a direct child continue; } } result.add(new URL(baseURL + urlEncoded)); } } jar.close(); } catch (final IOException e) { e.printStackTrace(); } catch (final URISyntaxException e) { throw new IllegalArgumentException(e); } } return result; } /** * Finds {@link URL}s of available resources. Both JAR files and files on disk * are searched, according to the following mechanism: *
    *
  1. Resources at the given {@code pathPrefix} are discovered using * {@link ClassLoader#getResources(String)} with the current thread's context * class loader. In particular, this invocation discovers resources in JAR * files beneath the given {@code pathPrefix}.
  2. *
  3. The directory named {@code pathPrefix} beneath the given * {@code baseDirectory} is scanned last, so that users can more easily * override resources provided inside JAR files by placing a resource of the * same name within that directory.
  4. *
*

* In both cases, resources are then recursively scanned using * {@link #listContents(URL)}, and anything matching the given {@code regex} * pattern is added to the output map. *

* * @param regex The regex to use when matching resources, or null to match * everything. * @param pathPrefix The path to search for resources. * @param baseDirectory The {@code baseDirectory/pathPrefix} directory to scan * after the URL resources. * @return A map of URLs referencing the matched resources. * @see AppUtils#getBaseDirectory */ public static Map findResources(final String regex, final String pathPrefix, final File baseDirectory) { // scan URL resource paths first final ClassLoader loader = Thread.currentThread().getContextClassLoader(); final ArrayList urls = new ArrayList<>(); try { urls.addAll(Collections.list(loader.getResources(pathPrefix + "/"))); } catch (final IOException exc) { // error loading resources; proceed with an empty list } // scan directory second; user can thus override resources from JARs if (baseDirectory != null) { try { urls.add(new File(baseDirectory, pathPrefix).toURI().toURL()); } catch (final MalformedURLException exc) { // error adding directory; proceed without it } } return findResources(regex, urls); } /** * Finds {@link URL}s of resources known to the system. *

* Each of the given {@link URL}s is recursively scanned using * {@link #listContents(URL)}, and anything matching the given {@code regex} * pattern is added to the output map. * * @param regex The regex to use when matching resources, or null to match * everything. * @param urls Paths to search for resources. * @return A map of URLs referencing the matched resources. */ public static Map findResources(final String regex, final Iterable urls) { final HashMap result = new HashMap<>(); final Pattern pattern = regex == null ? null : Pattern.compile(regex); for (final URL url : urls) { getResources(pattern, result, url); } return result; } // -- Helper methods -- /** Builds the {@link #VERSION_PATTERN} constant. */ private static Pattern buildVersionPattern() { final String version = "\\d+(\\.\\d+|\\d{7})+[a-z]?\\d?(-[A-Za-z0-9.]+?|\\.GA)*?"; final String suffix = "\\.jar(-[a-z]*)?"; return Pattern.compile("(.+?)(-" + version + ")?((-(" + classifiers() + "))?(" + suffix + "))"); } /** Helper method of {@link #buildVersionPattern()}. */ private static String classifiers() { final String[] classifiers = { "swing", "swt", "shaded", "sources", "javadoc", "native", "(natives-)?(android|linux|macosx|solaris|windows)-" + "(aarch64|amd64|arm|armv6|armv6hf|i586|universal|x86|x86_64)", }; final StringBuilder sb = new StringBuilder("("); for (final String classifier : classifiers) { if (sb.length() > 1) sb.append("|"); sb.append(classifier); } sb.append(")"); return sb.toString(); } /** Helper method of {@link #findResources(String, Iterable)}. */ private static void getResources(final Pattern pattern, final Map result, final URL base) { final String prefix = urlPath(base); if (prefix == null) return; // unsupported base URL for (final URL url : FileUtils.listContents(base)) { final String s = urlPath(url); if (s == null || !s.startsWith(prefix)) continue; if (pattern == null || pattern.matcher(s).matches()) { // this resource matches the pattern final String key = urlPath(s.substring(prefix.length())); if (key != null) result.put(key, url); } } } /** Helper method of {@link #getResources(Pattern, Map, URL)}. */ private static String urlPath(final URL url) { try { return url.toURI().toString(); } catch (final URISyntaxException exc) { return null; } } /** Helper method of {@link #getResources(Pattern, Map, URL)}. */ private static String urlPath(final String path) { try { return new URI(path).getPath(); } catch (final URISyntaxException exc) { return null; } } // -- Deprecated methods -- /** * Returns the {@link Matcher} object dissecting a versioned file name. * * @param filename the file name * @return the {@link Matcher} object * @deprecated see {@link #stripFilenameVersion(String)} */ @Deprecated public static Matcher matchVersionedFilename(final String filename) { return VERSION_PATTERN.matcher(filename); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy