001 /** 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017 package org.apache.camel.util; 018 019 020 import java.io.File; 021 import java.io.FileInputStream; 022 import java.io.FileOutputStream; 023 import java.io.IOException; 024 import java.net.URI; 025 import java.net.URISyntaxException; 026 import java.nio.channels.FileChannel; 027 import java.util.Iterator; 028 import java.util.Locale; 029 import java.util.Random; 030 import java.util.Stack; 031 032 import org.slf4j.Logger; 033 import org.slf4j.LoggerFactory; 034 035 /** 036 * File utilities 037 */ 038 public final class FileUtil { 039 040 public static final int BUFFER_SIZE = 128 * 1024; 041 042 private static final transient Logger LOG = LoggerFactory.getLogger(FileUtil.class); 043 private static final int RETRY_SLEEP_MILLIS = 10; 044 private static File defaultTempDir; 045 046 // current value of the "user.dir" property 047 private static String gUserDir; 048 // cached URI object for the current value of the escaped "user.dir" property stored as a URI 049 private static URI gUserDirURI; 050 // which ASCII characters need to be escaped 051 private static boolean gNeedEscaping[] = new boolean[128]; 052 // the first hex character if a character needs to be escaped 053 private static char gAfterEscaping1[] = new char[128]; 054 // the second hex character if a character needs to be escaped 055 private static char gAfterEscaping2[] = new char[128]; 056 private static char[] gHexChs = {'0', '1', '2', '3', '4', '5', '6', '7', 057 '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'}; 058 // initialize the above 3 arrays 059 static { 060 for (int i = 0; i <= 0x1f; i++) { 061 gNeedEscaping[i] = true; 062 gAfterEscaping1[i] = gHexChs[i >> 4]; 063 gAfterEscaping2[i] = gHexChs[i & 0xf]; 064 } 065 gNeedEscaping[0x7f] = true; 066 gAfterEscaping1[0x7f] = '7'; 067 gAfterEscaping2[0x7f] = 'F'; 068 char[] escChs = {' ', '<', '>', '#', '%', '"', '{', '}', 069 '|', '\\', '^', '~', '[', ']', '`'}; 070 int len = escChs.length; 071 char ch; 072 for (int i = 0; i < len; i++) { 073 ch = escChs[i]; 074 gNeedEscaping[ch] = true; 075 gAfterEscaping1[ch] = gHexChs[ch >> 4]; 076 gAfterEscaping2[ch] = gHexChs[ch & 0xf]; 077 } 078 } 079 080 private FileUtil() { 081 // Utils method 082 } 083 084 085 // To escape the "user.dir" system property, by using %HH to represent 086 // special ASCII characters: 0x00~0x1F, 0x7F, ' ', '<', '>', '#', '%' 087 // and '"'. It's a static method, so needs to be synchronized. 088 // this method looks heavy, but since the system property isn't expected 089 // to change often, so in most cases, we only need to return the URI 090 // that was escaped before. 091 // According to the URI spec, non-ASCII characters (whose value >= 128) 092 // need to be escaped too. 093 // REVISIT: don't know how to escape non-ASCII characters, especially 094 // which encoding to use. Leave them for now. 095 public static synchronized URI getUserDir() throws URISyntaxException { 096 // get the user.dir property 097 String userDir = ""; 098 try { 099 userDir = System.getProperty("user.dir"); 100 } catch (SecurityException se) { 101 } 102 103 // return empty string if property value is empty string. 104 if (userDir.length() == 0) { 105 return new URI("file", "", "", null, null); 106 } 107 // compute the new escaped value if the new property value doesn't 108 // match the previous one 109 if (gUserDirURI != null && userDir.equals(gUserDir)) { 110 return gUserDirURI; 111 } 112 113 // record the new value as the global property value 114 gUserDir = userDir; 115 116 char separator = java.io.File.separatorChar; 117 userDir = userDir.replace(separator, '/'); 118 119 int len = userDir.length(); 120 int ch; 121 StringBuffer buffer = new StringBuffer(len * 3); 122 // change C:/blah to /C:/blah 123 if (len >= 2 && userDir.charAt(1) == ':') { 124 ch = Character.toUpperCase(userDir.charAt(0)); 125 if (ch >= 'A' && ch <= 'Z') { 126 buffer.append('/'); 127 } 128 } 129 130 // for each character in the path 131 int i = 0; 132 for (; i < len; i++) { 133 ch = userDir.charAt(i); 134 // if it's not an ASCII character, break here, and use UTF-8 encoding 135 if (ch >= 128) { 136 break; 137 } 138 if (gNeedEscaping[ch]) { 139 buffer.append('%'); 140 buffer.append(gAfterEscaping1[ch]); 141 buffer.append(gAfterEscaping2[ch]); 142 // record the fact that it's escaped 143 } else { 144 buffer.append((char)ch); 145 } 146 } 147 148 // we saw some non-ascii character 149 if (i < len) { 150 // get UTF-8 bytes for the remaining sub-string 151 byte[] bytes = null; 152 byte b; 153 try { 154 bytes = userDir.substring(i).getBytes("UTF-8"); 155 } catch (java.io.UnsupportedEncodingException e) { 156 // should never happen 157 return new URI("file", "", userDir, null, null); 158 } 159 len = bytes.length; 160 161 // for each byte 162 for (i = 0; i < len; i++) { 163 b = bytes[i]; 164 // for non-ascii character: make it positive, then escape 165 if (b < 0) { 166 ch = b + 256; 167 buffer.append('%'); 168 buffer.append(gHexChs[ch >> 4]); 169 buffer.append(gHexChs[ch & 0xf]); 170 } else if (gNeedEscaping[b]) { 171 buffer.append('%'); 172 buffer.append(gAfterEscaping1[b]); 173 buffer.append(gAfterEscaping2[b]); 174 } else { 175 buffer.append((char)b); 176 } 177 } 178 } 179 180 // change blah/blah to blah/blah/ 181 if (!userDir.endsWith("/")) { 182 buffer.append('/'); 183 } 184 185 gUserDirURI = new URI("file", "", buffer.toString(), null, null); 186 187 return gUserDirURI; 188 } 189 190 /** 191 * Normalizes the path to cater for Windows and other platforms 192 */ 193 public static String normalizePath(String path) { 194 if (path == null) { 195 return null; 196 } 197 198 if (isWindows()) { 199 // special handling for Windows where we need to convert / to \\ 200 return path.replace('/', '\\'); 201 } else { 202 // for other systems make sure we use / as separators 203 return path.replace('\\', '/'); 204 } 205 } 206 207 public static boolean isWindows() { 208 String osName = System.getProperty("os.name").toLowerCase(Locale.US); 209 return osName.indexOf("windows") > -1; 210 } 211 212 public static File createTempFile(String prefix, String suffix) throws IOException { 213 return createTempFile(prefix, suffix, null); 214 } 215 216 public static File createTempFile(String prefix, String suffix, File parentDir) throws IOException { 217 File parent = (parentDir == null) ? getDefaultTempDir() : parentDir; 218 219 if (suffix == null) { 220 suffix = ".tmp"; 221 } 222 if (prefix == null) { 223 prefix = "camel"; 224 } else if (prefix.length() < 3) { 225 prefix = prefix + "camel"; 226 } 227 228 // create parent folder 229 parent.mkdirs(); 230 231 return File.createTempFile(prefix, suffix, parent); 232 } 233 234 /** 235 * Strip any leading separators 236 */ 237 public static String stripLeadingSeparator(String name) { 238 if (name == null) { 239 return null; 240 } 241 while (name.startsWith("/") || name.startsWith(File.separator)) { 242 name = name.substring(1); 243 } 244 return name; 245 } 246 247 /** 248 * Does the name start with a leading separator 249 */ 250 public static boolean hasLeadingSeparator(String name) { 251 if (name == null) { 252 return false; 253 } 254 if (name.startsWith("/") || name.startsWith(File.separator)) { 255 return true; 256 } 257 return false; 258 } 259 260 /** 261 * Strip first leading separator 262 */ 263 public static String stripFirstLeadingSeparator(String name) { 264 if (name == null) { 265 return null; 266 } 267 if (name.startsWith("/") || name.startsWith(File.separator)) { 268 name = name.substring(1); 269 } 270 return name; 271 } 272 273 /** 274 * Strip any trailing separators 275 */ 276 public static String stripTrailingSeparator(String name) { 277 if (ObjectHelper.isEmpty(name)) { 278 return name; 279 } 280 281 String s = name; 282 283 // there must be some leading text, as we should only remove trailing separators 284 while (s.endsWith("/") || s.endsWith(File.separator)) { 285 s = s.substring(0, s.length() - 1); 286 } 287 288 // if the string is empty, that means there was only trailing slashes, and no leading text 289 // and so we should then return the original name as is 290 if (ObjectHelper.isEmpty(s)) { 291 return name; 292 } else { 293 // return without trailing slashes 294 return s; 295 } 296 } 297 298 /** 299 * Strips any leading paths 300 */ 301 public static String stripPath(String name) { 302 if (name == null) { 303 return null; 304 } 305 int posUnix = name.lastIndexOf('/'); 306 int posWin = name.lastIndexOf('\\'); 307 int pos = Math.max(posUnix, posWin); 308 309 if (pos != -1) { 310 return name.substring(pos + 1); 311 } 312 return name; 313 } 314 315 public static String stripExt(String name) { 316 if (name == null) { 317 return null; 318 } 319 int pos = name.lastIndexOf('.'); 320 if (pos != -1) { 321 return name.substring(0, pos); 322 } 323 return name; 324 } 325 326 /** 327 * Returns only the leading path (returns <tt>null</tt> if no path) 328 */ 329 public static String onlyPath(String name) { 330 if (name == null) { 331 return null; 332 } 333 334 int posUnix = name.lastIndexOf('/'); 335 int posWin = name.lastIndexOf('\\'); 336 int pos = Math.max(posUnix, posWin); 337 338 if (pos > 0) { 339 return name.substring(0, pos); 340 } else if (pos == 0) { 341 // name is in the root path, so extract the path as the first char 342 return name.substring(0, 1); 343 } 344 // no path in name 345 return null; 346 } 347 348 /** 349 * Compacts a path by stacking it and reducing <tt>..</tt>, 350 * and uses OS specific file separators (eg {@link java.io.File#separator}). 351 */ 352 public static String compactPath(String path) { 353 return compactPath(path, File.separatorChar); 354 } 355 356 /** 357 * Compacts a path by stacking it and reducing <tt>..</tt>, 358 * and uses the given separator. 359 */ 360 public static String compactPath(String path, char separator) { 361 if (path == null) { 362 return null; 363 } 364 365 // only normalize if contains a path separator 366 if (path.indexOf('/') == -1 && path.indexOf('\\') == -1) { 367 return path; 368 } 369 370 // need to normalize path before compacting 371 path = normalizePath(path); 372 373 // preserve ending slash if given in input path 374 boolean endsWithSlash = path.endsWith("/") || path.endsWith("\\"); 375 376 // preserve starting slash if given in input path 377 boolean startsWithSlash = path.startsWith("/") || path.startsWith("\\"); 378 379 Stack<String> stack = new Stack<String>(); 380 381 // separator can either be windows or unix style 382 String separatorRegex = "\\\\|/"; 383 String[] parts = path.split(separatorRegex); 384 for (String part : parts) { 385 if (part.equals("..") && !stack.isEmpty() && !"..".equals(stack.peek())) { 386 // only pop if there is a previous path, which is not a ".." path either 387 stack.pop(); 388 } else if (part.equals(".") || part.isEmpty()) { 389 // do nothing because we don't want a path like foo/./bar or foo//bar 390 } else { 391 stack.push(part); 392 } 393 } 394 395 // build path based on stack 396 StringBuilder sb = new StringBuilder(); 397 398 if (startsWithSlash) { 399 sb.append(separator); 400 } 401 402 for (Iterator<String> it = stack.iterator(); it.hasNext();) { 403 sb.append(it.next()); 404 if (it.hasNext()) { 405 sb.append(separator); 406 } 407 } 408 409 if (endsWithSlash) { 410 sb.append(separator); 411 } 412 413 return sb.toString(); 414 } 415 416 private static synchronized File getDefaultTempDir() { 417 if (defaultTempDir != null && defaultTempDir.exists()) { 418 return defaultTempDir; 419 } 420 421 String s = System.getProperty("java.io.tmpdir"); 422 File checkExists = new File(s); 423 if (!checkExists.exists()) { 424 throw new RuntimeException("The directory " 425 + checkExists.getAbsolutePath() 426 + " does not exist, please set java.io.tempdir" 427 + " to an existing directory"); 428 } 429 430 // create a sub folder with a random number 431 Random ran = new Random(); 432 int x = ran.nextInt(1000000); 433 434 File f = new File(s, "camel-tmp-" + x); 435 while (!f.mkdir()) { 436 x = ran.nextInt(1000000); 437 f = new File(s, "camel-tmp-" + x); 438 } 439 440 defaultTempDir = f; 441 442 // create shutdown hook to remove the temp dir 443 Thread hook = new Thread() { 444 @Override 445 public void run() { 446 removeDir(defaultTempDir); 447 } 448 }; 449 Runtime.getRuntime().addShutdownHook(hook); 450 451 return defaultTempDir; 452 } 453 454 private static void removeDir(File d) { 455 String[] list = d.list(); 456 if (list == null) { 457 list = new String[0]; 458 } 459 for (String s : list) { 460 File f = new File(d, s); 461 if (f.isDirectory()) { 462 removeDir(f); 463 } else { 464 delete(f); 465 } 466 } 467 delete(d); 468 } 469 470 private static void delete(File f) { 471 if (!f.delete()) { 472 if (isWindows()) { 473 System.gc(); 474 } 475 try { 476 Thread.sleep(RETRY_SLEEP_MILLIS); 477 } catch (InterruptedException ex) { 478 // Ignore Exception 479 } 480 if (!f.delete()) { 481 f.deleteOnExit(); 482 } 483 } 484 } 485 486 /** 487 * Renames a file. 488 * 489 * @param from the from file 490 * @param to the to file 491 * @param copyAndDeleteOnRenameFail whether to fallback and do copy and delete, if renameTo fails 492 * @return <tt>true</tt> if the file was renamed, otherwise <tt>false</tt> 493 * @throws java.io.IOException is thrown if error renaming file 494 */ 495 public static boolean renameFile(File from, File to, boolean copyAndDeleteOnRenameFail) throws IOException { 496 // do not try to rename non existing files 497 if (!from.exists()) { 498 return false; 499 } 500 501 // some OS such as Windows can have problem doing rename IO operations so we may need to 502 // retry a couple of times to let it work 503 boolean renamed = false; 504 int count = 0; 505 while (!renamed && count < 3) { 506 if (LOG.isDebugEnabled() && count > 0) { 507 LOG.debug("Retrying attempt {} to rename file from: {} to: {}", new Object[]{count, from, to}); 508 } 509 510 renamed = from.renameTo(to); 511 if (!renamed && count > 0) { 512 try { 513 Thread.sleep(1000); 514 } catch (InterruptedException e) { 515 // ignore 516 } 517 } 518 count++; 519 } 520 521 // we could not rename using renameTo, so lets fallback and do a copy/delete approach. 522 // for example if you move files between different file systems (linux -> windows etc.) 523 if (!renamed && copyAndDeleteOnRenameFail) { 524 // now do a copy and delete as all rename attempts failed 525 LOG.debug("Cannot rename file from: {} to: {}, will now use a copy/delete approach instead", from, to); 526 copyFile(from, to); 527 if (!deleteFile(from)) { 528 throw new IOException("Renaming file from: " + from + " to: " + to + " failed due cannot delete from file: " + from + " after copy succeeded"); 529 } else { 530 renamed = true; 531 } 532 } 533 534 if (LOG.isDebugEnabled() && count > 0) { 535 LOG.debug("Tried {} to rename file: {} to: {} with result: {}", new Object[]{count, from, to, renamed}); 536 } 537 return renamed; 538 } 539 540 @SuppressWarnings("resource") 541 public static void copyFile(File from, File to) throws IOException { 542 FileChannel in = new FileInputStream(from).getChannel(); 543 FileChannel out = new FileOutputStream(to).getChannel(); 544 try { 545 if (LOG.isTraceEnabled()) { 546 LOG.trace("Using FileChannel to copy from: " + in + " to: " + out); 547 } 548 549 long size = in.size(); 550 long position = 0; 551 while (position < size) { 552 position += in.transferTo(position, BUFFER_SIZE, out); 553 } 554 } finally { 555 IOHelper.close(in, from.getName(), LOG); 556 IOHelper.close(out, to.getName(), LOG); 557 } 558 } 559 560 public static boolean deleteFile(File file) { 561 // do not try to delete non existing files 562 if (!file.exists()) { 563 return false; 564 } 565 566 // some OS such as Windows can have problem doing delete IO operations so we may need to 567 // retry a couple of times to let it work 568 boolean deleted = false; 569 int count = 0; 570 while (!deleted && count < 3) { 571 LOG.debug("Retrying attempt {} to delete file: {}", count, file); 572 573 deleted = file.delete(); 574 if (!deleted && count > 0) { 575 try { 576 Thread.sleep(1000); 577 } catch (InterruptedException e) { 578 // ignore 579 } 580 } 581 count++; 582 } 583 584 585 if (LOG.isDebugEnabled() && count > 0) { 586 LOG.debug("Tried {} to delete file: {} with result: {}", new Object[]{count, file, deleted}); 587 } 588 return deleted; 589 } 590 591 /** 592 * Is the given file an absolute file. 593 * <p/> 594 * Will also work around issue on Windows to consider files on Windows starting with a \ 595 * as absolute files. This makes the logic consistent across all OS platforms. 596 * 597 * @param file the file 598 * @return <tt>true</ff> if its an absolute path, <tt>false</tt> otherwise. 599 */ 600 public static boolean isAbsolute(File file) { 601 if (isWindows()) { 602 // special for windows 603 String path = file.getPath(); 604 if (path.startsWith(File.separator)) { 605 return true; 606 } 607 } 608 return file.isAbsolute(); 609 } 610 611 /** 612 * Creates a new file. 613 * 614 * @param file the file 615 * @return <tt>true</tt> if created a new file, <tt>false</tt> otherwise 616 * @throws IOException is thrown if error creating the new file 617 */ 618 public static boolean createNewFile(File file) throws IOException { 619 try { 620 return file.createNewFile(); 621 } catch (IOException e) { 622 if (file.exists()) { 623 return true; 624 } else { 625 throw e; 626 } 627 } 628 } 629 630 }