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