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         * {&quot;&quot;,&quot;home&quot;,&quot;username&quot;,&quot;txt.txt&quot;}. 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    }