001package co.codewizards.cloudstore.core.util; 002 003import static co.codewizards.cloudstore.core.util.Util.*; 004 005import java.io.ByteArrayOutputStream; 006import java.io.File; 007import java.io.FileFilter; 008import java.io.FileInputStream; 009import java.io.FileNotFoundException; 010import java.io.FileOutputStream; 011import java.io.IOException; 012import java.io.InputStream; 013import java.io.InputStreamReader; 014import java.io.OutputStream; 015import java.io.OutputStreamWriter; 016import java.io.Reader; 017import java.io.StreamTokenizer; 018import java.io.StringReader; 019import java.io.StringWriter; 020import java.io.UnsupportedEncodingException; 021import java.io.Writer; 022import java.net.URLEncoder; 023import java.nio.charset.Charset; 024import java.nio.file.Files; 025import java.nio.file.LinkOption; 026import java.nio.file.Path; 027import java.nio.file.attribute.BasicFileAttributeView; 028import java.nio.file.attribute.BasicFileAttributes; 029import java.nio.file.attribute.FileTime; 030import java.text.SimpleDateFormat; 031import java.util.ArrayList; 032import java.util.Date; 033import java.util.LinkedList; 034import java.util.List; 035import java.util.Map; 036import java.util.Properties; 037import java.util.StringTokenizer; 038 039import org.slf4j.Logger; 040import org.slf4j.LoggerFactory; 041 042import co.codewizards.cloudstore.core.childprocess.DumpStreamThread; 043import co.codewizards.cloudstore.core.progress.ProgressMonitor; 044 045public final class IOUtil { 046 /** 047 * UTF-8 caracter set name. 048 */ 049 public static final String CHARSET_NAME_UTF_8 = "UTF-8"; 050 051 /** 052 * UTF-8 caracter set. 053 */ 054 public static final Charset CHARSET_UTF_8 = Charset.forName(CHARSET_NAME_UTF_8); 055 056 /** 057 * 1 GB in bytes. 058 * This holds the result of the calculation 1 * 1024 * 1024 * 1024 059 */ 060 public static final long GIGABYTE = 1 * 1024 * 1024 * 1024; 061 062 public static final String COLLISION_FILE_NAME_INFIX = ".collision"; 063 064 private static File tempDir = null; 065 066 private static final Logger logger = LoggerFactory.getLogger(IOUtil.class); 067 068 private IOUtil() { } 069 070 /** 071 * This method finds - if possible - a relative path for addressing 072 * the given <code>file</code> from the given <code>baseDir</code>. 073 * <p> 074 * If it is not possible to denote <code>file</code> relative to <code>baseDir</code>, 075 * the absolute path of <code>file</code> will be returned. 076 * </p> 077 * <p> 078 * Examples: 079 * <ul> 080 * <li> 081 * <code>baseDir="/home/marco"</code><br/> 082 * <code>file="temp/jfire/jboss/bin/run.sh"</code><br/> 083 * or<br/> 084 * <code>file="/home/marco/temp/jfire/jboss/bin/run.sh"</code><br/> 085 * <code>result="temp/jfire/jboss/bin/run.sh"</code> 086 * </li> 087 * <li> 088 * <code>baseDir="/home/marco/workspace.jfire/JFireBase"</code><br/> 089 * <code>file="/home/marco/temp/jfire/jboss/bin/run.sh"</code><br/> 090 * or<br/> 091 * <code>file="../../temp/jfire/jboss/bin/run.sh"</code><br/> 092 * <code>result="../../temp/jfire/jboss/bin/run.sh"</code> 093 * </li> 094 * <li> 095 * <code>baseDir="/tmp/workspace.jfire/JFireBase"</code><br/> 096 * <code>file="/home/marco/temp/jfire/jboss/bin/run.sh"</code><br/> 097 * <code>result="/home/marco/temp/jfire/jboss/bin/run.sh"</code> (absolute, because relative is not possible) 098 * </li> 099 * </ul> 100 * </p> 101 * 102 * @param baseDir This directory is used as start for the relative path. It can be seen as the working directory 103 * from which to point to <code>file</code>. 104 * @param file The file to point to. 105 * @return the path to <code>file</code> relative to <code>baseDir</code> or the absolute path, 106 * if a relative path cannot be formulated (i.e. they have no directory in common). 107 * @throws IOException In case of an error 108 */ 109 public static String getRelativePath(File baseDir, String file) 110 throws IOException 111 { 112 // When passing the current working dir by "new File(".").getAbsoluteFile()" to this method, it 113 // generates 2 "../" where there should be only one "../", because it counts the single "." at the end as subdirectory. 114 // Therefore, we now call once simplifyPath before we start. 115 // Maybe we don't need to call simplifyPath in _getRelativePath anymore. Need to check later. Marco. 116 // Additionally, this method did not work without baseDir being an absolute path. Therefore, I now check and make it absolute, if it is not yet. 117 if (baseDir.isAbsolute()) 118 baseDir = new File(simplifyPath(baseDir)); 119 else 120 baseDir = new File(simplifyPath(baseDir.getAbsoluteFile())); 121 122 123 File absFile; 124 File tmpF = new File(file); 125 if (tmpF.isAbsolute()) 126 absFile = tmpF; 127 else 128 absFile = new File(baseDir, file); 129 130 File dest = absFile; 131 File b = baseDir; 132 String up = ""; 133 while (b.getParentFile() != null) { 134 String res = _getRelativePath(b, dest.getAbsolutePath()); 135 if (res != null) 136 return up + res; 137 138// up = "../" + up; 139 up = ".." + File.separatorChar + up; 140 b = b.getParentFile(); 141 } 142 143 return absFile.getAbsolutePath(); 144 } 145 146 /** 147 * This method finds - if possible - a relative path for addressing 148 * the given <code>file</code> from the given <code>baseDir</code>. 149 * <p> 150 * If it is not possible to denote <code>file</code> relative to <code>baseDir</code>, 151 * the absolute path of <code>file</code> will be returned. 152 * </p> 153 * See {@link getRelativePath} for examples. 154 * 155 * @param baseDir This directory is used as start for the relative path. It can be seen as the working directory 156 * from which to point to <code>file</code>. 157 * @param file The file to point to. 158 * @return the path to <code>file</code> relative to <code>baseDir</code> or the absolute path, 159 * if a relative path cannot be formulated (i.e. they have no directory in common). 160 * @throws IOException In case of an error 161 */ 162 public static String getRelativePath(File baseDir, File file) 163 throws IOException 164 { 165 return getRelativePath(baseDir, file.getPath()); 166 } 167 168 /** 169 * This method finds - if possible - a relative path for addressing 170 * the given <code>file</code> from the given <code>baseDir</code>. 171 * <p> 172 * If it is not possible to denote <code>file</code> relative to <code>baseDir</code>, 173 * the absolute path of <code>file</code> will be returned. 174 * </p> 175 * See {@link getRelativePath} for examples. 176 * 177 * @param baseDir This directory is used as start for the relative path. It can be seen as the working directory 178 * from which to point to <code>file</code>. 179 * @param file The file to point to. 180 * @return the path to <code>file</code> relative to <code>baseDir</code> or the absolute path, 181 * if a relative path cannot be formulated (i.e. they have no directory in common). 182 * @throws IOException In case of an error 183 */ 184 public static String getRelativePath(String baseDir, String file) 185 throws IOException 186 { 187 return getRelativePath(new File(baseDir), file); 188 } 189 190 /** 191 * This method is a helper method used by {@link #getRelativePath(File, String) }. It will 192 * only return a relative path, if <code>file</code> is a child node of <code>baseDir</code>. 193 * Otherwise it returns <code>null</code>. 194 * 195 * @param baseDir The directory to which the resulting path will be relative. 196 * @param file The file to which the resulting path will point. 197 */ 198 private static String _getRelativePath(File baseDir, String file) 199 throws IOException 200 { 201 // we make baseDir now absolute in getRelativePath(...) 202// if (!baseDir.isAbsolute()) 203// throw new IllegalArgumentException("baseDir \""+baseDir.getPath()+"\" is not absolute!"); 204 205 File absFile; 206 File tmpF = new File(file); 207 if (tmpF.isAbsolute()) 208 absFile = tmpF; 209 else 210 absFile = new File(baseDir, file); 211 212 String absFileStr = null; 213 String baseDirStr = null; 214 for (int mode_base = 0; mode_base < 2; ++mode_base) { 215 switch (mode_base) { 216 case 0: 217 baseDirStr = simplifyPath(baseDir); 218 break; 219 case 1: 220 baseDirStr = baseDir.getCanonicalPath(); 221 break; 222 default: 223 throw new IllegalStateException("this should never happen!"); 224 } 225 226 for (int mode_abs = 0; mode_abs < 2; ++mode_abs) { 227 baseDirStr = addFinalSlash(baseDirStr); 228 229 switch (mode_abs) { 230 case 0: 231 absFileStr = simplifyPath(absFile); 232 break; 233 case 1: 234 absFileStr = absFile.getCanonicalPath(); 235 break; 236 default: 237 throw new IllegalStateException("this should never happen!"); 238 } 239 240 if (!absFileStr.startsWith(baseDirStr)) { 241 if (mode_base >= 1 && mode_abs >= 1) 242 return null; 243 // throw new IllegalArgumentException("file \""+file+"\" is not a child of baseDir \""+baseDir.getCanonicalPath()+"\"!"); 244 } 245 else 246 break; 247 } // for (int mode_abs = 0; mode_abs < 2; ++mode_abs) { 248 } // for (int mode = 0; mode < 2; ++mode) { 249 250 if (baseDirStr == null) 251 throw new NullPointerException("baseDirStr == null"); 252 253 if (absFileStr == null) 254 throw new NullPointerException("absFileStr == null"); 255 256 return absFileStr.substring(baseDirStr.length(), absFileStr.length()); 257 } 258 259 /** 260 * This method removes double slashes, single-dot-directories and double-dot-directories 261 * from a path. This means, it does nearly the same as <code>File.getCanonicalPath</code>, but 262 * it does not resolve symlinks. This is essential for the method <code>getRelativePath</code>, 263 * because this method first tries it with a simplified path before using the canonical path 264 * (prevents problems with iteration through directories, where there are symlinks). 265 * <p> 266 * Please note that this method makes the given path absolute! 267 * 268 * @param path A path to simplify, e.g. "/../opt/java/jboss/../jboss/./bin/././../server/default/lib/." 269 * @return the simplified string (absolute path), e.g. "/opt/java/jboss/server/default/lib" 270 */ 271 public static String simplifyPath(File path) 272 { 273 logger.debug("simplifyPath: path='{}'", path); 274 LinkedList<String> dirs = new LinkedList<String>(); 275 276 String pathStr = path.getAbsolutePath(); 277 boolean startWithSeparator = pathStr.startsWith(File.separator); 278 279 StringTokenizer tk = new StringTokenizer(pathStr, File.separator, false); 280 while (tk.hasMoreTokens()) { 281 String dir = tk.nextToken(); 282 if (".".equals(dir)) 283 ;// nothing 284 else if ("..".equals(dir)) { 285 if (!dirs.isEmpty()) 286 dirs.removeLast(); 287 } 288 else 289 dirs.addLast(dir); 290 } 291 logger.debug("simplifyPath: dirs='{}'", dirs); 292 293 StringBuffer sb = new StringBuffer(); 294 for (String dir : dirs) { 295 if (startWithSeparator || sb.length() > 0) 296 sb.append(File.separator); 297 sb.append(dir); 298 } 299 300 return sb.toString(); 301 } 302 303 /** 304 * Transfer data between streams. 305 * @param in The input stream 306 * @param out The output stream 307 * @param inputOffset How many bytes to skip before transferring 308 * @param inputLen How many bytes to transfer. -1 = all 309 * @return The number of bytes transferred 310 * @throws IOException if an error occurs. 311 */ 312 public static long transferStreamData(InputStream in, OutputStream out, long inputOffset, long inputLen) 313 throws java.io.IOException 314 { 315 return transferStreamData(in, out, inputOffset, inputLen, null); 316 } 317 318 public static long transferStreamData(final InputStream in, final OutputStream out, final long inputOffset, final long inputLen, final ProgressMonitor monitor) 319 throws java.io.IOException 320 { 321 final byte[] buf = new byte[4 * 4096]; 322 323 if (monitor!= null) { 324 if (inputLen < 0) 325 monitor.beginTask("Copying data", 100); 326 else 327 monitor.beginTask("Copying data", (int)(inputLen / buf.length) + 1); 328 } 329 try { 330 int bytesRead; 331 int transferred = 0; 332 333 //skip offset 334 if(inputOffset > 0) 335 if(in.skip(inputOffset) != inputOffset) 336 throw new IOException("Input skip failed (offset "+inputOffset+")"); 337 338 while (true) { 339 if(inputLen >= 0) 340 bytesRead = in.read(buf, 0, (int)Math.min(buf.length, inputLen-transferred)); 341 else 342 bytesRead = in.read(buf); 343 344 if (bytesRead <= 0) 345 break; 346 347 out.write(buf, 0, bytesRead); 348 349 if (monitor != null && inputLen >= 0) 350 monitor.worked(1); 351 352 transferred += bytesRead; 353 354 if(inputLen >= 0 && transferred >= inputLen) 355 break; 356 } 357 out.flush(); 358 359 return transferred; 360 } finally { 361 if (monitor!= null) { 362 if (inputLen < 0) 363 monitor.worked(100); 364 365 monitor.done(); 366 } 367 } 368 } 369 370 /** 371 * Transfer all available data from an {@link InputStream} to an {@link OutputStream}. 372 * <p> 373 * This is a convenience method for <code>transferStreamData(in, out, 0, -1)</code> 374 * @param in The stream to read from 375 * @param out The stream to write to 376 * @return The number of bytes transferred 377 * @throws IOException In case of an error 378 */ 379 public static long transferStreamData(InputStream in, OutputStream out) 380 throws java.io.IOException 381 { 382 return transferStreamData(in, out, 0, -1); 383 } 384 385// public static enum CreateSymlinkResult { 386// /** 387// * The symlink was successfully created. 388// */ 389// symlinked, 390// 391// /** 392// * The data was copied instead of being symlinked. 393// */ 394// copied, 395// 396// /** 397// * If the destination already exists. 398// */ 399// alreadyExists, 400// 401// /** 402// * While waiting for the symlink to be created, we catched an {@link InterruptedException}. 403// */ 404// interrupted, 405// 406// /** 407// * The symlink was not created and <code>copyIfSymlinksNotSupported</code> was set to <code>false</code>. 408// */ 409// unsupported, 410// 411// /** 412// * Specifies that the existing File object neither denotes a Directory nor a File. This can only be returned 413// * when creation of the symlink was not possible. 414// */ 415// unknownFileType 416// } 417// 418// /** 419// * This method creates a symlink if supported by the operating system. Even though windows 420// * does support them, this method supports them only on GNU/Linux, so far. It seems, anyway, 421// * that symlinks on windows can cause very strange behaviour and data loss! see this for more 422// * details: http://shell-shocked.org/article.php?id=284 423// * 424// * @param existing The existing file or directory. If this is relative, the symlink will be relative, too. 425// * If the data needs to be copied, it will be copied to <code>new File(link, existing.getPath())</code>. 426// * @param link The link that shall be created. 427// * @param copyIfSymlinksNotSupported If this is <code>true</code> and the OS does not support symlinks, the method 428// * will delegate to {@link #copyDirectory(File, File)} or {@link #copyFile(File, File)}. If it is <code>false</code>, 429// * either the symlink can be created or nothing will be done. 430// * @return an instance of {@link CreateSymlinkResult} - never <code>null</code>. 431// * @throws IOException if IO fails in an underlying call. 432// */ 433// public static CreateSymlinkResult createSymlink(File existing, File link, boolean copyIfSymlinksNotSupported) 434// throws IOException 435// { 436// Logger logger = LoggerFactory.getLogger(IOUtil.class); 437// 438// if (logger.isDebugEnabled()) 439// logger.debug("createSymlink: begin: existing=\"" + existing.getPath() + "\" link=\"" + link.getPath() + "\""); 440// 441// if (link.exists()) 442// return CreateSymlinkResult.alreadyExists; 443// 444// File linkParent = link.getParentFile(); 445// if (linkParent != null && !linkParent.exists() && !linkParent.mkdirs()) 446// logger.warn("createSymlink: creating the link's parent directories failed!"); 447// 448// int processResult; 449// Process process = Runtime.getRuntime().exec(new String[] { "/bin/ln", "-s", existing.getPath(), link.getAbsolutePath() }); 450// try { 451// processResult = process.waitFor(); 452// } catch (InterruptedException e) { 453// logger.warn("createSymlink: interrupted! will return immediately!", e); 454// return CreateSymlinkResult.interrupted; 455// } 456// 457// if (processResult != 0) 458// logger.warn("createSymlink: processResult != 0, but: " + processResult); 459// 460// if (link.exists()) 461// return CreateSymlinkResult.symlinked; 462// 463// if (!copyIfSymlinksNotSupported) 464// return CreateSymlinkResult.unsupported; 465// 466// logger.debug("createSymlink: symlink could not be created - will copy instead: existing=\"" + existing.getPath() + "\" link=\"" + link.getPath() + "\""); 467// 468// // A symlink references from the ${link} to the ${existing}. Therefore, we must modify ${existing}, if it is not absolute. 469// if (!existing.isAbsolute()) 470// existing = new File(link, existing.getPath()); 471// 472// if (existing.isDirectory()) 473// copyDirectory(existing, link); 474// else if (existing.isFile()) 475// copyFile(existing, link); 476// else { 477// logger.debug("createSymlink: ${existing} is neither a directory, nor a file! cannot create link: existing=\"" + existing.getPath() + "\" link=\"" + link.getPath() + "\""); 478// return CreateSymlinkResult.unknownFileType; 479// } 480// 481// return CreateSymlinkResult.copied; 482// } 483 484 /** 485 * This method deletes the given directory recursively. If the given parameter 486 * specifies a file and no directory, it will be deleted anyway. If one or more 487 * files or subdirectories cannot be deleted, the method still continues and tries 488 * to delete as many files/subdirectories as possible. 489 * <p> 490 * Before diving into a subdirectory, it first tries to delete <code>dir</code>. If this 491 * succeeds, it does not try to dive into it. This way, this implementation is symlink-safe. 492 * Hence, if <code>dir</code> denotes a symlink to another directory, this method does 493 * normally not delete the real contents of the directory. 494 * </p> 495 * <p> 496 * <b>Warning!</b> It sometimes still deletes the contents! This happens, if the user has no permissions 497 * onto a symlinked directory (thus, the deletion before dive-in fails), but its contents. In this case, 498 * the method will dive into the directory (because the deletion failed and it therefore assumes it to be a real 499 * directory) and delete the contents. 500 * </p> 501 * <p> 502 * We might later extend this method to do further checks (maybe call an OS program like we do in 503 * {@link #createSymlink(File, File, boolean)}). 504 * </p> 505 * 506 * @param dir The directory or file to delete 507 * @return <code>true</code> if the file or directory does not exist anymore. 508 * This means it either was not existing already before or it has been 509 * successfully deleted. <code>false</code> if the directory could not be 510 * deleted. 511 */ 512 public static boolean deleteDirectoryRecursively(File dir) 513 { 514 if (!dir.exists()) 515 return true; 516 517 // If we're running this on linux (that's what I just tested ;) and dir denotes a symlink, 518 // we must not dive into it and delete its contents! We can instead directly delete dir. 519 // There is no way in Java (except for calling system tools) to find out whether it is a symlink, 520 // but we can simply delete it. If the deletion succeeds, it was a symlink, otherwise it's a real directory. 521 // This way, we don't delete the contents in symlinks and thus prevent data loss! 522 try { 523 if (dir.delete()) 524 return true; 525 } catch(SecurityException e) { 526 // ignore according to docs. 527 return false; // or should we really ignore this security exception and delete the contents?!?!?! To return false instead is definitely safer. 528 } 529 530 if (dir.isDirectory()) { 531 File[] content = dir.listFiles(); 532 for (File f : content) { 533 if (f.isDirectory()) 534 deleteDirectoryRecursively(f); 535 else 536 try { 537 f.delete(); 538 } catch(SecurityException e) { 539 // ignore according to docs. 540 } 541 } 542 } 543 544 try { 545 return dir.delete(); 546 } catch(SecurityException e) { 547 return false; 548 } 549 } 550 551 /** 552 * This method deletes the given directory recursively. If the given parameter 553 * specifies a file and no directory, it will be deleted anyway. If one or more 554 * files or subdirectories cannot be deleted, the method still continues and tries 555 * to delete as many files/subdirectories as possible. 556 * 557 * @param dir The directory or file to delete 558 * @return True, if the file or directory does not exist anymore. This means it either 559 * was not existing already before or it has been successfully deleted. False, if the 560 * directory could not be deleted. 561 */ 562 public static boolean deleteDirectoryRecursively(String dir) 563 { 564 File dirF = new File(dir); 565 return deleteDirectoryRecursively(dirF); 566 } 567 568 /** 569 * Tries to find a unique, not existing folder name under the given root folder 570 * suffixed with a number. When found, the directory will be created. 571 * It will start with 0 and make Integer.MAX_VALUE number 572 * of iterations maximum. The first not existing foldername will be returned. 573 * If no foldername could be found after the maximum iterations a {@link IOException} 574 * will be thrown. 575 * <p> 576 * Note that the creation of the directory is not completely safe. This method is 577 * synchronized, but other processes could "steal" the unique filename. 578 * 579 * @param rootFolder The rootFolder to find a unique subfolder for 580 * @param prefix A prefix for the folder that has to be found. 581 * @return A File pointing to an unique (not existing) Folder under the given rootFolder and with the given prefix 582 * @throws IOException in case of an error 583 */ 584 public static synchronized File createUniqueIncrementalFolder(File rootFolder, final String prefix) throws IOException 585 { 586 for(int n=0; n<Integer.MAX_VALUE; n++) { 587 File f = new File(rootFolder, String.format("%s%x", prefix, n)); 588 if(!f.exists()) { 589 if(!f.mkdirs()) 590 throw new IOException("The directory "+f.getAbsolutePath()+" could not be created"); 591 return f; 592 } 593 } 594 throw new IOException("Iterated to Integer.MAX_VALUE and could not find a unique folder!"); 595 } 596 597 /** 598 * Tries to find a unique, not existing folder name under the given root folder with a random 599 * number (in hex format) added to the given prefix. When found, the directory will be created. 600 * <p> 601 * The method will try to find a name for <code>maxIterations</code> number 602 * of itarations and use random numbers from 0 to <code>uniqueOutOf</code>. 603 * <p> 604 * Note that the creation of the directory is not completely safe. This method is 605 * synchronized, but other processes could "steal" the unique filename. 606 * 607 * @param rootFolder The rootFolder to find a unique subfolder for 608 * @param prefix A prefix for the folder that has to be found. 609 * @param maxIterations The maximum number of itarations this method shoud do. 610 * If after them still no unique folder could be found, a {@link IOException} 611 * is thrown. 612 * @param uniqueOutOf The range of random numbers to apply (0 - given value) 613 * 614 * @return A File pointing to an unique folder under the given rootFolder and with the given prefix 615 * @throws IOException in case of an error 616 */ 617 public static synchronized File createUniqueRandomFolder(File rootFolder, final String prefix, long maxIterations, long uniqueOutOf) throws IOException 618 { 619 long count = 0; 620 while(++count <= maxIterations) { 621 File f = new File(rootFolder, String.format("%s%x", prefix, (long)(Math.random() * uniqueOutOf))); 622 if(!f.exists()) { 623 if(!f.mkdirs()) 624 throw new IOException("The directory "+f.getAbsolutePath()+" could not be created"); 625 return f; 626 } 627 } 628 throw new IOException("Reached end of maxIteration("+maxIterations+"), but could not acquire a unique fileName for folder "+rootFolder); 629 } 630 631 /** 632 * Tries to find a unique, not existing folder name under the given root folder with a random 633 * number (in hex format) added to the given prefix. When found, the directory will be created. 634 * <p> 635 * This is a convenience method for {@link createUniqueRandomFolder} 636 * and calls it with 10000 as maxIterations and 10000 as number range. 637 * <p> 638 * Note that this method might throw a {@link IOException} 639 * if it will not find a unique name within 10000 iterations. 640 * <p> 641 * Note that the creation of the directory is not completely safe. This method is 642 * synchronized, but other processes could "steal" the unique filename. 643 * 644 * @param rootFolder The rootFolder to find a unique subfolder for 645 * @param prefix A prefix for the folder that has to be found. 646 * @return A File pointing to an unique (non-existing) Folder under the given rootFolder and with the given prefix 647 * @throws IOException in case of an error 648 */ 649 public static File createUniqueRandomFolder(File rootFolder, final String prefix) throws IOException 650 { 651 return createUniqueRandomFolder(rootFolder, prefix, 10000, 10000); 652 } 653 654 /** 655 * Get a file object from a base directory and a list of subdirectories or files. 656 * @param file The base directory 657 * @param subDirs The subdirectories or files 658 * @return The new file instance 659 */ 660 public static File getFile(File file, String ... subDirs) 661 { 662 File f = file; 663 for (String subDir : subDirs) 664 f = new File(f, subDir); 665 return f; 666 } 667 668 /** 669 * Write text to a file. 670 * @param file The file to write the text to 671 * @param text The text to write 672 * @param encoding The caracter set to use as file encoding (e.g. "UTF-8") 673 * @throws IOException in case of an io error 674 * @throws FileNotFoundException if the file exists but is a directory 675 * rather than a regular file, does not exist but cannot 676 * be created, or cannot be opened for any other reason 677 * @throws UnsupportedEncodingException If the named encoding is not supported 678 */ 679 public static void writeTextFile(File file, String text, String encoding) 680 throws IOException, FileNotFoundException, UnsupportedEncodingException 681 { 682 FileOutputStream out = null; 683 OutputStreamWriter w = null; 684 try { 685 out = new FileOutputStream(file); 686 w = new OutputStreamWriter(out, encoding); 687 w.write(text); 688 } finally { 689 if (w != null) w.close(); 690 if (out != null) out.close(); 691 } 692 } 693 694 /** 695 * Read a text file and return the contents as string. 696 * @param f The file to read, maximum size 1 GB 697 * @param encoding The file encoding, e.g. "UTF-8" 698 * @throws FileNotFoundException if the file was not found 699 * @throws IOException in case of an io error 700 * @throws UnsupportedEncodingException If the named encoding is not supported 701 * @return The contents of the text file 702 */ 703 public static String readTextFile(File f, String encoding) 704 throws FileNotFoundException, IOException, UnsupportedEncodingException 705 { 706 if (f.length() > GIGABYTE) 707 throw new IllegalArgumentException("File exceeds " + GIGABYTE + " bytes: " + f.getAbsolutePath()); 708 709 StringBuffer sb = new StringBuffer(); 710 FileInputStream fin = new FileInputStream(f); 711 try { 712 InputStreamReader reader = new InputStreamReader(fin, encoding); 713 try { 714 char[] cbuf = new char[1024]; 715 int bytesRead; 716 while (true) { 717 bytesRead = reader.read(cbuf); 718 if (bytesRead <= 0) 719 break; 720 else 721 sb.append(cbuf, 0, bytesRead); 722 } 723 } finally { 724 reader.close(); 725 } 726 } finally { 727 fin.close(); 728 } 729 return sb.toString(); 730 } 731 732 /** 733 * Read a UTF-8 encoded text file and return the contents as string. 734 * <p>For other encodings, use {@link readTextFile}. 735 * @param f The file to read, maximum size 1 GB 736 * @throws FileNotFoundException if the file was not found 737 * @throws IOException in case of an io error 738 * @return The contents of the text file 739 */ 740 public static String readTextFile(File f) 741 throws FileNotFoundException, IOException 742 { 743 return readTextFile(f, CHARSET_NAME_UTF_8); 744 } 745 746 /** 747 * Write text to a file using UTF-8 encoding. 748 * @param file The file to write the text to 749 * @param text The text to write 750 * @throws IOException in case of an io error 751 * @throws FileNotFoundException if the file exists but is a directory 752 * rather than a regular file, does not exist but cannot 753 * be created, or cannot be opened for any other reason 754 */ 755 public static void writeTextFile(File file, String text) 756 throws IOException, FileNotFoundException, UnsupportedEncodingException 757 { 758 writeTextFile(file, text, CHARSET_NAME_UTF_8); 759 } 760 761 /** 762 * Read a text file from an {@link InputStream} using 763 * the given encoding. 764 * <p> 765 * This method does NOT close the input stream! 766 * @param in The stream to read from. It will not be closed by this operation. 767 * @param encoding The charset used for decoding, e.g. "UTF-8" 768 * @return The contents of the input stream file 769 */ 770 public static String readTextFile(InputStream in, String encoding) 771 throws FileNotFoundException, IOException 772 { 773 StringBuffer sb = new StringBuffer(); 774 InputStreamReader reader = new InputStreamReader(in, encoding); 775 char[] cbuf = new char[1024]; 776 int bytesRead; 777 while (true) { 778 bytesRead = reader.read(cbuf); 779 if (bytesRead <= 0) 780 break; 781 else 782 sb.append(cbuf, 0, bytesRead); 783 784 if (sb.length() > GIGABYTE) 785 throw new IllegalArgumentException("Text exceeds " + GIGABYTE + " bytes!"); 786 } 787 return sb.toString(); 788 } 789 790 /** 791 * Read a text file from an {@link InputStream} using 792 * UTF-8 encoding. 793 * <p> 794 * This method does NOT close the input stream! 795 * @param in The stream to read from. It will not be closed by this operation. 796 * @return The contents of the input stream file 797 */ 798 public static String readTextFile(InputStream in) 799 throws FileNotFoundException, IOException 800 { 801 return readTextFile(in, CHARSET_NAME_UTF_8); 802 } 803 804 /** 805 * Get the extension of a filename. 806 * @param fileName A file name (might contain the full path) or <code>null</code>. 807 * @return <code>null</code>, if the given <code>fileName</code> doesn't contain 808 * a dot (".") or if the given <code>fileName</code> is <code>null</code>. Otherwise, 809 * returns all AFTER the last dot. 810 */ 811 public static String getFileExtension(String fileName) 812 { 813 if (fileName == null) 814 return null; 815 816 int lastIndex = fileName.lastIndexOf("."); 817 if (lastIndex < 0) 818 return null; 819 820 return fileName.substring(lastIndex + 1); 821 } 822 823 /** 824 * Get a filename without extension. 825 * @param fileName A file name (might contain the full path) or <code>null</code>. 826 * @return all before the last dot (".") or the full <code>fileName</code> if no dot exists. 827 * Returns <code>null</code>, if the given <code>fileName</code> is <code>null</code>. 828 */ 829 public static String getFileNameWithoutExtension(String fileName) 830 { 831 if (fileName == null) 832 return null; 833 834 int lastIndex = fileName.lastIndexOf("."); 835 if (lastIndex < 0) 836 return fileName; 837 838 return fileName.substring(0, lastIndex); 839 } 840 841 /** 842 * Get the temporary directory. 843 * <p> 844 * Note, that you should better use {@link #getUserTempDir()} in many situations 845 * since there is solely one global temp directory in GNU/Linux and you might run into permissions trouble 846 * and other collisions when using the global temp directory with a hardcoded static subdir. 847 * </p> 848 * 849 * @return The temporary directory. 850 * @see #getUserTempDir() 851 */ 852 public static File getTempDir() 853 { 854 if(tempDir == null) 855 tempDir = new File(System.getProperty("java.io.tmpdir")); 856 return tempDir; 857 } 858 859 /** 860 * Get a user-dependent temp directory in every operating system and create it, if it does not exist. 861 * 862 * @param prefix <code>null</code> or a prefix to be added before the user-name. 863 * @param suffix <code>null</code> or a suffix to be added after the user-name. 864 * @return a user-dependent temp directory, which is created if it does not yet exist. 865 * @throws IOException if the directory does not exist and cannot be created. 866 */ 867 public static File createUserTempDir(String prefix, String suffix) 868 throws IOException 869 { 870 File userTempDir = getUserTempDir(prefix, suffix); 871 if (userTempDir.isDirectory()) 872 return userTempDir; 873 874 if (userTempDir.exists()) { 875 if (!userTempDir.isDirectory()) // two processes might call this method simultaneously - thus we must check again; in case it was just created. 876 throw new IOException("The user-dependent temp directory's path exists already, but is no directory: " + userTempDir.getPath()); 877 } 878 879 if (!userTempDir.mkdirs()) { 880 if (!userTempDir.isDirectory()) 881 throw new IOException("The user-dependent temp directory could not be created: " + userTempDir.getPath()); 882 } 883 884 return userTempDir; 885 } 886 887 /** 888 * Get a user-dependent temp directory in every operating system. Given the same prefix and suffix, the resulting 889 * directory will always be the same on subsequent calls - even across different VMs. 890 * <p> 891 * Since many operating systems (for example GNU/Linux and other unixes) use one 892 * global temp directory for all users, you might run into collisions when using hard-coded sub-directories within the 893 * temp dir. Permission problems can cause exceptions and multiple users sharing the same directory 894 * at the same time might even lead to heisenbugs. Therefore, this method encodes the user 895 * name into a subdirectory under the system's temp dir. 896 * </p> 897 * <p> 898 * In order to prevent illegal directory names to be generated, this method encodes the user name (including the 899 * pre- and suffixes passed) using a {@link ParameterCoderMinusHex} which encodes all chars except 900 * '0'...'9', 'A'...'Z', 'a'...'z', '.', '_' and '+'. 901 * </p> 902 * 903 * @param prefix <code>null</code> or a prefix to be added before the user-name. 904 * @param suffix <code>null</code> or a suffix to be added after the user-name. 905 * @return a user-dependent temp directory. 906 * @see #getTempDir() 907 * @see #createUserTempDir(String, String) 908 */ 909 public static File getUserTempDir(String prefix, String suffix) 910 { 911 // In GNU/Linux, there is exactly one temp-directory for all users; hence we need to put the current OS user's name into the path. 912 String userNameDir = (prefix == null ? "" : prefix) + String.valueOf(getUserName()) + (suffix == null ? "" : suffix); 913 914 // the user name might contain illegal characters (in windows) => we encode basically all characters. 915 try { 916 userNameDir = URLEncoder.encode(userNameDir.replace('*', '_'), CHARSET_NAME_UTF_8); 917 } catch (UnsupportedEncodingException e) { 918 throw new RuntimeException(e); 919 } 920 921 return new File(IOUtil.getTempDir(), userNameDir); 922 } 923 924 private static String getUserName() 925 { 926 return System.getProperty("user.name"); //$NON-NLS-1$ 927 } 928 929 public static File getUserHome() 930 { 931 String userHome = System.getProperty("user.home"); //$NON-NLS-1$ 932 if (userHome == null) 933 throw new IllegalStateException("System property user.home is not set! This should never happen!"); //$NON-NLS-1$ 934 935 return new File(userHome); 936 } 937 938 /** 939 * Compares two InputStreams. The amount of bytes given in the parameter 940 * <code>length</code> are consumed from the streams, no matter if their 941 * contents are equal or not. 942 * 943 * @param in1 the first InputStream 944 * @param in2 the second InputStream 945 * @param length how many bytes to read from each InputStream 946 * @return true if both InputStreams contain the identical data or false if not 947 * @throws IOException if an I/O error occurs while reading <code>length</code> bytes 948 * from one of the input streams. 949 */ 950 public static boolean compareInputStreams(InputStream in1, InputStream in2, long length) 951 throws IOException 952 { 953 return compareInputStreams(in1, in2, length, length); 954 } 955 956 /** 957 * Compares two InputStreams. This method returns when the first 958 * 959 * @param in1 the first InputStream 960 * @param in2 the second InputStream 961 * @param compareLength how many bytes to compare before considering the stream 962 * contents as equal. 963 * @param minimumReadLength how many bytes to read from each InputStream. This amount of 964 * bytes is read from the stream, no matter if contents are equal or not. 965 * @return true if both InputStreams contain the identical data or false if not 966 * @throws IOException if an I/O error occurs while reading <code>length</code> bytes 967 * from one of the input streams. 968 */ 969 public static boolean compareInputStreams(InputStream in1, InputStream in2, long compareLength, long minimumReadLength) 970 throws IOException 971 { 972 boolean identical = true; 973 int read = 0; 974 while(read<compareLength) { 975 int int1 = in1.read(); 976 int int2 = in2.read(); 977 read++; 978 if (int1 != int2) { 979 identical = false; 980 break; 981 } 982 } 983 if(read < minimumReadLength) { 984 in1.skip(minimumReadLength-read); 985 in2.skip(minimumReadLength-read); 986 } 987 return identical; 988 } 989 990 /** 991 * Compare the contents of two given files. 992 * @param f1 the first file 993 * @param f2 the second file 994 * @return <code>true</code> if the files have same size and their contents are equal - 995 * <code>false</code> otherwise. 996 * @throws FileNotFoundException If one of the files could not be found. 997 * @throws IOException If reading one of the files failed. 998 */ 999 public static boolean compareFiles(File f1, File f2) throws FileNotFoundException, IOException 1000 { 1001 if(!f1.exists()) 1002 throw new FileNotFoundException(f1.getAbsolutePath()); 1003 if(!f2.exists()) 1004 throw new FileNotFoundException(f2.getAbsolutePath()); 1005 if(f1.equals(f2)) 1006 return true; 1007 if(f1.length() != f2.length()) 1008 return false; 1009 FileInputStream in1 = new FileInputStream(f1); 1010 FileInputStream in2 = new FileInputStream(f2); 1011 try { 1012 return compareInputStreams(in1, in2, f1.length(), 0); 1013 } finally { 1014 in1.close(); 1015 in2.close(); 1016 } 1017 } 1018 1019 /** 1020 * Copy a directory recursively. 1021 * @param sourceDirectory The source directory 1022 * @param destinationDirectory The destination directory 1023 * @throws IOException in case of an error 1024 */ 1025 public static void copyDirectory(File sourceDirectory, File destinationDirectory) throws IOException 1026 { 1027 copyDirectory(sourceDirectory, destinationDirectory, null); 1028 } 1029 public static void copyDirectory(File sourceDirectory, File destinationDirectory, FileFilter fileFilter) throws IOException 1030 { 1031 if(!sourceDirectory.exists() || !sourceDirectory.isDirectory()) 1032 throw new IOException("No such source directory: "+sourceDirectory.getAbsolutePath()); 1033 if(destinationDirectory.exists()) { 1034 if(!destinationDirectory.isDirectory()) 1035 throw new IOException("Destination exists but is not a directory: "+sourceDirectory.getAbsolutePath()); 1036 } else 1037 destinationDirectory.mkdirs(); 1038 1039 File[] files = sourceDirectory.listFiles(fileFilter); 1040 for (File file : files) { 1041 File destinationFile = new File(destinationDirectory, file.getName()); 1042 if(file.isDirectory()) { 1043 if (destinationDirectory.getAbsoluteFile().equals(file.getAbsoluteFile())) 1044 logger.warn("copyDirectory: Skipping directory, because it equals the destination: {}", file.getAbsoluteFile()); 1045 else 1046 copyDirectory(file, destinationFile, fileFilter); 1047 } 1048 else 1049 copyFile(file, destinationFile); 1050 } 1051 1052 destinationDirectory.setLastModified(sourceDirectory.lastModified()); 1053 } 1054 1055 /** 1056 * Copy a resource loaded by the class loader of a given class to a file. 1057 * <p> 1058 * This is a convenience method for <code>copyResource(sourceResClass, sourceResName, new File(destinationFilename))</code>. 1059 * @param sourceResClass The class whose class loader to use. If the class 1060 * was loaded using the bootstrap class loaderClassloader.getSystemResourceAsStream 1061 * will be used. See {@link Class#getResourceAsStream(String)} for details. 1062 * @param sourceResName The name of the resource 1063 * @param destinationFilename Where to copy the contents of the resource 1064 * @throws IOException in case of an error 1065 */ 1066 public static void copyResource(Class<?> sourceResClass, String sourceResName, String destinationFilename) 1067 throws IOException 1068 { 1069 copyResource(sourceResClass, sourceResName, new File(destinationFilename)); 1070 } 1071 1072 1073 /** 1074 * Copy a resource loaded by the class loader of a given class to a file. 1075 * @param sourceResClass The class whose class loader to use. If the class 1076 * was loaded using the bootstrap class loaderClassloader.getSystemResourceAsStream 1077 * will be used. See {@link Class#getResourceAsStream(String)} for details. 1078 * @param sourceResName The name of the resource 1079 * @param destinationFile Where to copy the contents of the resource 1080 * @throws IOException in case of an error 1081 */ 1082 public static void copyResource(Class<?> sourceResClass, String sourceResName, File destinationFile) 1083 throws IOException 1084 { 1085 InputStream source = null; 1086 FileOutputStream destination = null; 1087 try{ 1088 source = sourceResClass.getResourceAsStream(sourceResName); 1089 if (source == null) 1090 throw new FileNotFoundException("Class " + sourceResClass.getName() + " could not find resource " + sourceResName); 1091 1092 if (destinationFile.exists()) { 1093 if (destinationFile.isFile()) { 1094 if (!destinationFile.canWrite()) 1095 throw new IOException("FileCopy: destination file is unwriteable: " + destinationFile.getCanonicalPath()); 1096 } else 1097 throw new IOException("FileCopy: destination is not a file: " + destinationFile.getCanonicalPath()); 1098 } else { 1099 File parentdir = destinationFile.getAbsoluteFile().getParentFile(); 1100 if (parentdir == null || !parentdir.exists()) 1101 throw new IOException("FileCopy: destination's parent directory doesn't exist: " + destinationFile.getCanonicalPath()); 1102 if (!parentdir.canWrite()) 1103 throw new IOException("FileCopy: destination's parent directory is unwriteable: " + destinationFile.getCanonicalPath()); 1104 } 1105 destination = new FileOutputStream(destinationFile); 1106 transferStreamData(source,destination); 1107 } finally { 1108 if (source != null) 1109 try { source.close(); } catch (IOException e) { ; } 1110 1111 if (destination != null) 1112 try { destination.close(); } catch (IOException e) { ; } 1113 } 1114 } 1115 1116 /** 1117 * Copy a file. 1118 * @param sourceFile The source file to copy 1119 * @param destinationFile To which file to copy the source 1120 * @throws IOException in case of an error 1121 */ 1122 public static void copyFile(File sourceFile, File destinationFile) 1123 throws IOException 1124 { 1125 copyFile(sourceFile, destinationFile, null); 1126 } 1127 public static void copyFile(File sourceFile, File destinationFile, ProgressMonitor monitor) 1128 throws IOException 1129 { 1130 FileInputStream source = null; 1131 FileOutputStream destination = null; 1132 1133 try { 1134 // First make sure the specified source file 1135 // exists, is a file, and is readable. 1136 if (!sourceFile.exists() || !sourceFile.isFile()) 1137 throw new IOException("FileCopy: no such source file: "+sourceFile.getCanonicalPath()); 1138 if (!sourceFile.canRead()) 1139 throw new IOException("FileCopy: source file is unreadable: "+sourceFile.getCanonicalPath()); 1140 1141 // If the destination exists, make sure it is a writeable file. If the destination doesn't 1142 // exist, make sure the directory exists and is writeable. 1143 if (destinationFile.exists()) { 1144 if (destinationFile.isFile()) { 1145 if (!destinationFile.canWrite()) 1146 throw new IOException("FileCopy: destination file is unwriteable: " + destinationFile.getCanonicalPath()); 1147 } else 1148 throw new IOException("FileCopy: destination is not a file: " + destinationFile.getCanonicalPath()); 1149 } else { 1150 File parentdir = destinationFile.getParentFile(); 1151 if (parentdir == null || !parentdir.exists()) 1152 throw new IOException("FileCopy: destination directory doesn't exist: " + 1153 destinationFile.getCanonicalPath()); 1154 if (!parentdir.canWrite()) 1155 throw new IOException("FileCopy: destination directory is unwriteable: " + 1156 destinationFile.getCanonicalPath()); 1157 } 1158 // If we've gotten this far, then everything is okay; we can 1159 // copy the file. 1160 source = new FileInputStream(sourceFile); 1161 destination = new FileOutputStream(destinationFile); 1162 transferStreamData(source, destination, 0, sourceFile.length(), monitor); 1163 // No matter what happens, always close any streams we've opened. 1164 } finally { 1165 if (source != null) 1166 try { source.close(); } catch (IOException e) { ; } 1167 if (destination != null) 1168 try { destination.close(); } catch (IOException e) { ; } 1169 } 1170 1171 // copy the timestamp 1172 destinationFile.setLastModified(sourceFile.lastModified()); 1173 } 1174 1175 /** 1176 * Add a trailing file separator character to the 1177 * given directory name if it does not already 1178 * end with one. 1179 * @see File#separator 1180 * @param directory A directory name 1181 * @return the directory name anding with a file seperator 1182 */ 1183 public static String addFinalSlash(String directory) 1184 { 1185 if (directory.endsWith(File.separator)) 1186 return directory; 1187 else 1188 return directory + File.separator; 1189 } 1190 1191 private static enum ParserExpects { 1192 NORMAL, 1193 BRACKET_OPEN, 1194 VARIABLE, 1195 BRACKET_CLOSE 1196 } 1197 1198 1199 /** 1200 * Generate a file from a template. The template file can contain variables which are formatted <code>"${variable}"</code>. 1201 * All those variables will be replaced, for which a value has been passed in the map <code>variables</code>. 1202 * <p> 1203 * Example:<br/> 1204 * <pre> 1205 * *** 1206 * Dear ${receipient.fullName}, 1207 * this is a spam mail trying to sell you ${product.name} for a very cheap price. 1208 * Best regards, ${sender.fullName} 1209 * *** 1210 * </pre> 1211 * <br/> 1212 * In order to generate a file from the above template, the map <code>variables</code> needs to contain values for these keys: 1213 * <ul> 1214 * <li>receipient.fullName</li> 1215 * <li>product.name</li> 1216 * <li>sender.fullName</li> 1217 * </ul> 1218 * </p> 1219 * <p> 1220 * If a key is missing in the map, the variable will not be replaced but instead written as-is into the destination file (a warning will be 1221 * logged). 1222 * </p> 1223 * 1224 * @param destinationFile The file (absolute!) that shall be created out of the template. 1225 * @param templateFile The template file to use. Must not be <code>null</code>. 1226 * @param characterSet The charset to use for the input and output file. Use <code>null</code> for the default charset. 1227 * @param variables This map defines what variable has to be replaced by what value. The 1228 * key is the variable name (without '$' and brackets '{', '}'!) and the value is the 1229 * value that will replace the variable in the output. This argument must not be <code>null</code>. 1230 * In order to allow passing {@link Properties} directly, this has been redefined as <code>Map<?,?></code> 1231 * from version 1.3.0 on. Map entries with a key that is not of type <code>String</code> or with a <code>null</code> 1232 * value are ignored. Values that are not of type <code>String</code> are converted using the 1233 * {@link Object#toString() toString()} method. 1234 */ 1235 public static void replaceTemplateVariables(File destinationFile, File templateFile, String characterSet, Map<?, ?> variables) 1236 throws IOException 1237 { 1238 if (!destinationFile.isAbsolute()) 1239 throw new IllegalArgumentException("destinationFile is not absolute: " + destinationFile.getPath()); 1240 1241 logger.info("Creating destination file \""+destinationFile.getAbsolutePath()+"\" from template \""+templateFile.getAbsolutePath()+"\"."); 1242 File destinationDirectory = destinationFile.getParentFile(); 1243 if (!destinationDirectory.exists()) { 1244 logger.info("Directory for destination file does not exist. Creating it: " + destinationDirectory.getAbsolutePath()); 1245 if (!destinationDirectory.mkdirs()) 1246 logger.error("Creating directory for destination file failed: " + destinationDirectory.getAbsolutePath()); 1247 } 1248 1249 // Create and configure StreamTokenizer to read template file. 1250 FileInputStream fin = new FileInputStream(templateFile); 1251 try { 1252 Reader fr = characterSet != null ? new InputStreamReader(fin, characterSet) : new InputStreamReader(fin); 1253 try { 1254 // Create FileWriter 1255 FileOutputStream fos = new FileOutputStream(destinationFile); 1256 try { 1257 Writer fw = characterSet != null ? new OutputStreamWriter(fos, characterSet) : new OutputStreamWriter(fos); 1258 try { 1259 replaceTemplateVariables(fw, fr, variables); 1260 } finally { 1261 fw.close(); 1262 } 1263 } finally { 1264 fos.close(); 1265 } 1266 } finally { 1267 fr.close(); 1268 } 1269 } finally { 1270 fin.close(); 1271 } 1272 } 1273 1274 /** 1275 * Replace variables (formatted <code>"${variable}"</code>) with their values 1276 * passed in the map <code>variables</code>. 1277 * <p> 1278 * Example:<br/> 1279 * <pre> 1280 * *** 1281 * Dear ${receipient.fullName}, 1282 * this is a spam mail trying to sell you ${product.name} for a very cheap price. 1283 * Best regards, ${sender.fullName} 1284 * *** 1285 * </pre> 1286 * <br/> 1287 * In order to generate a text from the above template, the map <code>variables</code> needs to contain values for these keys: 1288 * <ul> 1289 * <li>receipient.fullName</li> 1290 * <li>product.name</li> 1291 * <li>sender.fullName</li> 1292 * </ul> 1293 * </p> 1294 * <p> 1295 * If a key is missing in the map, the variable will not be replaced but instead written as-is into the destination file (a warning will be 1296 * logged). 1297 * </p> 1298 * 1299 * @param template the input text containing variables (or not). 1300 * @param variables This map defines what variable has to be replaced by what value. The 1301 * key is the variable name (without '$' and brackets '{', '}'!) and the value is the 1302 * value that will replace the variable in the output. This argument must not be <code>null</code>. 1303 * In order to allow passing {@link Properties} directly, this has been redefined as <code>Map<?,?></code> 1304 * from version 1.3.0 on. Map entries with a key that is not of type <code>String</code> or with a <code>null</code> 1305 * value are ignored. Values that are not of type <code>String</code> are converted using the 1306 * {@link Object#toString() toString()} method. 1307 * @return the processed text (i.e. the same as the template, but all known variables replaced by their values). 1308 */ 1309 public static String replaceTemplateVariables(String template, Map<?, ?> variables) 1310 { 1311 try { 1312 StringReader r = new StringReader(template); 1313 StringWriter w = new StringWriter(); 1314 try { 1315 replaceTemplateVariables(w, r, variables); 1316 w.flush(); 1317 return w.toString(); 1318 } finally { 1319 r.close(); 1320 w.close(); 1321 } 1322 } catch (IOException x) { 1323 throw new RuntimeException(x); // StringReader/Writer should *NEVER* throw any IOException since it's working in-memory only. 1324 } 1325 } 1326 1327 /** 1328 * Copy contents from the given reader to the given writer while 1329 * replacing variables which are formatted <code>"${variable}"</code> with their values 1330 * passed in the map <code>variables</code>. 1331 * <p> 1332 * Example:<br/> 1333 * <pre> 1334 * *** 1335 * Dear ${receipient.fullName}, 1336 * this is a spam mail trying to sell you ${product.name} for a very cheap price. 1337 * Best regards, ${sender.fullName} 1338 * *** 1339 * </pre> 1340 * <br/> 1341 * In order to generate a file from the above template, the map <code>variables</code> needs to contain values for these keys: 1342 * <ul> 1343 * <li>receipient.fullName</li> 1344 * <li>product.name</li> 1345 * <li>sender.fullName</li> 1346 * </ul> 1347 * </p> 1348 * <p> 1349 * If a key is missing in the map, the variable will not be replaced but instead written as-is into the destination file (a warning will be 1350 * logged). 1351 * </p> 1352 * 1353 * @param writer The writer to write the output to. 1354 * @param reader The template file to use. Must not be <code>null</code>. 1355 * @param variables This map defines what variable has to be replaced by what value. The 1356 * key is the variable name (without '$' and brackets '{', '}'!) and the value is the 1357 * value that will replace the variable in the output. This argument must not be <code>null</code>. 1358 * In order to allow passing {@link Properties} directly, this has been redefined as <code>Map<?,?></code> 1359 * from version 1.3.0 on. Map entries with a key that is not of type <code>String</code> or with a <code>null</code> 1360 * value are ignored. Values that are not of type <code>String</code> are converted using the 1361 * {@link Object#toString() toString()} method. 1362 */ 1363 public static void replaceTemplateVariables(Writer writer, Reader reader, Map<?, ?> variables) 1364 throws IOException 1365 { 1366 StreamTokenizer stk = new StreamTokenizer(reader); 1367 stk.resetSyntax(); 1368 stk.wordChars(0, Integer.MAX_VALUE); 1369 stk.ordinaryChar('$'); 1370 stk.ordinaryChar('{'); 1371 stk.ordinaryChar('}'); 1372 stk.ordinaryChar('\n'); 1373 1374 // Read, parse and replace variables from template and write to FileWriter fw. 1375 String variableName = null; 1376 StringBuilder tmpBuf = new StringBuilder(); 1377 ParserExpects parserExpects = ParserExpects.NORMAL; 1378 while (stk.nextToken() != StreamTokenizer.TT_EOF) { 1379 String stringToWrite = null; 1380 1381 if (stk.ttype == StreamTokenizer.TT_WORD) { 1382 switch (parserExpects) { 1383 case VARIABLE: 1384 parserExpects = ParserExpects.BRACKET_CLOSE; 1385 variableName = stk.sval; 1386 tmpBuf.append(variableName); 1387 break; 1388 case NORMAL: 1389 stringToWrite = stk.sval; 1390 break; 1391 default: 1392 parserExpects = ParserExpects.NORMAL; 1393 stringToWrite = tmpBuf.toString() + stk.sval; 1394 tmpBuf.setLength(0); 1395 } 1396 } 1397 else if (stk.ttype == '\n') { 1398 stringToWrite = new String(Character.toChars(stk.ttype)); 1399 1400 // These chars are not valid within a variable, so we reset the variable parsing, if we're currently parsing one. 1401 // This helps keeping the tmpBuf small (to check for rowbreaks is not really necessary). 1402 if (parserExpects != ParserExpects.NORMAL) { 1403 parserExpects = ParserExpects.NORMAL; 1404 tmpBuf.append(stringToWrite); 1405 stringToWrite = tmpBuf.toString(); 1406 tmpBuf.setLength(0); 1407 } 1408 } 1409 else if (stk.ttype == '$') { 1410 if (parserExpects != ParserExpects.NORMAL) { 1411 stringToWrite = tmpBuf.toString(); 1412 tmpBuf.setLength(0); 1413 } 1414 tmpBuf.appendCodePoint(stk.ttype); 1415 parserExpects = ParserExpects.BRACKET_OPEN; 1416 } 1417 else if (stk.ttype == '{') { 1418 switch (parserExpects) { 1419 case NORMAL: 1420 stringToWrite = new String(Character.toChars(stk.ttype)); 1421 break; 1422 case BRACKET_OPEN: 1423 tmpBuf.appendCodePoint(stk.ttype); 1424 parserExpects = ParserExpects.VARIABLE; 1425 break; 1426 default: 1427 parserExpects = ParserExpects.NORMAL; 1428 tmpBuf.appendCodePoint(stk.ttype); 1429 stringToWrite = tmpBuf.toString(); 1430 tmpBuf.setLength(0); 1431 } 1432 } 1433 else if (stk.ttype == '}') { 1434 switch (parserExpects) { 1435 case NORMAL: 1436 stringToWrite = new String(Character.toChars(stk.ttype)); 1437 break; 1438 case BRACKET_CLOSE: 1439 parserExpects = ParserExpects.NORMAL; 1440 tmpBuf.appendCodePoint(stk.ttype); 1441 1442 if (variableName == null) 1443 throw new IllegalStateException("variableName is null!!!"); 1444 1445 Object variableValue = variables.get(variableName); 1446 stringToWrite = variableValue == null ? null : variableValue.toString(); 1447 if (stringToWrite == null) { 1448 logger.warn("Variable " + tmpBuf.toString() + " occuring in template is unknown!"); 1449 stringToWrite = tmpBuf.toString(); 1450 } 1451 tmpBuf.setLength(0); 1452 break; 1453 default: 1454 parserExpects = ParserExpects.NORMAL; 1455 tmpBuf.appendCodePoint(stk.ttype); 1456 stringToWrite = tmpBuf.toString(); 1457 tmpBuf.setLength(0); 1458 } 1459 } 1460 1461 if (stringToWrite != null) 1462 writer.write(stringToWrite); 1463 } // while (stk.nextToken() != StreamTokenizer.TT_EOF) { 1464 } 1465 1466 public static byte[] getBytesFromFile(File file) throws IOException { 1467 // Get the size of the file 1468 long length = file.length(); 1469 byte[] bytes = new byte[(int)length]; 1470 1471 InputStream is = new FileInputStream(file); 1472 try { 1473 // Read in the bytes 1474 int offset = 0; 1475 int numRead = 0; 1476 while (offset < bytes.length && (numRead = is.read(bytes, offset, bytes.length-offset)) >= 0) { 1477 offset += numRead; 1478 } 1479 1480 // Ensure all the bytes have been read in 1481 if (offset < bytes.length) { 1482 throw new IOException("Could not completely read file "+file.getName()); 1483 } 1484 } finally { 1485 // Close the input stream and return bytes 1486 is.close(); 1487 } 1488 return bytes; 1489 } 1490 1491 public static File createCollisionFile(File file) { 1492 final File parentFile = file.getParentFile(); 1493 final String fileName = file.getName(); 1494 1495 String fileExtension = nullToEmptyString(getFileExtension(fileName)); 1496 if (!fileExtension.isEmpty()) 1497 fileExtension = '.' + fileExtension; 1498 1499 final File result = new File(parentFile, 1500 String.format("%s.%s%s%s", 1501 fileName, 1502 Long.toString(System.currentTimeMillis(), 36), 1503 COLLISION_FILE_NAME_INFIX, 1504 fileExtension)); 1505 return result; 1506 } 1507 1508 private static String nullToEmptyString(String s) { 1509 return s == null ? "" : s; 1510 } 1511 1512 public static String toPathString(final Path path) { 1513 assertNotNull("path", path); 1514 return path.toString().replace(File.separatorChar, '/'); 1515 } 1516 1517 public static long getLastModifiedNoFollow(final File file) { 1518 return getLastModifiedNoFollow(file.toPath()); 1519 } 1520 1521 public static long getLastModifiedNoFollow(final Path path) { 1522 try { 1523 BasicFileAttributes attributes = Files.readAttributes(path, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS); 1524 return attributes.lastModifiedTime().toMillis(); 1525 } catch (IOException e) { 1526 throw new RuntimeException(e); 1527 } 1528 } 1529 1530 public static void setLastModifiedNoFollow(final File file, final long lastModified) { 1531 setLastModifiedNoFollow(file.toPath(), lastModified); 1532 } 1533 1534 public static void setLastModifiedNoFollow(Path path, final long lastModified) { 1535 path = path.toAbsolutePath(); 1536 final List<Throwable> errors = new ArrayList<>(); 1537 1538 final FileTime lastModifiedTime = FileTime.fromMillis(lastModified); 1539 try { 1540 Files.getFileAttributeView(path, BasicFileAttributeView.class, LinkOption.NOFOLLOW_LINKS) 1541 .setTimes(lastModifiedTime, null, null); 1542 1543 return; 1544 } catch (IOException e) { 1545 errors.add(e); 1546 } 1547 1548 // It's currently impossible to modify the 'lastModified' timestamp of a symlink :-( 1549 // http://stackoverflow.com/questions/17308363/symlink-lastmodifiedtime-in-java-1-7 1550 // Therefore, we fall back to the touch command, if the above code failed. 1551 1552 final String timestamp = new SimpleDateFormat("YYYYMMddHHmm.ss").format(new Date(lastModified)); 1553 final ProcessBuilder processBuilder = new ProcessBuilder("touch", "-c", "-h", "-m", "-t", timestamp, path.toString()); 1554 processBuilder.redirectErrorStream(true); 1555 try { 1556 final Process process = processBuilder.start(); 1557 final ByteArrayOutputStream stdOut = new ByteArrayOutputStream(); 1558 final int processExitCode; 1559 final DumpStreamThread dumpInputStreamThread = new DumpStreamThread(process.getInputStream(), stdOut, null); 1560 try { 1561 dumpInputStreamThread.start(); 1562 processExitCode = process.waitFor(); 1563 } finally { 1564 dumpInputStreamThread.flushBuffer(); 1565 dumpInputStreamThread.interrupt(); 1566 } 1567 1568 if (processExitCode != 0) { 1569 final String stdOutString = new String(stdOut.toByteArray()); 1570 throw new IOException(String.format( 1571 "Command 'touch' failed with exitCode=%s and the following message: %s", 1572 processExitCode, stdOutString)); 1573 } 1574 1575 return; 1576 } catch (IOException | InterruptedException e) { 1577 errors.add(e); 1578 } 1579 1580 if (!errors.isEmpty()) { 1581 logger.error("Setting the lastModified timestamp of '{}' failed with the following errors:", path); 1582 for (Throwable error : errors) { 1583 logger.error("" + error, error); 1584 } 1585 } 1586 } 1587}