001 package edu.rice.cs.cunit.util;
002
003 import junit.framework.TestCase;
004
005 import java.io.*;
006 import java.util.ArrayList;
007 import java.util.Enumeration;
008 import java.util.HashSet;
009 import java.util.Set;
010 import java.util.jar.JarEntry;
011 import java.util.jar.JarFile;
012 import java.util.jar.JarOutputStream;
013 import java.util.zip.ZipEntry;
014
015 /**
016 * File Operations.
017 *
018 * @author Mathias Ricken
019 */
020 public class FileOps {
021 /**
022 * Makes a file equivalent to the given file f that is relative to base file b. In other words, <code>new
023 * File(b,makeRelativeTo(base,abs)).getCanonicalPath()</code> equals <code>f.getCanonicalPath()</code><p> In
024 * Linux/Unix, if the file f is <code>/home/username/folder/file.java</code> and the file b is
025 * <code>/home/username/folder/sublevel/file2.java</code>, then the resulting File path from this method would
026 * be <code>../file.java</code> while its canoncial path would be <code>/home/username/folder/file.java</code>.</p>
027 * In Windows, a file will be made absolute if it is on a different drive than the base.
028 *
029 * @param f The path that is to be made relative to the base file
030 * @param b The file to make the next file relative to
031 *
032 * @return A new file whose path is relative to the base file while the value of <code>getCanonicalPath()</code>
033 * for the returned file is the same as the result of <code>getCanonicalPath()</code> for the given
034 * file.
035 *
036 * @throws IOException if an I/O error occurs; may happen since getCanonicalFile uses the file system
037 */
038 public static File makeRelativeTo(File f, File b) throws IOException {
039 File base = b.getCanonicalFile();
040 File abs = f.getCanonicalFile(); // If f is relative, uses current working directory ("user.dir")
041 if (!base.isDirectory()) {
042 base = base.getParentFile();
043 }
044
045 File[] roots = File.listRoots();
046 for (File r: roots) {
047 if (abs.getAbsolutePath().startsWith(r.toString())) {
048 if (!base.getAbsolutePath().startsWith(r.toString())) {
049 return abs;
050 }
051 break;
052 }
053 }
054
055 String last = "";
056 if (!abs.isDirectory()) {
057 String tmp = abs.getPath();
058 last = tmp.substring(tmp.lastIndexOf(File.separator) + 1);
059 abs = abs.getParentFile();
060 }
061
062 // System.err.println("makeRelativeTo called; f = " + f + " = " + abs + "; b = " + b + " = " + base);
063 String[] basParts = splitFile(base);
064 String[] absParts = splitFile(abs);
065
066 StringBuffer result = new StringBuffer();
067 // loop until elements differ, record what part of absParts to append
068 // next find out how many .. to put in.
069 int diffIndex = -1;
070 boolean different = false;
071 for(int i = 0; i < basParts.length; i++) {
072 if (!different && ((i >= absParts.length) || !basParts[i].equals(absParts[i]))) {
073 different = true;
074 diffIndex = i;
075 }
076 if (different) {
077 result.append("..").append(File.separator);
078 }
079 }
080 if (diffIndex < 0) {
081 diffIndex = basParts.length;
082 }
083 for(int i = diffIndex; i < absParts.length; i++) {
084 result.append(absParts[i]).append(File.separator);
085 }
086 result.append(last);
087 // System.err.println("makeRelativeTo(" + f + ", " + b + ") = " + result);
088 return new File(result.toString());
089 }
090
091 /**
092 * Returns true if the file f is contained in the directory dir or its subdirectories.
093 * @param f the file
094 * @param dir the directory
095 * @return true if file is contained in directory or one of its subdirectories
096 */
097 public static boolean isContainedIn(File f, File dir) {
098 try {
099 return isContainedInCanonical(f.getCanonicalFile(), dir.getCanonicalFile());
100 }
101 catch(IOException ioe) { return false; }
102 }
103
104 /**
105 * Returns true if the file f is contained in the directory dir or its subdirectories, or if
106 * the file *is* the directory.
107 * Both the file and the directory must be canonical already.
108 * @param f the canonical file
109 * @param dir the canonical directory
110 * @return true if file is contained in directory or one of its subdirectories or the file *is* the directory
111 */
112 public static boolean isContainedInCanonical(File f, File dir) {
113 if ((dir==null)||(f==null)) { return false; }
114 if (f.equals(dir)) { return true; }
115 File parent = f.getParentFile();
116 return isContainedInCanonical(parent, dir);
117 }
118
119 /**
120 * Splits a file into an array of strings representing each parent folder of the given file. The file whose
121 * path is <code>/home/username/txt.txt</code> in linux would be split into the string array:
122 * {"","home","username","txt.txt"}. Delimeters are excluded.
123 *
124 * @param fileToSplit the file to split into its directories.
125 * @return array of path element names
126 */
127 public static String[] splitFile(File fileToSplit) {
128 String path = fileToSplit.getPath();
129 ArrayList<String> list = new ArrayList<String>();
130 while(!path.equals("")) {
131 int idx = path.indexOf(File.separator);
132 if (idx < 0) {
133 list.add(path);
134 path = "";
135 }
136 else {
137 list.add(path.substring(0, idx));
138 path = path.substring(idx + 1);
139 }
140 }
141 return list.toArray(new String[list.size()]);
142 }
143
144 /**
145 * Splits a string with a list of paths, separated by pathSeparator, into an array of paths.
146 * This will correctly split a path even if it contains a drive letter, but only if
147 * File.pathSeparator ('/' or '\\') follows the drive letter.
148 * Examples (assuming the File.separatorChar is '\\'):
149 * splitPaths("C:\\foo:C:\\bar", ':', true) will be split into {"C:\\foo", "C:\\bar"}.
150 * splitPaths("C:foo:C:\\bar", ':', true) will be split into {"C", "foo", "C:\\bar"}.
151 *
152 * @param pathString the string with the list of paths, separated by pathSeparator
153 * @param pathSeparator the character separating the paths (should be either ':' or ';')
154 * @return array of paths
155 */
156 public static String[] splitPaths(String pathString, char pathSeparator) {
157 return splitPaths(pathString, pathSeparator, true);
158 }
159
160 /**
161 * Splits a string with a list of paths, separated by pathSeparator, into an array of paths.
162 * If winDriveLetters is true, this will correctly split a path even if it contains
163 * a drive letter, but only if File.pathSeparator ('/' or '\\') follows the drive letter.
164 * Examples (assuming the File.separatorChar is '\\'):
165 * splitPaths("C:\\foo:C:\\bar", ':', true) will be split into {"C:\\foo", "C:\\bar"}.
166 * splitPaths("C:foo:C:\\bar", ':', true) will be split into {"C", "foo", "C:\\bar"}.
167 *
168 * @param pathString the string with the list of paths, separated by pathSeparator
169 * @param pathSeparator the character separating the paths (should be either ':' or ';')
170 * @param winDriveLetters treat "C:" followed by separatorChar as drive letter
171 * @return array of paths
172 */
173 public static String[] splitPaths(String pathString, char pathSeparator, boolean winDriveLetters) {
174 // need to check if the pathSeparator is ':' and we want Windows drive letters
175 boolean checkDriveLetters = (pathSeparator ==':') && winDriveLetters;
176 ArrayList<String> list = new ArrayList<String>();
177 int idx = 0;
178 while((idx = pathString.indexOf(pathSeparator,idx))>=0) {
179 String path = pathString.substring(0, idx);
180 if(checkDriveLetters && ((path.length()==1) && (pathString.charAt(idx+1)==File.separatorChar))) {
181 // string begins with x:\ or x:/
182 ++idx;
183 continue;
184 }
185 list.add(path);
186 pathString = pathString.substring(idx+1);
187 idx = 0;
188 }
189 list.add(pathString);
190 return list.toArray(new String[list.size()]);
191 }
192
193 /**
194 * Unequivocally exits a program, if necessary by using Runtime.halt and not executing ShutdownHooks.
195 * @param status program's exit status
196 */
197 public static void exit(final int status) {
198 Thread terminator = new Thread(new Runnable() {
199 public void run() {
200 System.exit(status);
201 }
202 }, "Attempt System.exit");
203 terminator.start();
204 try { Thread.sleep(2000); } // sleep for two seconds to allow the terminator thread to
205 catch(InterruptedException e) { /* ignore */ }
206 Runtime.getRuntime().halt(status); // force program to halt if it still alive
207 }
208
209 /**
210 * Create a new temporary directory. The directory will be deleted on exit, if empty. (To delete it recursively on
211 * exit, use deleteDirectoryOnExit.)
212 *
213 * @param name Non-unique portion of the name of the directory to create.
214 *
215 * @return File representing the directory that was created.
216 * @throws IOException
217 */
218 public static File createTempDirectory(final String name) throws IOException {
219 return createTempDirectory(name, null);
220 }
221
222 /**
223 * Create a new temporary directory. The directory will be deleted on exit, if it only contains temp files and temp
224 * directories created after it. (To delete it on exit regardless of contents, call deleteDirectoryOnExit after
225 * constructing the file tree rooted at this directory. Note that createTempDirectory(..) is not much more helpful
226 * than mkdir() in this context (other than generating a new temp file name) because cleanup is a manual process.)
227 *
228 * @param name Non-unique portion of the name of the directory to create.
229 * @param parent Parent directory to contain the new directory
230 *
231 * @return File representing the directory that was created.
232 * @throws IOException
233 */
234 public static File createTempDirectory(final String name, final File parent) throws IOException {
235 File file = File.createTempFile(name, "", parent);
236 file.delete();
237 file.mkdir();
238 file.deleteOnExit();
239
240 return file;
241 }
242
243 /**
244 * Delete the given directory including any files and directories it contains.
245 *
246 * @param dir File object representing directory to delete. If, for some reason, this file object is not a
247 * directory, it will still be deleted.
248 *
249 * @return true if there were no problems in deleting. If it returns false, something failed and the directory
250 * contents likely at least partially still exist.
251 */
252 public static boolean deleteDirectory(final File dir) {
253 if (!dir.isDirectory()) {
254 boolean res;
255 res = dir.delete();
256 if (!res) {
257 System.err.println("Could not delete "+dir);
258 }
259 return res;
260 }
261
262 boolean ret = true;
263 File[] childFiles = dir.listFiles();
264 if (childFiles != null) { // listFiles may return null if there's an IO error
265 for(File f : childFiles) {
266 ret = ret && deleteDirectory(f);
267 }
268 }
269
270 // Now we should have an empty directory
271 ret = ret && dir.delete();
272 return ret;
273 }
274
275 /**
276 * Instructs Java to recursively delete the given directory and its contents when the JVM exits.
277 *
278 * @param dir File object representing directory to delete. If, for some reason, this file object is not a
279 * directory, it will still be deleted.
280 */
281 public static void deleteDirectoryOnExit(final File dir) {
282
283 // Delete this on exit, whether it's a directory or file
284 dir.deleteOnExit();
285
286 // If it's a directory, visit its children. This recursive walk has to be done AFTER calling deleteOnExit
287 // on the directory itself because Java closes the list of files to deleted on exit in reverse order.
288 if (dir.isDirectory()) {
289 File[] childFiles = dir.listFiles();
290 if (childFiles != null) { // listFiles may return null if there's an IO error
291 for(File f : childFiles) {
292 deleteDirectoryOnExit(f);
293 }
294 }
295 }
296 }
297
298 /**
299 * Copy the the file or directory src to dst.
300 * @param src source file
301 * @param dst destination
302 * @throws IOException
303 */
304 public static void copyFile(final File src, final File dst) throws IOException {
305 if (src.isFile()) {
306 InputStream in = new FileInputStream(src);
307 OutputStream out = new FileOutputStream(dst);
308
309 // Transfer bytes from in to out
310 byte[] buf = new byte[1024];
311 int len;
312 while ((len = in.read(buf)) > 0) {
313 out.write(buf, 0, len);
314 }
315 in.close();
316 out.close();
317 }
318 else if (src.isDirectory()) {
319 dst.mkdirs();
320 for(File f: src.listFiles()) {
321 copyFile(f, new File(dst, f.getName()));
322 }
323 }
324 }
325
326 /**
327 * Enumerate the files in src.
328 * @param src source file
329 * @return list of files in src
330 * @throws IOException
331 */
332 public static Set<File> enumFiles(final File src) throws IOException {
333 Set<File> l = new HashSet<File>();
334 if (src.isFile()) {
335 l.add(src);
336 }
337 else if (src.isDirectory()) {
338 for(File f: src.listFiles()) {
339 Set<File> recur = enumFiles(f);
340 l.addAll(recur);
341 }
342 }
343 return l;
344 }
345
346 /** Convert all path entries in a path string to absolute paths. The delimiter in the path string is the
347 * "path.separator" property. Empty entries are equivalent to "." and will thus are converted to the
348 * "user.dir" (working directory).
349 * Example:
350 * ".:drjava::/home/foo/junit.jar" with "user.dir" set to "/home/foo/bar" will be converted to
351 * "/home/foo/bar:/home/foo/bar/drjava:/home/foo/bar:/home/foo/junit.jar".
352 *
353 * @param path path string with entries to convert
354 * @return path string with all entries as absolute paths
355 */
356 public static String convertToAbsolutePathEntries(String path) {
357 String pathSep = System.getProperty("path.separator");
358
359 // split leaves off trailing empty strings
360 // (see API javadocs: "Trailing empty strings are therefore not included in the resulting array.")
361 // we therefore append one element at the end and later remove it
362 path += pathSep + "x";
363
364 // now path ends with ":x", so we'll have an additional "x" element in the pathEntries array
365
366 // split up the path into individual entries, turn all of the entries
367 // into absolute paths, and put the path back together
368 // EXCEPT for the last item in the array, because that's the "x" we added
369 String[] pathEntries = path.split(pathSep);
370 final StringBuilder sb = new StringBuilder();
371 for(int i=0; i<pathEntries.length-1; ++i) { // length-1 to ignore the last element
372 File f = new File(pathEntries[i]);
373 sb.append(f.getAbsolutePath());
374 sb.append(pathSep);
375 }
376 String reconstructedPath = sb.toString();
377
378 // if the reconstructed path is non-empty, then it will have an extra
379 // path separator at the end; take it off
380 if (reconstructedPath.length()!=0) {
381 reconstructedPath = reconstructedPath.substring(0, reconstructedPath.length()-1);
382 }
383
384 return reconstructedPath;
385 }
386
387 /**
388 * Unpack all files in the jar file to the specified directory.
389 *
390 * @param inJar the input Jar
391 * @param outDir the output directory
392 * @return number of files unpacked
393 * @throws IOException
394 */
395 public static long unpackJar(File inJar, File outDir) throws IOException {
396 JarFile jf = null;
397 long count = 0;
398 try {
399 jf = new JarFile(inJar);
400
401 Enumeration<JarEntry> entries = jf.entries();
402 while(entries.hasMoreElements()) {
403 JarEntry e = entries.nextElement();
404 if (!e.isDirectory()) {
405 InputStream is = null;
406 try {
407 is = jf.getInputStream(e);
408 FileOutputStream fos = null;
409 try {
410 String name = e.getName().replace('/', File.separatorChar);
411
412 // create directories
413 int pos = name.lastIndexOf(File.separatorChar);
414 if (pos>=0) {
415 String dir = name.substring(0,pos);
416 (new File(outDir, dir)).mkdirs();
417 }
418
419 // write file
420 fos = new FileOutputStream(new File(outDir, name));
421 byte[] buffer = new byte[1024];
422 int bytesRead;
423 while(-1 != (bytesRead = is.read(buffer))) {
424 fos.write(buffer, 0, bytesRead);
425 }
426 ++count;
427 }
428 finally {
429 if (fos!=null) { fos.close(); fos = null; }
430 }
431 }
432 finally {
433 if (is!=null) { is.close(); is = null; }
434 }
435 }
436 }
437 }
438 finally {
439 if (jf!=null) { jf.close(); jf = null; }
440 }
441 return count;
442 }
443
444 /**
445 * Pack all files in the directory into the specified Jar.
446 *
447 * @param inDir the input directory
448 * @param outJar the output Jar
449 * @return number of files packed
450 * @throws IOException
451 */
452 public static long packJar(File inDir, File outJar) throws IOException {
453 FileOutputStream fos = new FileOutputStream(outJar);
454 JarOutputStream jos = new JarOutputStream(fos);
455 long count = 0;
456
457 Set<File> files = enumFiles(inDir);
458 for(File f: files) {
459 File fRel = makeRelativeTo(f, inDir);
460 FileInputStream fis = null;
461 try {
462 fis = new FileInputStream(f);
463 jos.putNextEntry(new ZipEntry(fRel.getPath().replace(File.separatorChar,'/')));
464 byte[] buffer = new byte[1024];
465 int bytesRead;
466 while(-1 != (bytesRead = fis.read(buffer))) {
467 jos.write(buffer, 0, bytesRead);
468 }
469 ++count;
470 }
471 finally {
472 if (fis!=null) { fis.close(); fis = null; }
473 }
474 }
475
476 jos.close();
477 fos.close();
478
479 return count;
480 }
481
482 public static class FileOpsTest extends TestCase {
483 public void testSplitPaths() {
484 String[] p;
485 final String fs = File.separator;
486 p = splitPaths("C:"+ fs +"foo:C:"+ fs +"bar::C:"+ fs +"foo"+ fs +"bar:", ':', true);
487 assertEquals(5, p.length);
488 assertEquals("C:"+ fs +"foo", p[0]);
489 assertEquals("C:"+ fs +"bar", p[1]);
490 assertEquals("", p[2]);
491 assertEquals("C:"+ fs +"foo"+ fs +"bar", p[3]);
492 assertEquals("", p[4]);
493 p = splitPaths("C:foo:C:bar::C:foo"+ fs +"bar:", ':', true);
494 assertEquals(8, p.length);
495 assertEquals("C", p[0]);
496 assertEquals("foo", p[1]);
497 assertEquals("C", p[2]);
498 assertEquals("bar", p[3]);
499 assertEquals("", p[4]);
500 assertEquals("C", p[5]);
501 assertEquals("foo"+ fs +"bar", p[6]);
502 assertEquals("", p[7]);
503 p = splitPaths("", ':', true);
504 assertEquals(1, p.length);
505 assertEquals("", p[0]);
506 p = splitPaths("C:"+ fs +"foo;C:"+ fs +"bar;;C:"+ fs +"foo"+ fs +"bar;", ';', true);
507 assertEquals(5, p.length);
508 assertEquals("C:"+ fs +"foo", p[0]);
509 assertEquals("C:"+ fs +"bar", p[1]);
510 assertEquals("", p[2]);
511 assertEquals("C:"+ fs +"foo"+ fs +"bar", p[3]);
512 assertEquals("", p[4]);
513 p = splitPaths("C:foo;C:bar;;C:foo"+ fs +"bar;", ';', true);
514 assertEquals(5, p.length);
515 assertEquals("C:foo", p[0]);
516 assertEquals("C:bar", p[1]);
517 assertEquals("", p[2]);
518 assertEquals("C:foo"+ fs +"bar", p[3]);
519 assertEquals("", p[4]);
520 p = splitPaths("", ':', true);
521 assertEquals(1, p.length);
522 assertEquals("", p[0]);
523 p = splitPaths("C;foo;C;bar;;C;foo/bar;", ';', true);
524 assertEquals(8, p.length);
525 assertEquals("C", p[0]);
526 assertEquals("foo", p[1]);
527 assertEquals("C", p[2]);
528 assertEquals("bar", p[3]);
529 assertEquals("", p[4]);
530 assertEquals("C", p[5]);
531 assertEquals("foo/bar", p[6]);
532 assertEquals("", p[7]);
533
534 p = splitPaths("C:\\foo:C:\\bar::C:\\foo\\bar:", ':', false);
535 assertEquals(8, p.length);
536 assertEquals("C", p[0]);
537 assertEquals("\\foo", p[1]);
538 assertEquals("C", p[2]);
539 assertEquals("\\bar", p[3]);
540 assertEquals("", p[4]);
541 assertEquals("C", p[5]);
542 assertEquals("\\foo\\bar", p[6]);
543 assertEquals("", p[7]);
544 p = splitPaths("C:foo:C:bar::C:foo\\bar:", ':', false);
545 assertEquals(8, p.length);
546 assertEquals("C", p[0]);
547 assertEquals("foo", p[1]);
548 assertEquals("C", p[2]);
549 assertEquals("bar", p[3]);
550 assertEquals("", p[4]);
551 assertEquals("C", p[5]);
552 assertEquals("foo\\bar", p[6]);
553 assertEquals("", p[7]);
554 p = splitPaths("", ':', true);
555 assertEquals(1, p.length);
556 assertEquals("", p[0]);
557 p = splitPaths("C:/foo:C:/bar::C:/foo/bar:", ':', false);
558 assertEquals(8, p.length);
559 assertEquals("C", p[0]);
560 assertEquals("/foo", p[1]);
561 assertEquals("C", p[2]);
562 assertEquals("/bar", p[3]);
563 assertEquals("", p[4]);
564 assertEquals("C", p[5]);
565 assertEquals("/foo/bar", p[6]);
566 assertEquals("", p[7]);
567 p = splitPaths("C:foo:C:bar::C:foo/bar:", ':', false);
568 assertEquals(8, p.length);
569 assertEquals("C", p[0]);
570 assertEquals("foo", p[1]);
571 assertEquals("C", p[2]);
572 assertEquals("bar", p[3]);
573 assertEquals("", p[4]);
574 assertEquals("C", p[5]);
575 assertEquals("foo/bar", p[6]);
576 assertEquals("", p[7]);
577 p = splitPaths("C:\\foo;C:\\bar;;C:\\foo\\bar;", ';', false);
578 assertEquals(5, p.length);
579 assertEquals("C:\\foo", p[0]);
580 assertEquals("C:\\bar", p[1]);
581 assertEquals("", p[2]);
582 assertEquals("C:\\foo\\bar", p[3]);
583 assertEquals("", p[4]);
584 p = splitPaths("C:foo;C:bar;;C:foo\\bar;", ';', false);
585 assertEquals(5, p.length);
586 assertEquals("C:foo", p[0]);
587 assertEquals("C:bar", p[1]);
588 assertEquals("", p[2]);
589 assertEquals("C:foo\\bar", p[3]);
590 assertEquals("", p[4]);
591 p = splitPaths("", ':', true);
592 assertEquals(1, p.length);
593 assertEquals("", p[0]);
594 p = splitPaths("C:/foo;C:/bar;;C:/foo/bar;", ';', false);
595 assertEquals(5, p.length);
596 assertEquals("C:/foo", p[0]);
597 assertEquals("C:/bar", p[1]);
598 assertEquals("", p[2]);
599 assertEquals("C:/foo/bar", p[3]);
600 assertEquals("", p[4]);
601 p = splitPaths("C;foo;C;bar;;C;foo/bar;", ';', false);
602 assertEquals(8, p.length);
603 assertEquals("C", p[0]);
604 assertEquals("foo", p[1]);
605 assertEquals("C", p[2]);
606 assertEquals("bar", p[3]);
607 assertEquals("", p[4]);
608 assertEquals("C", p[5]);
609 assertEquals("foo/bar", p[6]);
610 assertEquals("", p[7]);
611 }
612 }
613 }