View Javadoc
1   /**
2    * Copyright 2010-2013 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.common.util;
17  
18  import java.io.File;
19  import java.io.IOException;
20  import java.util.ArrayList;
21  import java.util.Arrays;
22  import java.util.Collections;
23  import java.util.HashSet;
24  import java.util.List;
25  import java.util.Set;
26  
27  import org.apache.commons.io.FileUtils;
28  import org.apache.commons.lang3.StringUtils;
29  import org.kuali.common.util.base.Threads;
30  import org.kuali.common.util.execute.CopyFileRequest;
31  import org.kuali.common.util.execute.CopyFileResult;
32  import org.kuali.common.util.file.DirDiff;
33  import org.kuali.common.util.file.DirRequest;
34  import org.kuali.common.util.file.MD5Result;
35  import org.slf4j.Logger;
36  import org.slf4j.LoggerFactory;
37  
38  public class FileSystemUtils {
39  
40  	private static final Logger logger = LoggerFactory.getLogger(FileSystemUtils.class);
41  
42  	public static final String RECURSIVE_FILE_INCLUDE_PATTERN = "**/**";
43  	public static final List<String> DEFAULT_RECURSIVE_INCLUDES = Arrays.asList(RECURSIVE_FILE_INCLUDE_PATTERN);
44  
45  	private static final String SVN_PATTERN = "**/.svn/**";
46  	private static final String GIT_PATTERN = "**/.git/**";
47  	public static final List<String> DEFAULT_SCM_IGNORE_PATTERNS = Arrays.asList(SVN_PATTERN, GIT_PATTERN);
48  
49  	/**
50  	 * 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>
51  	 */
52  	public static List<File> getAllNonScmFiles(File dir) {
53  		return getAllNonScmFiles(dir, DEFAULT_SCM_IGNORE_PATTERNS);
54  	}
55  
56  	/**
57  	 * Return a recursive listing of all files in the directory ignoring files that match <code>scmIgnorePatterns</code>
58  	 */
59  	public static List<File> getAllNonScmFiles(File dir, List<String> scmIgnorePatterns) {
60  		SimpleScanner scanner = new SimpleScanner(dir, DEFAULT_RECURSIVE_INCLUDES, scmIgnorePatterns);
61  		return scanner.getFiles();
62  	}
63  
64  	/**
65  	 * 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:
66  	 * 
67  	 * <pre>
68  	 *  1 - both     - files that exist in both directories 
69  	 *  2 - dir1Only - files that exist in the source directory but not the SCM directory
70  	 *  3 - dir2Only - files that exist in the SCM directory but not the source directory
71  	 * </pre>
72  	 * 
73  	 * 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
74  	 * SCM system.
75  	 */
76  	@Deprecated
77  	public static DirectoryDiff prepareScmDir(PrepareScmDirRequest request) {
78  		return prepareScmDir(request, null, false);
79  	}
80  
81  	/**
82  	 * 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:
83  	 * 
84  	 * <pre>
85  	 *  1 - both     - files that exist in both directories 
86  	 *  2 - dir1Only - files that exist in the source directory but not the SCM directory
87  	 *  3 - dir2Only - files that exist in the SCM directory but not the source directory
88  	 * </pre>
89  	 * 
90  	 * 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
91  	 * SCM system.
92  	 * 
93  	 * @deprecated
94  	 */
95  	@Deprecated
96  	public static DirectoryDiff prepareScmDir(PrepareScmDirRequest request, File relativeDir, boolean diffOnly) {
97  
98  		// Make sure we are configured correctly
99  		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 }