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