001/**
002 * Copyright 2010-2013 The Kuali Foundation
003 *
004 * Licensed under the Educational Community License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.opensource.org/licenses/ecl2.php
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.kuali.common.util;
017
018import java.io.File;
019import java.io.IOException;
020import java.util.ArrayList;
021import java.util.Arrays;
022import java.util.Collections;
023import java.util.HashSet;
024import java.util.List;
025import java.util.Set;
026
027import org.apache.commons.io.FileUtils;
028import org.apache.commons.lang3.StringUtils;
029import org.kuali.common.util.base.Threads;
030import org.kuali.common.util.execute.CopyFileRequest;
031import org.kuali.common.util.execute.CopyFileResult;
032import org.kuali.common.util.file.DirDiff;
033import org.kuali.common.util.file.DirRequest;
034import org.kuali.common.util.file.MD5Result;
035import org.slf4j.Logger;
036import org.slf4j.LoggerFactory;
037
038public class FileSystemUtils {
039
040        private static final Logger logger = LoggerFactory.getLogger(FileSystemUtils.class);
041
042        public static final String RECURSIVE_FILE_INCLUDE_PATTERN = "**/**";
043        public static final List<String> DEFAULT_RECURSIVE_INCLUDES = Arrays.asList(RECURSIVE_FILE_INCLUDE_PATTERN);
044
045        private static final String SVN_PATTERN = "**/.svn/**";
046        private static final String GIT_PATTERN = "**/.git/**";
047        public static final List<String> DEFAULT_SCM_IGNORE_PATTERNS = Arrays.asList(SVN_PATTERN, GIT_PATTERN);
048
049        /**
050         * Return a recursive listing of all files in the directory ignoring <code>&#43;&#43;/.svn/&#43;&#43;</code> and <code>&#43;&#43;/.git/&#43;&#43;</code>
051         */
052        public static List<File> getAllNonScmFiles(File dir) {
053                return getAllNonScmFiles(dir, DEFAULT_SCM_IGNORE_PATTERNS);
054        }
055
056        /**
057         * Return a recursive listing of all files in the directory ignoring files that match <code>scmIgnorePatterns</code>
058         */
059        public static List<File> getAllNonScmFiles(File dir, List<String> scmIgnorePatterns) {
060                SimpleScanner scanner = new SimpleScanner(dir, DEFAULT_RECURSIVE_INCLUDES, scmIgnorePatterns);
061                return scanner.getFiles();
062        }
063
064        /**
065         * This method recursively copies one file system directory to another directory under the control of SCM. Before doing so, it records 3 types of files:
066         * 
067         * <pre>
068         *  1 - both     - files that exist in both directories 
069         *  2 - dir1Only - files that exist in the source directory but not the SCM directory
070         *  3 - dir2Only - files that exist in the SCM directory but not the source directory
071         * </pre>
072         * 
073         * This provides enough information for SCM tooling to then complete the work of making the SCM directory exactly match the file system directory and commit any changes to the
074         * SCM system.
075         */
076        @Deprecated
077        public static DirectoryDiff prepareScmDir(PrepareScmDirRequest request) {
078                return prepareScmDir(request, null, false);
079        }
080
081        /**
082         * This method recursively copies one file system directory to another directory under the control of SCM. Before doing so, it records 3 types of files:
083         * 
084         * <pre>
085         *  1 - both     - files that exist in both directories 
086         *  2 - dir1Only - files that exist in the source directory but not the SCM directory
087         *  3 - dir2Only - files that exist in the SCM directory but not the source directory
088         * </pre>
089         * 
090         * This provides enough information for SCM tooling to then complete the work of making the SCM directory exactly match the file system directory and commit any changes to the
091         * SCM system.
092         * 
093         * @deprecated
094         */
095        @Deprecated
096        public static DirectoryDiff prepareScmDir(PrepareScmDirRequest request, File relativeDir, boolean diffOnly) {
097
098                // Make sure we are configured correctly
099                Assert.notNull(request, "request is null");
100                Assert.notNull(request.getSrcDir(), "srcDir is null");
101                Assert.notNull(request.getScmDir(), "scmDir is null");
102
103                // Both must already exist and must be directories (can't be a regular file)
104                Assert.isExistingDir(request.getSrcDir(), "srcDir is not an existing directory");
105                Assert.isExistingDir(request.getScmDir(), "scmDir is not an existing directory");
106
107                // Setup a diff request
108                DirectoryDiffRequest diffRequest = new DirectoryDiffRequest();
109                diffRequest.setDir1(request.getSrcDir());
110                diffRequest.setDir2(request.getScmDir());
111                diffRequest.setExcludes(request.getScmIgnorePatterns());
112
113                // Record the differences between the two directories
114                DirectoryDiff diff = getDiff(diffRequest);
115
116                // Copy files from the source directory to the SCM directory
117                if (!diffOnly) {
118                        org.kuali.common.util.execute.CopyFilePatternsExecutable exec = new org.kuali.common.util.execute.CopyFilePatternsExecutable();
119                        exec.setSrcDir(request.getSrcDir());
120                        exec.setDstDir(request.getScmDir());
121                        exec.setExcludes(request.getScmIgnorePatterns());
122                        exec.setRelativeDir(relativeDir);
123                        exec.execute();
124                }
125
126                // Return the diff so we'll know what SCM needs to add/delete from its directory
127                return diff;
128        }
129
130        public static List<File> getFiles(File dir, List<String> includes, List<String> excludes) {
131                SimpleScanner scanner = new SimpleScanner(dir, includes, excludes);
132                return scanner.getFiles();
133        }
134
135        @Deprecated
136        public static DirectoryDiff getDiff(File dir1, File dir2, List<String> includes, List<String> excludes) {
137                DirectoryDiffRequest request = new DirectoryDiffRequest();
138                request.setDir1(dir1);
139                request.setDir2(dir2);
140                request.setIncludes(includes);
141                request.setExcludes(excludes);
142                return getDiff(request);
143        }
144
145        /**
146         * Compare 2 directories on the file system and return an object containing the results. All of the files contained in either of the 2 directories get aggregated into 5
147         * categories.
148         * 
149         * <pre>
150         * 1 - Both            - Files that exist in both directories
151         * 2 - Different       - Files that exist in both directories but who's MD5 checksums do not match 
152         * 3 - Identical       - Files that exist in both directories with matching MD5 checksums 
153         * 4 - Source Dir Only - Files that exist only in the source directory
154         * 5 - Target Dir Only - Files that exist only in the target directory
155         * </pre>
156         * 
157         * The 5 lists in <code>DirDiff</code> contain the relative paths to files for each category.
158         */
159        public static DirDiff getMD5Diff(DirRequest request) {
160
161                // Do a quick diff (just figures out what files are unique to each directory vs files that are in both)
162                DirDiff diff = getQuickDiff(request);
163
164                // Do a deep diff
165                // This computes MD5 checksums for any files present in both directories
166                fillInMD5Results(diff);
167
168                // return the diff result
169                return diff;
170        }
171
172        public static List<MD5Result> getMD5Results(List<File> sources, List<File> targets) {
173                Assert.isTrue(sources.size() == targets.size(), "lists are not the same size");
174                List<MD5Result> results = new ArrayList<MD5Result>();
175                for (int i = 0; i < sources.size(); i++) {
176                        File source = sources.get(i);
177                        File target = targets.get(i);
178                        MD5Result md5Result = getMD5Result(source, target);
179                        results.add(md5Result);
180                }
181                return results;
182        }
183
184        protected static void fillInMD5Results(DirDiff diff) {
185                List<File> sources = getFullPaths(diff.getSourceDir(), diff.getBoth());
186                List<File> targets = getFullPaths(diff.getTargetDir(), diff.getBoth());
187
188                List<MD5Result> results = getMD5Results(sources, targets);
189
190                List<MD5Result> different = new ArrayList<MD5Result>();
191                List<MD5Result> identical = new ArrayList<MD5Result>();
192                for (MD5Result md5Result : results) {
193                        String sourceChecksum = md5Result.getSourceChecksum();
194                        String targetChecksum = md5Result.getTargetChecksum();
195                        Assert.notNull(sourceChecksum, "sourceChecksum is null");
196                        Assert.notNull(targetChecksum, "targetChecksum is null");
197                        if (StringUtils.equals(sourceChecksum, targetChecksum)) {
198                                identical.add(md5Result);
199                        } else {
200                                different.add(md5Result);
201                        }
202                }
203
204                //
205                diff.setDifferent(different);
206                diff.setIdentical(identical);
207        }
208
209        public static MD5Result getMD5Result(File source, File target) {
210
211                String sourceChecksum = LocationUtils.getMD5Checksum(source);
212                String targetChecksum = LocationUtils.getMD5Checksum(target);
213
214                return new MD5Result(source, sourceChecksum, target, targetChecksum);
215        }
216
217        /**
218         * Compare 2 directories on the file system and return an object containing the results. All of the files contained in either of the 2 directories get placed into one of 3
219         * categories.
220         * 
221         * <pre>
222         * 1 - Both       - Files that exist in both directories
223         * 2 - Dir 1 Only - Files that exist only in directory 1
224         * 3 - Dir 2 Only - Files that exist only in directory 2
225         * </pre>
226         * 
227         * The 3 lists in <code>DirectoryDiff</code> contain the relative paths to files for each category.
228         */
229        @Deprecated
230        public static DirectoryDiff getDiff(DirectoryDiffRequest request) {
231                DirRequest newRequest = new DirRequest();
232                newRequest.setExcludes(request.getExcludes());
233                newRequest.setIncludes(request.getIncludes());
234                newRequest.setSourceDir(request.getDir1());
235                newRequest.setTargetDir(request.getDir2());
236                DirDiff diff = getQuickDiff(newRequest);
237
238                DirectoryDiff dd = new DirectoryDiff(diff.getSourceDir(), diff.getTargetDir());
239                dd.setBoth(diff.getBoth());
240                dd.setDir1Only(diff.getSourceDirOnly());
241                dd.setDir2Only(diff.getTargetDirOnly());
242                return dd;
243        }
244
245        public static DirDiff getQuickDiff(DirRequest request) {
246
247                // Get a listing of files from both directories using the exact same includes/excludes
248                List<File> sourceFiles = getFiles(request.getSourceDir(), request.getIncludes(), request.getExcludes());
249                List<File> targetFiles = getFiles(request.getTargetDir(), request.getIncludes(), request.getExcludes());
250
251                // Get the unique set of paths for each file relative to their parent directory
252                Set<String> sourcePaths = new HashSet<String>(getRelativePaths(request.getSourceDir(), sourceFiles));
253                Set<String> targetPaths = new HashSet<String>(getRelativePaths(request.getTargetDir(), targetFiles));
254
255                // Paths that exist in both directories
256                Set<String> both = SetUtils.intersection(sourcePaths, targetPaths);
257
258                // Paths that exist in source but not target
259                Set<String> sourceOnly = SetUtils.difference(sourcePaths, targetPaths);
260
261                // Paths that exist in target but not source
262                Set<String> targetOnly = SetUtils.difference(targetPaths, sourcePaths);
263
264                logger.debug("source={}, sourceOnly.size()={}", request.getSourceDir(), sourceOnly.size());
265                logger.debug("target={}, targetOnly.size()={}", request.getTargetDir(), targetOnly.size());
266
267                // Store the information we've collected into a result object
268                DirDiff result = new DirDiff(request.getSourceDir(), request.getTargetDir());
269
270                // Store the relative paths on the diff object
271                result.setBoth(new ArrayList<String>(both));
272                result.setSourceDirOnly(new ArrayList<String>(sourceOnly));
273                result.setTargetDirOnly(new ArrayList<String>(targetOnly));
274
275                // Sort the relative paths
276                Collections.sort(result.getBoth());
277                Collections.sort(result.getSourceDirOnly());
278                Collections.sort(result.getTargetDirOnly());
279
280                // return the diff
281                return result;
282        }
283
284        /**
285         * Examine the contents of a text file, stopping as soon as it contains <code>token</code>, or <code>timeout</code> is exceeded, whichever comes first.
286         */
287        public static MonitorTextFileResult monitorTextFile(File file, String token, int intervalMillis, int timeoutMillis, String encoding) {
288
289                // Make sure we are configured correctly
290                Assert.notNull(file, "file is null");
291                Assert.hasText(token, "token has no text");
292                Assert.hasText(encoding, "encoding has no text");
293                Assert.isTrue(intervalMillis > 0, "interval must be a positive integer");
294                Assert.isTrue(timeoutMillis > 0, "timeout must be a positive integer");
295
296                // Setup some member variables to record what happens
297                long start = System.currentTimeMillis();
298                long stop = start + timeoutMillis;
299                boolean exists = false;
300                boolean contains = false;
301                boolean timeoutExceeded = false;
302                long now = -1;
303                String content = null;
304
305                // loop until timeout is exceeded or we find the token inside the file
306                for (;;) {
307
308                        // Always pause (unless this is the first iteration)
309                        if (now != -1) {
310                                Threads.sleep(intervalMillis);
311                        }
312
313                        // Check to make sure we haven't exceeded our timeout limit
314                        now = System.currentTimeMillis();
315                        if (now > stop) {
316                                timeoutExceeded = true;
317                                break;
318                        }
319
320                        // If the file does not exist, no point in going any further
321                        exists = LocationUtils.exists(file);
322                        if (!exists) {
323                                continue;
324                        }
325
326                        // The file exists, check to see if the token we are looking for is present in the file
327                        content = LocationUtils.toString(file, encoding);
328                        contains = StringUtils.contains(content, token);
329                        if (contains) {
330                                // We found what we are looking for, we are done
331                                break;
332                        }
333                }
334
335                // Record how long the overall process took
336                long elapsed = now - start;
337
338                // Fill in a pojo detailing what happened
339                MonitorTextFileResult mtfr = new MonitorTextFileResult(exists, contains, timeoutExceeded, elapsed);
340                mtfr.setAbsolutePath(LocationUtils.getCanonicalPath(file));
341                mtfr.setContent(content);
342                return mtfr;
343        }
344
345        public static List<SyncResult> syncFiles(List<SyncRequest> requests) throws IOException {
346                List<SyncResult> results = new ArrayList<SyncResult>();
347                for (SyncRequest request : requests) {
348                        SyncResult result = syncFiles(request);
349                        results.add(result);
350                }
351                return results;
352        }
353
354        public static SyncResult syncFilesQuietly(SyncRequest request) {
355                try {
356                        return syncFiles(request);
357                } catch (IOException e) {
358                        throw new IllegalStateException("Unexpected IO error");
359                }
360        }
361
362        public static SyncResult syncFiles(SyncRequest request) throws IOException {
363                logger.info("Sync [{}] -> [{}]", request.getSrcDir(), request.getDstDir());
364                List<File> dstFiles = getAllNonScmFiles(request.getDstDir());
365                List<File> srcFiles = request.getSrcFiles();
366
367                List<String> dstPaths = getRelativePaths(request.getDstDir(), dstFiles);
368                List<String> srcPaths = getRelativePaths(request.getSrcDir(), srcFiles);
369
370                List<String> adds = new ArrayList<String>();
371                List<String> updates = new ArrayList<String>();
372                List<String> deletes = new ArrayList<String>();
373
374                for (String srcPath : srcPaths) {
375                        boolean existing = dstPaths.contains(srcPath);
376                        if (existing) {
377                                updates.add(srcPath);
378                        } else {
379                                adds.add(srcPath);
380                        }
381                }
382                for (String dstPath : dstPaths) {
383                        boolean extra = !srcPaths.contains(dstPath);
384                        if (extra) {
385                                deletes.add(dstPath);
386                        }
387                }
388
389                copyFiles(request.getSrcDir(), request.getSrcFiles(), request.getDstDir());
390
391                SyncResult result = new SyncResult();
392                result.setAdds(getFullPaths(request.getDstDir(), adds));
393                result.setUpdates(getFullPaths(request.getDstDir(), updates));
394                result.setDeletes(getFullPaths(request.getDstDir(), deletes));
395                return result;
396        }
397
398        protected static void copyFiles(File srcDir, List<File> files, File dstDir) throws IOException {
399                for (File file : files) {
400                        String relativePath = getRelativePath(srcDir, file);
401                        File dstFile = new File(dstDir, relativePath);
402                        FileUtils.copyFile(file, dstFile);
403                }
404        }
405
406        public static List<File> getFullPaths(File dir, Set<String> relativePaths) {
407                return getFullPaths(dir, new ArrayList<String>(relativePaths));
408        }
409
410        public static List<File> getSortedFullPaths(File dir, List<String> relativePaths) {
411                List<File> files = getFullPaths(dir, relativePaths);
412                Collections.sort(files);
413                return files;
414        }
415
416        public static List<File> getFullPaths(File dir, List<String> relativePaths) {
417                List<File> files = new ArrayList<File>();
418                for (String relativePath : relativePaths) {
419                        File file = new File(dir, relativePath);
420                        File canonical = new File(LocationUtils.getCanonicalPath(file));
421                        files.add(canonical);
422                }
423                return files;
424        }
425
426        protected static List<String> getRelativePaths(File dir, List<File> files) {
427                List<String> relativePaths = new ArrayList<String>();
428                for (File file : files) {
429                        String relativePath = getRelativePath(dir, file);
430                        relativePaths.add(relativePath);
431                }
432                return relativePaths;
433        }
434
435        /**
436         * Return true if child lives on the file system somewhere underneath parent, false otherwise.
437         */
438        public static boolean isParent(File parent, File child) {
439                if (parent == null || child == null) {
440                        return false;
441                }
442
443                String parentPath = LocationUtils.getCanonicalPath(parent);
444                String childPath = LocationUtils.getCanonicalPath(child);
445
446                if (StringUtils.equals(parentPath, childPath)) {
447                        return false;
448                } else {
449                        return StringUtils.contains(childPath, parentPath);
450                }
451        }
452
453        /**
454         * Return the relative path to <code>file</code> from <code>parentDir</code>. <code>parentDir</code> is optional and can be <code>null</code>. If <code>parentDir</code> is not
455         * supplied (or is not a parent directory to <code>file</code> the canonical path to <code>file</code> is returned.
456         */
457        public static String getRelativePathQuietly(File parentDir, File file) {
458                Assert.notNull(file, "file is null");
459                if (isParent(parentDir, file)) {
460                        return getRelativePath(parentDir, file);
461                } else {
462                        return LocationUtils.getCanonicalPath(file);
463                }
464        }
465
466        public static String getRelativePath(File dir, File file) {
467                String dirPath = LocationUtils.getCanonicalPath(dir);
468                String filePath = LocationUtils.getCanonicalPath(file);
469                if (!StringUtils.contains(filePath, dirPath)) {
470                        throw new IllegalArgumentException(file + " does not reside under " + dir);
471                }
472                return StringUtils.remove(filePath, dirPath);
473        }
474
475        public static List<CopyFileRequest> getCopyFileRequests(File srcDir, List<String> includes, List<String> excludes, File dstDir) {
476                SimpleScanner scanner = new SimpleScanner(srcDir, includes, excludes);
477                List<File> srcFiles = scanner.getFiles();
478
479                List<CopyFileRequest> requests = new ArrayList<CopyFileRequest>();
480                for (File srcFile : srcFiles) {
481                        String relativePath = FileSystemUtils.getRelativePath(srcDir, srcFile);
482                        File dstFile = new File(dstDir, relativePath);
483                        CopyFileRequest request = new CopyFileRequest(srcFile, dstFile);
484                        requests.add(request);
485                }
486                return requests;
487        }
488
489        public static CopyFileResult copyFile(File src, File dst) {
490                try {
491                        long start = System.currentTimeMillis();
492                        boolean overwritten = dst.exists();
493                        FileUtils.copyFile(src, dst);
494                        return new CopyFileResult(src, dst, overwritten, System.currentTimeMillis() - start);
495                } catch (IOException e) {
496                        throw new IllegalStateException("Unexpected IO error", e);
497                }
498        }
499
500        public static List<CopyFileResult> copyFiles(List<CopyFileRequest> requests) {
501                List<CopyFileResult> results = new ArrayList<CopyFileResult>();
502                for (CopyFileRequest request : requests) {
503                        CopyFileResult result = copyFile(request.getSource(), request.getDestination());
504                        results.add(result);
505                }
506                return results;
507        }
508
509}