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>++/.svn/++</code> and <code>++/.git/++</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}