/*BEGIN_COPYRIGHT_BLOCK
*
* Copyright (c) 2001-2010, JavaPLT group at Rice University (drjava@rice.edu)
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * 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.
* * Neither the names of DrJava, the JavaPLT group, Rice University, nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* 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 OWNER 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.
*
* This software is Open Source Initiative approved Open Source Software.
* Open Source Initative Approved is a trademark of the Open Source Initiative.
*
* This file is part of DrJava. Download the current version of this project
* from http://www.drjava.org/ or http://sourceforge.net/projects/drjava/
*
* END_COPYRIGHT_BLOCK*/
package edu.rice.cs.util;
import java.io.*;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.*;
import java.util.jar.*;
import edu.rice.cs.drjava.DrJava;
import edu.rice.cs.util.FileOps;
import edu.rice.cs.util.Log;
import edu.rice.cs.plt.io.IOUtil;
import edu.rice.cs.plt.text.TextUtil;
import static edu.rice.cs.drjava.config.OptionConstants.*;
/** A class to provide some convenient file operations as static methods.
* It's abstract to prevent (useless) instantiation, though it can be subclassed
* to provide convenient namespace importation of its methods.
*
* @version $Id$
*/
public abstract class FileOps {
private static Log _log = new Log("FileOpsTest.txt", false);
/** A singleton null file class. There is a separate NullFile class in this package. TODO: merge these two classes.
* This class is used for all NullFile.ONLY references while the other is used for distinct untitled documents.
* Both appear to define the same notion of equality.
*/
public static class NullFile extends File {
public static final NullFile ONLY = new NullFile();
private NullFile() { super(""); }
public boolean canRead() { return false; }
public boolean canWrite() { return false; }
public int compareTo(File f) { return (f == this) ? 0 : -1; }
public boolean createNewFile() { return false; }
public boolean delete() { return false; }
public void deleteOnExit() { }
public boolean equals(Object o) { return o == this; }
public boolean exists() { return false; }
public int hashCode() { return getClass().hashCode(); }
public File getAbsoluteFile() { return this; }
public String getAbsolutePath() { return ""; }
public File getCanonicalFile() { return this; }
public String getCanonicalPath() { return ""; }
public String getName() { return ""; }
public String getParent() { return null; }
public File getParentFile() { return null; }
public String getPath() { return ""; }
public boolean isAbsolute() { return false; }
public boolean isDirectory() { return false; }
public boolean isFile() { return false; }
public boolean isHidden() { return false; }
public long lastModified() { return 0L; }
public long length() { return 0L; }
public String[] list() { return null; }
public String[] list(FilenameFilter filter) { return null; }
public File[] listFiles() { return null; }
public File[] listFiles(FileFilter filter) { return null; }
public File[] listFiles(FilenameFilter filter) { return null; }
public boolean mkdir() { return false; }
public boolean mkdirs() { return false; }
public boolean renameTo(File dest) { return false; }
public boolean setLastModified(long time) { return false; }
public boolean setReadOnly() { return false; }
public String toString() { return ""; }
//public URI toURI() {} (Defer to super implementation.)
//public URL toURL() {} (Defer to super implementation.)
};
/** Special sentinal file used in FileOption and test classes among others. */
public static final File NULL_FILE = NullFile.ONLY;
/** @deprecated For a best-attempt canonical file, use {@link edu.rice.cs.plt.io.IOUtil#attemptCanonicalFile} instead.
* (for example, {@code IOUtil.attemptCanonicalFile(new File(path))})
*/
@Deprecated public static File makeFile(String path) {
File f = new File(path);
try { return f.getCanonicalFile(); }
catch(IOException e) { return f; }
}
/** @deprecated For a best-attempt canonical file, use {@link edu.rice.cs.plt.io.IOUtil#attemptCanonicalFile} instead.
* (for example, {@code IOUtil.attemptCanonicalFile(new File(path))})
*/
@Deprecated public static File makeFile(File parentDir, String child) {
File f = new File(parentDir, child);
try { return f.getCanonicalFile(); }
catch(IOException e) { return f; }
}
/** Determines whether the specified file in within the specified file tree.
* @deprecated Use {@link edu.rice.cs.plt.io.IOUtil#isMember} instead. Note that the new method does not test for
* {@code null} values and does not convert to canonical paths -- if these things are necessary, they
* should be done before invoking the method.
*/
@Deprecated public static boolean inFileTree(File f, File root) {
if (root == null || f == null) return false;
try {
if (! f.isDirectory()) f = f.getParentFile();
String filePath = f.getCanonicalPath() + File.separator;
String projectPath = root.getCanonicalPath() + File.separator;
return (filePath.startsWith(projectPath));
}
catch(IOException e) { return false; }
}
/** Return true if the directory ancestor is an ancestor of the file f, i.e.
* you can get from f to ancestor by using getParentFile zero or more times.
* @param ancestor the ancestor
* @param f the file to test
* @return true if ancestor is an ancestor of f. */
public static boolean isAncestorOf(File ancestor, File f) {
ancestor = ancestor.getAbsoluteFile();
f = f.getAbsoluteFile();
_log.log("ancestor = " +ancestor + " f = " + f);
while ((!ancestor.equals(f)) && (f != null)) {
f = f.getParentFile();
}
return (ancestor.equals(f));
}
/** Makes a file equivalent to the given file f that is relative to base file b. In other words,
* new File(b,makeRelativeTo(base,abs)).getCanonicalPath() equals
* f.getCanonicalPath()
*
*
In Linux/Unix, if the file f is /home/username/folder/file.java and the file b is
* /home/username/folder/sublevel/file2.java, then the resulting File path from this method would be
* ../file.java while its canoncial path would be /home/username/folder/file.java.
* Warning: making paths relative is inherently broken on some file systems, because a relative path
* requires that both the file and the base have the same root. The Windows file system, and therefore also
* the Java file system model, however, support multiple system roots (see {@link File#listRoots}).
* Thus, two files with different roots cannot have a relative path. In that case the absolute path of
* the file will be returned
* @param f The path that is to be made relative to the base file
* @param b The file to make the next file relative to
* @return A new file whose path is relative to the base file while the value of getCanonicalPath()
* for the returned file is the same as the result of getCanonicalPath() for the given file.
*/
public static File makeRelativeTo(File f, File b) throws IOException, SecurityException {
return new File(b, stringMakeRelativeTo(f,b));
}
/** Makes a file equivalent to the given file f that is relative to base file b. In other words,
* new File(b,makeRelativeTo(base,abs)).getCanonicalPath() equals
* f.getCanonicalPath()
*
* In Linux/Unix, if the file f is /home/username/folder/file.java and the file b is
* /home/username/folder/sublevel/file2.java, then the resulting File path from this method would be
* ../file.java while its canoncial path would be /home/username/folder/file.java.
* Warning: making paths relative is inherently broken on some file systems, because a relative path
* requires that both the file and the base have the same root. The Windows file system, and therefore also
* the Java file system model, however, support multiple system roots (see {@link File#listRoots}).
* Thus, two files with different roots cannot have a relative path. In that case the absolute path of
* the file will be returned
* @param f The path that is to be made relative to the base file
* @param b The file to make the next file relative to
* @return A new file whose path is relative to the base file while the value of getCanonicalPath()
* for the returned file is the same as the result of getCanonicalPath() for the given file.
*/
public static String stringMakeRelativeTo(File f, File b) throws IOException /*, SecurityException */ {
try {
File[] roots = File.listRoots();
File fRoot = null;
File bRoot = null;
for(File r: roots) {
if (isAncestorOf(r, f)) { fRoot = r; }
if (isAncestorOf(r, b)) { bRoot = r; }
if ((fRoot != null) && (bRoot != null)) { break; }
}
//Makes exception for //server/folder
if (((fRoot == null) || (!fRoot.equals(bRoot))) && (!f.getAbsoluteFile().getCanonicalFile().toString().startsWith(File.separator + File.separator))) {
return f.getAbsoluteFile().getCanonicalFile().toString();
}
}
catch(Exception e) { /* ignore, follow previous procedure */ }
File base = b.getCanonicalFile();
File abs = f.getCanonicalFile(); // If f is relative, uses current working directory ("user.dir")
if (! base.isDirectory()) base = base.getParentFile();
String last = "";
if (! abs.isDirectory()) {
String tmp = abs.getPath();
last = tmp.substring(tmp.lastIndexOf(File.separator) + 1);
abs = abs.getParentFile();
}
// System.err.println("makeRelativeTo called; f = " + f + " = " + abs + "; b = " + b + " = " + base);
String[] basParts = splitFile(base);
String[] absParts = splitFile(abs);
final StringBuilder result = new StringBuilder();
// loop until elements differ, record what part of absParts to append
// next find out how many .. to put in.
int diffIndex = -1;
boolean different = false;
for (int i = 0; i < basParts.length; i++) {
if (!different && ((i >= absParts.length) || !basParts[i].equals(absParts[i]))) {
different = true;
diffIndex = i;
}
if (different) result.append("..").append(File.separator);
}
if (diffIndex < 0) diffIndex = basParts.length;
for (int i = diffIndex; i < absParts.length; i++) {
result.append(absParts[i]).append(File.separator);
}
result.append(last);
// System.err.println("makeRelativeTo(" + f + ", " + b + ") = " + result);
return result.toString();
}
/** Splits a file into an array of strings representing each parent folder of the given file. The file whose path
* is /home/username/txt.txt in linux would be split into the string array:
* {"","home","username","txt.txt"}. Delimeters are excluded.
* @param fileToSplit the file to split into its directories.
* @deprecated Use {@link edu.rice.cs.plt.io.IOUtil#fullPath} instead. It returns a list of {@code File}
* objects rather than strings, but they appear in the same order.
*/
@Deprecated public static String[] splitFile(File fileToSplit) {
String path = fileToSplit.getPath();
ArrayList list = new ArrayList();
while (! path.equals("")) {
int idx = path.indexOf(File.separator);
if (idx < 0) {
list.add(path);
path = "";
}
else {
list.add(path.substring(0,idx));
path = path.substring(idx + 1);
}
}
return list.toArray(new String[list.size()]);
}
/** List all files (that is, {@code File}s for which {@code isFile()} is {@code true}) matching the provided filter in
* the given directory.
* @param d The directory to search.
* @param recur Whether subdirectories accepted by {@code f} should be recursively searched. Note that
* subdirectories that aren't accepted by {@code f} will be ignored.
* @param f The filter to apply to contained {@code File}s.
* @return An array of Files in the directory specified; if the directory does not exist, returns an empty list.
* @deprecated Use {@link edu.rice.cs.plt.io.IOUtil#attemptListFilesAsIterable} or
* {@link edu.rice.cs.plt.io.IOUtil#listFilesRecursively(File, FileFilter, FileFilter)} instead.
*/
@Deprecated public static ArrayList getFilesInDir(File d, boolean recur, FileFilter f) {
ArrayList l = new ArrayList();
getFilesInDir(d, l, recur, f);
return l;
}
/** Helper fuction for getFilesInDir(File d, boolean recur). {@code acc} is mutated to contain
* a list of Files in the directory specified, not including directories.
*/
private static void getFilesInDir(File d, List acc, boolean recur, FileFilter filter) {
if (d.isDirectory()) {
File[] files = d.listFiles(filter);
if (files != null) { // listFiles may return null if there's an IO error
for (File f: files) {
if (f.isDirectory() && recur) getFilesInDir(f, acc, recur, filter);
else if (f.isFile()) acc.add(f);
}
}
}
}
/** @return the canonical file equivalent to f. Identical to f.getCanonicalFile() except it does not throw an
* exception when the file path syntax is incorrect (or an IOException or SecurityException occurs for any
* other reason). It returns the absolute File intead.
* @deprecated Use {@link edu.rice.cs.plt.io.IOUtil#attemptCanonicalFile} instead.
*/
@Deprecated public static File getCanonicalFile(File f) {
if (f == null) return f;
try { return f.getCanonicalFile(); }
catch (IOException e) { /* fall through */ }
catch (SecurityException e) { /* fall through */ }
return f.getAbsoluteFile();
}
/** @return the canonical path for f. Identical to f.getCanonicalPath() except it does not throw an
* exception when the file path syntax is incorrect; it returns the absolute path instead.
* @deprecated Use {@link edu.rice.cs.plt.io.IOUtil#attemptCanonicalFile} instead. (The result will be a
* {@code File} instead of a {@code String}.)
*/
@Deprecated public static String getCanonicalPath(File f) { return getCanonicalFile(f).getPath(); }
/** @return the file f unchanged if f exists; otherwise returns NULL_FILE. */
public static File validate(File f) {
if (f.exists()) return f;
return FileOps.NULL_FILE; // This File object exists
}
/** This filter checks for files with names that end in ".java". (Note that while this filter was intended
* to be a {@code javax.swing.filechooser.FileFilter}, it actually implements a {@code java.io.FileFilter}, because
* that is what {@code FileFilter} means in the context of this source file.)
*/
@Deprecated public static final FileFilter JAVA_FILE_FILTER = new FileFilter() {
public boolean accept(File f){
// Do this runaround for filesystems that are case preserving but case insensitive. Remove the last 5
// letters from the file name, append ".java" to the end, create a new file and see if its equivalent
// to the original
final StringBuilder name = new StringBuilder(f.getAbsolutePath());
String shortName = f.getName();
if (shortName.length() < 6) return false;
name.delete(name.length() - 5, name.length());
name.append(".java");
File test = new File(name.toString());
return (test.equals(f));
}
/* The following method is commented out because it was inaccessible. */
// public String getDescription() { return "Java Source Files (*.java)"; }
};
/** Reads the stream until it reaches EOF, and then returns the read contents as a byte array. This call may block,
* since it will not return until EOF has been reached.
* @param stream Input stream to read.
* @return Byte array consisting of all data read from stream.
* @deprecated Use {@link edu.rice.cs.plt.io.IOUtil#toByteArray} instead. Note that the {@code IOUtil} method will
* not close the {@code InputStream}, while this method does.
*/
@Deprecated public static byte[] readStreamAsBytes(final InputStream stream) throws IOException {
BufferedInputStream buffered;
if (stream instanceof BufferedInputStream) buffered = (BufferedInputStream) stream;
else buffered = new BufferedInputStream(stream);
ByteArrayOutputStream out = new ByteArrayOutputStream();
int readVal = buffered.read();
while (readVal != -1) {
out.write(readVal);
readVal = buffered.read();
}
stream.close();
return out.toByteArray();
}
/** Reads the entire contents of a file and return them as canonicalized Swing Document text. All newLine sequences,
* including "\n", "\r", and "\r\n" are converted to "\n". Characters below 32, except for newlines, are changed to spaces. */
public static String readFileAsSwingText(final File file) throws IOException {
FileReader reader = null;
try {
reader = new FileReader(file);
final StringBuilder buf = new StringBuilder();
char pred = (char) 0; // initialize as null character
while (reader.ready()) {
char c = (char) reader.read();
if (c == '\n' && pred == '\r') { } // do nothing ignoring second character of "\r\n";
else if (c == '\r') buf.append('\n');
else if ((c < 32) && (c != '\n')) buf.append(' ');
else buf.append(c);
pred = c;
}
return buf.toString();
}
finally { if (reader != null) reader.close(); }
}
/** Reads the entire contents of a file and return them as a String.
* @deprecated Use {@link edu.rice.cs.plt.io.IOUtil#toString(File)} instead, which provides the same functionality.
*/
@Deprecated public static String readFileAsString(final File file) throws IOException {
FileReader reader = null;
try {
reader = new FileReader(file);
final StringBuilder buf = new StringBuilder();
while (reader.ready()) {
char c = (char) reader.read();
buf.append(c);
}
return buf.toString();
}
finally { if (reader != null) reader.close(); }
}
/** Copies the text of one file into another.
* @param source the file to be copied
* @param dest the file to be copied to
* @deprecated Use {@link edu.rice.cs.plt.io.IOUtil#copyFile} instead; it scales in a much more efficiently.
*/
@Deprecated public static void copyFile(File source, File dest) throws IOException {
String text = readFileAsString(source);
writeStringToFile(dest, text);
}
/** Creates a new temporary file and writes the given text to it. The file will be deleted on exit.
* @param prefix Beginning part of file name, before unique number
* @param suffix Ending part of file name, after unique number
* @param text Text to write to file
* @return name of the temporary file that was created
* @deprecated Instead, create a temp file with {@link edu.rice.cs.plt.io.IOUtil#createAndMarkTempFile(String, String)},
* then write to it with {@link edu.rice.cs.plt.io.IOUtil#writeStringToFile(File, String)}.
*/
@Deprecated public static File writeStringToNewTempFile(final String prefix, final String suffix, final String text)
throws IOException {
File file = File.createTempFile(prefix, suffix);
file.deleteOnExit();
writeStringToFile(file, text);
return file;
}
/** Writes text to the file overwriting whatever was there.
* @param file File to write to
* @param text Test to write
* @deprecated Use the equivalent {@link edu.rice.cs.plt.io.IOUtil#writeStringToFile(File, String)} instead
*/
@Deprecated public static void writeStringToFile(File file, String text) throws IOException {
writeStringToFile(file, text, false);
}
/** Writes text to the file.
* @param file File to write to
* @param text Text to write
* @param append whether to append. (false=overwrite)
* @deprecated Use the equivalent {@link edu.rice.cs.plt.io.IOUtil#writeStringToFile(File, String, boolean)} instead
*/
@Deprecated public static void writeStringToFile(File file, String text, boolean append) throws IOException {
FileWriter writer = null;
try {
writer = new FileWriter(file, append);
writer.write(text);
}
finally { if (writer != null) writer.close(); }
}
/** Writes text to the given file returning true if it succeeded and false if not. This is a simple wrapper for
* writeStringToFile that doesn't throw an IOException.
* @param file File to write to
* @param text Text to write
* @param append Whether to append. (false=overwrite)
* @deprecated Use {@link edu.rice.cs.plt.io.IOUtil#attemptWriteStringToFile(File, String, boolean)} instead.
*/
@Deprecated public static boolean writeIfPossible(File file, String text, boolean append) {
try {
writeStringToFile(file, text, append);
return true;
}
catch(IOException e) { return false; }
}
/** Create a new temporary directory. The directory will be deleted on exit, if empty.
* (To delete it recursively on exit, use deleteDirectoryOnExit.)
* @param name Non-unique portion of the name of the directory to create.
* @return File representing the directory that was created.
*/
// * @deprecated Use {@link edu.rice.cs.plt.io.IOUtil#createAndMarkTempDirectory(String, String)} instead.
// * Example: {@code IOUtil.createAndMarkTempDirectory(name, "")}.
/* @Deprecated */ public static File createTempDirectory(final String name) throws IOException {
return createTempDirectory(name, null);
}
/** Create a new temporary directory. The directory will be deleted on exit, if it only contains temp files and temp
* directories created after it. (To delete it on exit regardless of contents, call deleteDirectoryOnExit after
* constructing the file tree rooted at this directory. Note that createTempDirectory(..) is not much more helpful
* than mkdir() in this context (other than generating a new temp file name) because cleanup is a manual process.)
* @param name Non-unique portion of the name of the directory to create.
* @param parent Parent directory to contain the new directory
* @return File representing the directory that was created.
*/
// * @deprecated Use {@link edu.rice.cs.plt.io.IOUtil#createAndMarkTempDirectory(String, String, File)} instead.
// * Example: {@code IOUtil.createAndMarkTempDirectory(name, "", parent)}.
/* @Deprecated */ public static File createTempDirectory(/* final */ String name, /* final */ File parent) throws IOException {
File result = File.createTempFile(name, "", parent);
boolean success = result.delete();
success = success && result.mkdir();
if (! success) { throw new IOException("Attempt to create directory failed"); }
IOUtil.attemptDeleteOnExit(result);
return result;
// File file = File.createTempFile(name, "", parent);
// file.delete();
// file.mkdir();
// file.deleteOnExit();
//
// return file;
}
/** Delete the given directory including any files and directories it contains.
* @param dir File object representing directory to delete. If, for some reason, this file object is not a
* directory, it will still be deleted.
* @return true if there were no problems in deleting. If it returns false, something failed and the directory
* contents likely at least partially still exist.
* @deprecated Use {@link edu.rice.cs.plt.io.IOUtil#deleteRecursively} instead
*/
@Deprecated public static boolean deleteDirectory(final File dir) {
// System.err.println("Deleting file or directory " + dir);
if (! dir.isDirectory()) {
boolean res;
res = dir.delete();
// System.err.println("Deletion of " + dir + " returned " + res);
return res;
}
boolean ret = true;
File[] childFiles = dir.listFiles();
if (childFiles != null) { // listFiles may return null if there's an IO error
for (File f: childFiles) { ret = ret && deleteDirectory(f); }
}
// Now we should have an empty directory
ret = ret && dir.delete();
// System.err.println("Recursive deletion of " + dir + " returned " + ret);
return ret;
}
/** Instructs Java to recursively delete the given directory and its contents when the JVM exits.
* @param dir File object representing directory to delete. If, for some reason, this file object is not a
* directory, it will still be deleted.
* @deprecated Use {@link edu.rice.cs.plt.io.IOUtil#deleteOnExitRecursively} instead
*/
@Deprecated public static void deleteDirectoryOnExit(final File dir) {
// Delete this on exit, whether it's a directory or file
_log.log("Deleting file/directory " + dir + " on exit");
dir.deleteOnExit();
// If it's a directory, visit its children. This recursive walk has to be done AFTER calling deleteOnExit
// on the directory itself because Java closes the list of files to deleted on exit in reverse order.
if (dir.isDirectory()) {
File[] childFiles = dir.listFiles();
if (childFiles != null) { // listFiles may return null if there's an IO error
for (File f: childFiles) { deleteDirectoryOnExit(f); }
}
}
}
/** This function starts from the given directory and finds all packages within that directory
* @param prefix the package name of files in the given root
* @param root the directory to start exploring from
* @return a list of valid packages, excluding the root ("") package
*/
public static LinkedList packageExplore(String prefix, File root) {
/* Inner holder class. */
class PrefixAndFile {
public String prefix;
public File root;
public PrefixAndFile(String prefix, File root) {
this.root = root;
this.prefix = prefix;
}
}
// This set makes sure we don't get caught in a loop if the filesystem has symbolic links
// that form a circle by tracking the directories we have already explored
final Set exploredDirectories = new HashSet();
LinkedList output = new LinkedList();
Stack working = new Stack();
working.push(new PrefixAndFile(prefix, root));
exploredDirectories.add(root);
// This filter allows only directories, and accepts each directory only once
FileFilter directoryFilter = new FileFilter(){
public boolean accept(File f){
boolean toReturn = f.isDirectory() && ! exploredDirectories.contains(f);
exploredDirectories.add(f);
return toReturn;
}
/* The following method is commented out because it was inaccessible. */
// public String getDescription() { return "All Folders"; }
};
// Explore each directory, adding (unique) subdirectories to the working list. If a directory has .java
// files, add the associated package to the list of packages
while (! working.empty()) {
PrefixAndFile current = working.pop();
File [] subDirectories = current.root.listFiles(directoryFilter);
if (subDirectories != null) { // listFiles may return null if there's an IO error
for (File dir: subDirectories) {
PrefixAndFile paf;
// System.out.println("exploring " + dir);
if (current.prefix.equals("")) paf = new PrefixAndFile(dir.getName(), dir);
else paf = new PrefixAndFile(current.prefix + "." + dir.getName(), dir);
working.push(paf);
}
}
File [] javaFiles = current.root.listFiles(JAVA_FILE_FILTER);
if (javaFiles != null) { // listFiles may return null if there's an IO error
//Only add package names if they have java files and are not the root package
if (javaFiles.length != 0 && !current.prefix.equals("")) {
output.add(current.prefix);
// System.out.println("adding " + current.prefix);
}
}
}
return output;
}
/** Renames the given file to the given destination. Needed since Windows does not allow a rename to overwrite an
* existing file.
* @param file the file to rename
* @param dest the destination file
* @return true iff the rename was successful
* @deprecated Use {@link edu.rice.cs.plt.io.IOUtil#attemptMove}, which is equally Windows-friendly, instead.
*/
@Deprecated public static boolean renameFile(File file, File dest) {
if (dest.exists()) dest.delete();
return file.renameTo(dest);
}
/** This method writes files correctly; it takes care of catching errors, making backups, and keeping an unsuccessful
* file save from destroying the old file (unless a backup is made). It makes sure that the file to be saved is not
* read-only, throwing an IOException if it is. Note: if saving fails and a backup was being created, any existing
* backup will be destroyed (because the backup is written before saving begins, then moved back over the original
* file when saving fails). Since the old backup would have been destroyed anyway if saving had succeeded, this
* behavior is appropriate.
* @param fileSaver Keeps track of the name of the file to write, whether to back up the file, and has
* a method that actually performs the writing of the file
* @throws IOException if the saving or backing up of the file fails for any reason
*/
public static void saveFile(FileSaver fileSaver) throws IOException {
boolean makeBackup = fileSaver.shouldBackup();
boolean success = false;
File file = fileSaver.getTargetFile();
// System.err.println("Saving file " + file + " with backup status = " + makeBackup);
File backup = null;
boolean tempFileUsed = true;
// file.canWrite() is false if file.exists() is false, but we want to be able to save a file that doesn't yet exist.
if (file.exists() && ! file.canWrite()) throw new IOException("Permission denied");
// First back up the file, if necessary.
if (makeBackup) {
backup = fileSaver.getBackupFile();
if (! renameFile(file, backup)) {
throw new IOException("Save failed. Could not create backup file "
+ backup.getAbsolutePath() +
"\nIt may be possible to save by disabling file backups\n");
}
// System.err.println("saveFile renamed " + file + " as " + backup);
fileSaver.backupDone(); // Why? This action may have to be reversed if writing new file fails!
// System.err.println("Contents: '" + IOUtil.toString(backup) + "'");
}
// ScrollableDialog sd2 = new ScrollableDialog(null, "backup done in FileOps.saveFile", "", "");
// sd2.show();
// Create a temp file in the same directory as the file to be saved.
// From this point forward, enclose in try...finally so that we can clean
// up the temp file and restore the file from its backup.
File parent = file.getParentFile();
File tempFile = File.createTempFile("drjava", ".temp", parent);
// System.err.println("tempfileName = " + tempFile + " for backup file " + backup);
// ScrollableDialog sd3 = new ScrollableDialog(null, "temp file " + tempFile + "created in FileOps.saveFile", "", "");
// sd3.show();
try {
/* Now, write your output to the temp file, then rename it to the correct
name. This way, if writing fails in the middle, the old file is not
lost. */
FileOutputStream fos;
try {
/* The next line will fail if we can't create the temp file. This may mean that
* the user does not have write permission on the directory the file they
* are editing is in. We may want to go ahead and try writing directly
* to the target file in this case
*/
fos = new FileOutputStream(tempFile);
}
catch (FileNotFoundException fnfe) {
if (fileSaver.continueWhenTempFileCreationFails()) {
fos = new FileOutputStream(file);
tempFileUsed = false;
}
else throw new IOException("Could not create temp file " + tempFile + " in attempt to save " + file);
}
BufferedOutputStream bos = new BufferedOutputStream(fos);
fileSaver.saveTo(bos);
// System.err.println(bos + " written");
// System.err.println("Closing " + bos + " and " + fos);
bos.close();
// fos.close();
// System.err.println("Wrote: " + tempFile);
if (tempFileUsed && ! renameFile(tempFile, file))
throw new IOException("Save failed. Another process may be using " + file + ".");
// System.err.println("Renamed " + tempFile + " as " + file);
// if (makeBackup) System.err.println("Does " + backup + " still exists? " + backup.exists());
success = true;
}
finally {
// ScrollableDialog sd4 = new ScrollableDialog(null, "finally clause reached in FileOps.saveFile", "", "");
// sd4.show();
if (tempFileUsed) tempFile.delete(); /* Delete the temp file */
if (makeBackup) {
/* On failure, attempt to move the backup back to its original location if we
made one. On success, register that a backup was successfully made */
if (success) fileSaver.backupDone();
else {
renameFile(backup, file);
// System.out.println("Forced to rename backup " + backup + " as file " + file);
}
}
}
}
public interface FileSaver {
/** This method tells what to name the backup file, if a backup is made. It may depend on getTargetFile(), so it
* can throw an IOException.
*/
public abstract File getBackupFile() throws IOException;
/** This method indicates whether or not a backup of the file should be made. It may depend on getTargetFile(),
* so it can throw an IOException.
*/
public abstract boolean shouldBackup() throws IOException;
/** This method specifies if the saving process should continue trying to save the file if the temp file that is
* written initially cannot be created. Continue saving in this case is dangerous because the original file may
* be lost if saving fails.
*/
public abstract boolean continueWhenTempFileCreationFails();
/** This method is called to tell the file saver that a backup was successfully made. */
public abstract void backupDone();
/**
* This method actually writes info to a file. NOTE: It is important that this
* method write to the stream it is passed, not the target file. If you write
* directly to the target file, the target file will be destroyed if saving fails.
* Also, it is important that when saving fails this method throw an IOException
* @throws IOException when saving fails for any reason
*/
public abstract void saveTo(OutputStream os) throws IOException;
/** This method specifies the file for saving. It should return the canonical name of the file, resolving symlinks.
* Otherwise, the saver cannot deal correctly with symlinks. Resolving symlinks may cause an IOException, so this
* method declares that it may throw an IOException.
*/
public abstract File getTargetFile() throws IOException;
}
/** This class is a default implementation of FileSaver that makes only one backup of each file per instantiation of
* the program (following the Emacs convention). It creates a backup file named ~. It does not implement the
* saveTo method.
*/
public abstract static class DefaultFileSaver implements FileSaver {
private File outputFile = FileOps.NULL_FILE;
private static Set filesNotNeedingBackup = new HashSet();
private boolean backupsEnabled = DrJava.getConfig().getSetting(BACKUP_FILES); // uses the config default
/** This field keeps track of whether or not outputFile has been resolved to its canonical name. */
private boolean isCanonical = false;
// /** Globally enables backups for any DefaultFileSaver that does not override the shouldBackup method. */
// public static void setBackupsEnabled(boolean isEnabled) { backupsEnabled = isEnabled; }
public DefaultFileSaver(File file){ outputFile = file.getAbsoluteFile(); }
public boolean continueWhenTempFileCreationFails() { return true; }
public File getBackupFile() throws IOException { return new File(getTargetFile().getPath() + "~"); }
public boolean shouldBackup() throws IOException{
if (! backupsEnabled) return false;
if (! getTargetFile().exists()) return false;
if (filesNotNeedingBackup.contains(getTargetFile())) return false;
return true;
}
public void backupDone() {
try { filesNotNeedingBackup.add(getTargetFile()); }
catch (IOException ioe) { throw new UnexpectedException(ioe, "getTargetFile should fail earlier"); }
}
public File getTargetFile() throws IOException{
if (!isCanonical) {
outputFile = outputFile.getCanonicalFile();
isCanonical = true;
}
return outputFile;
}
}
/** Converts all path entries in a path string to absolute paths. The delimiter in the path string is the
* "path.separator" property. Empty entries are equivalent to "." and thus are converted to the value of "user.dir".
* Example: ".:drjava::/home/foo/junit.jar" with "user.dir" set to "/home/foo/bar" will be converted to
* "/home/foo/bar:/home/foo/bar/drjava:/home/foo/bar:/home/foo/junit.jar".
* @param path path string with entries to convert
* @return path string with all entries as absolute paths
* @deprecated Use {@link edu.rice.cs.plt.io.IOUtil#parsePath}, {@link edu.rice.cs.plt.io.IOUtil#getAbsoluteFiles},
* {@link edu.rice.cs.plt.io.IOUtil#attemptAbsoluteFiles}, and {@link edu.rice.cs.plt.io.IOUtil#pathToString},
* as needed, instead.
*/
@Deprecated public static String convertToAbsolutePathEntries(String path) {
String pathSep = System.getProperty("path.separator");
// split leaves off trailing empty strings
// (see API javadocs: "Trailing empty strings are therefore not included in the resulting array.")
// we therefore append one element at the end and later remove it
path += pathSep + "x";
// now path ends with ":x", so we'll have an additional "x" element in the pathEntries array
// split up the path into individual entries, turn all of the entries
// into absolute paths, and put the path back together
// EXCEPT for the last item in the array, because that's the "x" we added
String[] pathEntries = path.split(pathSep);
final StringBuilder sb = new StringBuilder();
for(int i = 0; i < pathEntries.length - 1; ++i) { // length-1 to ignore the last element
File f = new File(pathEntries[i]);
sb.append(f.getAbsolutePath());
sb.append(pathSep);
}
String reconstructedPath = sb.toString();
// if the reconstructed path is non-empty, then it will have an extra
// path separator at the end; take it off
if (reconstructedPath.length() != 0) {
reconstructedPath = reconstructedPath.substring(0, reconstructedPath.length() - 1);
}
return reconstructedPath;
}
/** Return a valid directory for use, i.e. one that exists and is as "close" to the file specified. It is
* 1) file, if file is a directory and exists
* 2) the closest parent of file, if file is not a directory or does not exist
* 3) "user.home"
* @return a valid directory for use */
public static File getValidDirectory(final File origFile) {
File file = origFile;
// if it's the NULL_FILE or null, use "user.home"
if ((file == FileOps.NULL_FILE) || (file == null)) {
file = new File(System.getProperty("user.home"));
}
assert file != null;
while (file != null && ! file.exists()) {
// if the saved path doesn't exist anymore, try the parent
//NB: getParentFile() may return null
file = file.getParentFile();
}
if (file == null) {
// somehow we ended up with null, use "user.home"
file = new File(System.getProperty("user.home"));
}
assert file != null;
// if it's not a directory, try the parent
if (! file.isDirectory()) {
if (file.getParent() != null) {
file = file.getParentFile();
//NB: getParentFile() may return null
if (file == null) {
// somehow we ended up with null, use "user.home"
file = new File(System.getProperty("user.home"));
}
assert file != null;
}
}
// this should be an existing directory now
if (file.exists() && file.isDirectory()) return file;
// ye who enter here, abandon all hope...
// the saved path didn't work, and neither did "user.home"
throw new UnexpectedException(new IOException(origFile.getPath()
+ " is not a valid directory, and all attempts "
+ "to locate a valid directory have failed. "
+ "Check your configuration."));
}
/** Converts the abstract pathname for f into a URL. This method is included in class java.io.File as f.toURL(), but
* has been deprecated in Java 6.0 because escape characters on some systems are not handled correctly. The workaround,
* f.toURI().toURL(), is unsatisfactory because we rely on the old (broken) behavior: toURI() produces escape
* characters (for example, " " becomes "%20"), which remain in the name when we attempt to convert back
* to a filename. That is, f.toURI().toURL().getFile() may not be a valid path, even if f exists. (The correct
* solution is to avoid trying to convert from a URL to a File, because this conversion is not guaranteed
* to work.)
*/
public static URL toURL(File f) throws MalformedURLException { return f.toURI().toURL(); }
public static boolean makeWritable(File roFile) throws IOException {
/* Try to make the file writable. Strangely enough, there is a File.setReadOnly() method, but no built-in way to
* make the file writable. Sun recommends deleting the read-only file (does that work on all operating systems?).*/
boolean shouldBackup = edu.rice.cs.drjava.DrJava.getConfig().
getSetting(edu.rice.cs.drjava.config.OptionConstants.BACKUP_FILES);
boolean madeBackup = false;
File backup = new File(roFile.getAbsolutePath() + "~");
try {
boolean noBackup = true;
if (backup.exists()) {
try { noBackup = backup.delete(); }
catch(SecurityException se) { noBackup = false; }
}
if (noBackup) {
try {
noBackup = roFile.renameTo(backup);
madeBackup = true;
roFile.createNewFile();
}
catch(SecurityException se) { noBackup = false; }
catch(IOException ioe) { }
try { roFile.createNewFile(); }
catch(SecurityException se) { }
catch(IOException ioe) { }
}
if (! noBackup) {
try { roFile.delete(); }
catch(SecurityException se) { return false; }
}
try { edu.rice.cs.plt.io.IOUtil.copyFile(backup, roFile);}
catch(SecurityException se) { return false; }
catch(IOException ioe) { return false; }
return true;
}
finally {
if (! shouldBackup && madeBackup) {
try { backup.delete(); }
catch(Exception e) { /* not so important if we made a backup and now can't delete it */ }
}
}
}
/** Move f to n, recursively if necessary.
* @param f file or directory to move
* @param n new location and name for the file or directory
* @return true if successful */
public static boolean moveRecursively(File f, File n) {
boolean res = true;
try {
if (!f.exists()) { return false; }
if (f.isFile()) { return edu.rice.cs.plt.io.IOUtil.attemptMove(f,n); }
else {
// recursively move directory
// first create the target directory
if (!n.mkdir()) { return false; }
// now process children
for(String child: f.list()) {
File oldChild = new File(f, child);
File newChild = new File(n, child);
res = res && moveRecursively(oldChild, newChild);
}
if (! f.delete()) { return false; }
}
}
catch(Exception e) { return false; }
return res;
}
/** Generate a new file name that does not yet exist. Maximum of 20 attempts.
* Example: generateNewFileName(new File("foo.bar"))
* generates "foo.bar", "foo.bar-2", "foo.bar-3", and so on.
* @param base base name of the file
* @return new file name that does not yet exist
* @throws IOException if file name cannot be generated within 100 attempts */
public static File generateNewFileName(File base) throws IOException {
return generateNewFileName(base.getParentFile(), base.getName());
}
/** Generate a new file name that does not yet exist. Maximum of 20 attempts.
* Example: generateNewFileName(new File("."), "foo.bar")
* generates "foo.bar", "foo.bar-2", "foo.bar-3", and so on.
* @param dir directory of the file
* @param name the base file name
* @return new file name that does not yet exist
* @throws IOException if file name cannot be generated within 100 attempts */
public static File generateNewFileName(File dir, String name) throws IOException {
return generateNewFileName(dir, name, "", 100);
}
/** Generate a new file name that does not yet exist. Maximum of 20 attempts.
* @param dir directory of the file
* @param prefix the beginning of the file name
* @param suffix the end of the file name
* @return new file name that does not yet exist
* @throws IOException if file name cannot be generated within 100 attempts */
public static File generateNewFileName(File dir, String prefix, String suffix) throws IOException {
return generateNewFileName(dir, prefix, suffix, 100);
}
/** Generate a new file name that does not yet exist.
* Example: generateNewFileName(new File("."), "foo", ".bar", 10)
* generates "foo.bar", "foo-2.bar", "foo-3.bar", and so on.
* @param dir directory of the file
* @param prefix the beginning of the file name
* @param suffix the end of the file name
* @param max maximum number of attempts
* @return new file name that does not yet exist
* @throws IOException if file name cannot be generated within max attempts */
public static File generateNewFileName(File dir, String prefix, String suffix, int max) throws IOException {
File temp = new File(dir, prefix+suffix);
if (temp.exists()) {
int count = 2;
do {
temp = new File(dir, prefix + "-" + count+suffix);
++count;
} while(temp.exists() && (count DOCUME~1 Documents and Settings
// 09/02/2009 11:02 PM 123 LONGFI~1 Long File Name
// 09/02/2009 11:02 PM shortdir
// 09/02/2009 11:02 PM 123 short
// skip empty lines
if (line.trim().length() == 0) continue;
// header starts with whitespace
if (line.startsWith(" ")) continue;
// strip off first two columns
int pos = line.indexOf(" ");
if (pos == -1) continue;
pos = line.indexOf(" ", pos+2);
if (pos == -1) continue;
line = line.substring(pos).trim();
// LOG.log("\t[1] '"+line+"'");
// DOCUME~1 Documents and Settings
// 123 LONGFI~1 Long File Name
// shortdir
// 123 short
// strip off third column ( or file size)
pos = line.indexOf(' ');
if (pos == -1) continue;
line = line.substring(pos).trim();
// LOG.log("\t[2] '"+line+"'");
File shortF = null;
// if the line ends with the file name we are looking for...
// do a case-insensitive comparison here to accomodate Windows
// we later check if it's the same file
if (line.toLowerCase().equals(f.getName().toLowerCase())) {
// short file name only
shortF = new File(parent, line);
// LOG.log("\t[3] shortF = "+shortF);
if (f.getCanonicalFile().equals(shortF.getCanonicalFile())) {
// this is the short file name we are looking for
// LOG.log("\t[3a] found");
found = true;
}
}
else if (line.toLowerCase().startsWith(f.getName().toLowerCase()) && f.getName().contains("~")) {
// perhaps already short file name of a long file name
shortF = new File(parent, f.getName());
// LOG.log("\t[4] shortF = "+shortF);
if (f.getCanonicalFile().equals(shortF.getCanonicalFile())) {
// this is the short file name we are looking for
// LOG.log("\t[4a] found");
found = true;
}
}
else if (line.toLowerCase().endsWith(" "+f.getName().toLowerCase())) {
// remove the long file name at the end and trim off whitespace
// DOCUME~1
// LONGFI~1
//
//
String shortLine = line.substring(0, line.length() - f.getName().length()).trim();
// LOG.log("\t[5] shortLine: '"+shortLine+"'");
if (line.length() == 0) {
// already short
found = true;
shortF = f;
// LOG.log("\t[6] shortF = "+shortF);
}
else {
shortF = new File(parent, shortLine);
// LOG.log("\t[7] shortF = "+shortF);
// if this file exists, check that it is exactly the file we're looking for
if (shortF.exists()) {
if (f.getCanonicalFile().equals(shortF.getCanonicalFile())) {
// this is the short file name we are looking for
// set flag to true, but continue reading lines from the process
// otherwise DIR /X may block because the stdout stream is full
found = true;
}
}
}
}
if (found && (shortF != null)) {
// prepend the short file name to s
// LOG.log("\t[8 ] s = '"+s+"'");
s = shortF.getName()+((s.length() == 0)?"":(File.separator+s));
// LOG.log("\t[8a] s = '"+s+"'");
}
}
}
try {
// wait until the process is done
p.waitFor();
}
catch(InterruptedException ie) {
throw new IOException("Could not get short windows file name: "+ie);
}
if (!found) {
throw new IOException("Could not get short windows file name: "+f.getAbsolutePath()+" not found");
}
}
catch(IOException ioe) {
throw new IOException("Could not get short windows file name: "+ioe);
}
f = parent;
parent = parent.getParentFile();
}
// create the short file
File shortF = new File(root, s);
if (!shortF.exists()) {
throw new IOException("Could not get short windows file name: "+shortF.getAbsolutePath()+" not found");
}
return shortF;
}
/** Returns the drjava.jar file.
* @return drjava.jar file */
public static File getDrJavaFile() {
String[] cps = System.getProperty("java.class.path").split(TextUtil.regexEscape(File.pathSeparator),-1);
File found = null;
for(String cp: cps) {
try {
File f = new File(cp);
if (!f.exists()) { continue; }
if (f.isDirectory()) {
// this is a directory, maybe DrJava is contained here as individual files
File cf = new File(f, edu.rice.cs.drjava.DrJava.class.getName().replace('.', File.separatorChar) + ".class");
if (cf.exists() && cf.isFile()) {
found = f;
break;
}
}
else if (f.isFile()) {
// this is a file, it should be a jar file
JarFile jf = new JarFile(f);
// if it's not a jar file, an exception will already have been thrown
// so we know it is a jar file
// now let's check if it contains DrJava
if (jf.getJarEntry(edu.rice.cs.drjava.DrJava.class.getName().replace('.', '/') + ".class") != null) {
found = f;
break;
}
}
}
catch(IOException e) { /* ignore, we'll continue with the next classpath item */ }
}
return found.getAbsoluteFile();
}
/** Returns the current DrJava application, i.e. the drjava.jar, drjava.exe or DrJava.app file.
* @return DrJava application file */
public static File getDrJavaApplicationFile() {
File found = FileOps.getDrJavaFile();
if (found != null) {
if (edu.rice.cs.drjava.platform.PlatformFactory.ONLY.isMacPlatform()) {
// fix for Mac applications
String s = found.getAbsolutePath();
if (s.endsWith(".app/Contents/Resources/Java/drjava.jar")) {
found = new File(s.substring(0, s.lastIndexOf("/Contents/Resources/Java/drjava.jar")));
}
}
}
return found.getAbsoluteFile();
}
}