001// Generated by delombok at Mon Jan 02 16:27:27 UTC 2023 002package io.cui.tools.io; 003 004import java.io.File; 005import java.util.ArrayDeque; 006import java.util.ArrayList; 007import java.util.Collection; 008import java.util.Deque; 009 010/** 011 * Copied from commons.io:org.apache.commons.io.FilenameUtils 012 * <p> 013 * General filename and filepath manipulation utilities. 014 * <p> 015 * When dealing with filenames you can hit problems when moving from a Windows 016 * based development machine to a Unix based production machine. 017 * This class aims to help avoid those problems. 018 * <p> 019 * <b>NOTE</b>: You may be able to avoid using this class entirely simply by 020 * using JDK {@link java.io.File File} objects and the two argument constructor 021 * {@link java.io.File#File(java.io.File, java.lang.String) File(File,String)}. 022 * <p> 023 * Most methods on this class are designed to work the same on both Unix and Windows. 024 * Those that don't include 'System', 'Unix' or 'Windows' in their name. 025 * <p> 026 * Most methods recognise both separators (forward and back), and both 027 * sets of prefixes. See the javadoc of each method for details. 028 * <p> 029 * This class defines six components within a filename 030 * (example C:\dev\project\file.txt): 031 * <ul> 032 * <li>the prefix - C:\</li> 033 * <li>the path - dev\project\</li> 034 * <li>the full path - C:\dev\project\</li> 035 * <li>the name - file.txt</li> 036 * <li>the base name - file</li> 037 * <li>the extension - txt</li> 038 * </ul> 039 * Note that this class works best if directory filenames end with a separator. 040 * If you omit the last separator, it is impossible to determine if the filename 041 * corresponds to a file or a directory. As a result, we have chosen to say 042 * it corresponds to a file. 043 * <p> 044 * This class only supports Unix and Windows style names. 045 * Prefixes are matched as follows: 046 * 047 * <pre> 048 * Windows: 049 * a\b\c.txt --> "" --> relative 050 * \a\b\c.txt --> "\" --> current drive absolute 051 * C:a\b\c.txt --> "C:" --> drive relative 052 * C:\a\b\c.txt --> "C:\" --> absolute 053 * \\server\a\b\c.txt --> "\\server\" --> UNC 054 * 055 * Unix: 056 * a/b/c.txt --> "" --> relative 057 * /a/b/c.txt --> "/" --> absolute 058 * ~/a/b/c.txt --> "~/" --> current user 059 * ~ --> "~/" --> current user (slash added) 060 * ~user/a/b/c.txt --> "~user/" --> named user 061 * ~user --> "~user/" --> named user (slash added) 062 * </pre> 063 * 064 * Both prefix styles are matched always, irrespective of the machine that you are 065 * currently running on. 066 * <p> 067 * Origin of code: Excalibur, Alexandria, Tomcat, Commons-Utils. 068 * </p> 069 */ 070public final class FilenameUtils { 071 private static final int NOT_FOUND = -1; 072 /** 073 * The extension separator character. 074 */ 075 public static final char EXTENSION_SEPARATOR = '.'; 076 /** 077 * The extension separator String. 078 */ 079 public static final String EXTENSION_SEPARATOR_STR = Character.toString(EXTENSION_SEPARATOR); 080 /** 081 * The Unix separator character. 082 */ 083 private static final char UNIX_SEPARATOR = '/'; 084 /** 085 * The Windows separator character. 086 */ 087 private static final char WINDOWS_SEPARATOR = '\\'; 088 /** 089 * The system separator character. 090 */ 091 private static final char SYSTEM_SEPARATOR = File.separatorChar; 092 /** 093 * The separator character that is the opposite of the system separator. 094 */ 095 private static final char OTHER_SEPARATOR; 096 097 static { 098 if (isSystemWindows()) { 099 OTHER_SEPARATOR = UNIX_SEPARATOR; 100 } else { 101 OTHER_SEPARATOR = WINDOWS_SEPARATOR; 102 } 103 } 104 105 // ----------------------------------------------------------------------- 106 /** 107 * Determines if Windows file system is in use. 108 * 109 * @return true if the system is Windows 110 */ 111 static boolean isSystemWindows() { 112 return SYSTEM_SEPARATOR == WINDOWS_SEPARATOR; 113 } 114 115 // ----------------------------------------------------------------------- 116 /** 117 * Checks if the character is a separator. 118 * 119 * @param ch the character to check 120 * @return true if it is a separator character 121 */ 122 private static boolean isSeparator(final char ch) { 123 return ch == UNIX_SEPARATOR || ch == WINDOWS_SEPARATOR; 124 } 125 126 // ----------------------------------------------------------------------- 127 /** 128 * Normalizes a path, removing double and single dot path steps. 129 * <p> 130 * This method normalizes a path to a standard format. 131 * The input may contain separators in either Unix or Windows format. 132 * The output will contain separators in the format of the system. 133 * <p> 134 * A trailing slash will be retained. 135 * A double slash will be merged to a single slash (but UNC names are handled). 136 * A single dot path segment will be removed. 137 * A double dot will cause that path segment and the one before to be removed. 138 * If the double dot has no parent path segment to work with, {@code null} 139 * is returned. 140 * <p> 141 * The output will be the same on both Unix and Windows except 142 * for the separator character. 143 * 144 * <pre> 145 * /foo// --> /foo/ 146 * /foo/./ --> /foo/ 147 * /foo/../bar --> /bar 148 * /foo/../bar/ --> /bar/ 149 * /foo/../bar/../baz --> /baz 150 * //foo//./bar --> /foo/bar 151 * /../ --> null 152 * ../foo --> null 153 * foo/bar/.. --> foo/ 154 * foo/../../bar --> null 155 * foo/../bar --> bar 156 * //server/foo/../bar --> //server/bar 157 * //server/../bar --> null 158 * C:\foo\..\bar --> C:\bar 159 * C:\..\bar --> null 160 * ~/foo/../bar/ --> ~/bar/ 161 * ~/../bar --> null 162 * </pre> 163 * 164 * (Note the file separator returned will be correct for Windows/Unix) 165 * 166 * @param filename the filename to normalize, null returns null 167 * @return the normalized filename, or null if invalid. Null bytes inside string will be removed 168 */ 169 public static String normalize(final String filename) { 170 return doNormalize(filename, SYSTEM_SEPARATOR, true); 171 } 172 173 /** 174 * Normalizes a path, removing double and single dot path steps. 175 * <p> 176 * This method normalizes a path to a standard format. 177 * The input may contain separators in either Unix or Windows format. 178 * The output will contain separators in the format specified. 179 * <p> 180 * A trailing slash will be retained. 181 * A double slash will be merged to a single slash (but UNC names are handled). 182 * A single dot path segment will be removed. 183 * A double dot will cause that path segment and the one before to be removed. 184 * If the double dot has no parent path segment to work with, {@code null} 185 * is returned. 186 * <p> 187 * The output will be the same on both Unix and Windows except 188 * for the separator character. 189 * 190 * <pre> 191 * /foo// --> /foo/ 192 * /foo/./ --> /foo/ 193 * /foo/../bar --> /bar 194 * /foo/../bar/ --> /bar/ 195 * /foo/../bar/../baz --> /baz 196 * //foo//./bar --> /foo/bar 197 * /../ --> null 198 * ../foo --> null 199 * foo/bar/.. --> foo/ 200 * foo/../../bar --> null 201 * foo/../bar --> bar 202 * //server/foo/../bar --> //server/bar 203 * //server/../bar --> null 204 * C:\foo\..\bar --> C:\bar 205 * C:\..\bar --> null 206 * ~/foo/../bar/ --> ~/bar/ 207 * ~/../bar --> null 208 * </pre> 209 * 210 * The output will be the same on both Unix and Windows including 211 * the separator character. 212 * 213 * @param filename the filename to normalize, null returns null 214 * @param unixSeparator {@code true} if a unix separator should 215 * be used or {@code false} if a windows separator should be used. 216 * @return the normalized filename, or null if invalid. Null bytes inside string will be removed 217 */ 218 public static String normalize(final String filename, final boolean unixSeparator) { 219 final char separator = unixSeparator ? UNIX_SEPARATOR : WINDOWS_SEPARATOR; 220 return doNormalize(filename, separator, true); 221 } 222 223 // ----------------------------------------------------------------------- 224 /** 225 * Normalizes a path, removing double and single dot path steps, 226 * and removing any final directory separator. 227 * <p> 228 * This method normalizes a path to a standard format. 229 * The input may contain separators in either Unix or Windows format. 230 * The output will contain separators in the format of the system. 231 * <p> 232 * A trailing slash will be removed. 233 * A double slash will be merged to a single slash (but UNC names are handled). 234 * A single dot path segment will be removed. 235 * A double dot will cause that path segment and the one before to be removed. 236 * If the double dot has no parent path segment to work with, {@code null} 237 * is returned. 238 * <p> 239 * The output will be the same on both Unix and Windows except 240 * for the separator character. 241 * 242 * <pre> 243 * /foo// --> /foo 244 * /foo/./ --> /foo 245 * /foo/../bar --> /bar 246 * /foo/../bar/ --> /bar 247 * /foo/../bar/../baz --> /baz 248 * //foo//./bar --> /foo/bar 249 * /../ --> null 250 * ../foo --> null 251 * foo/bar/.. --> foo 252 * foo/../../bar --> null 253 * foo/../bar --> bar 254 * //server/foo/../bar --> //server/bar 255 * //server/../bar --> null 256 * C:\foo\..\bar --> C:\bar 257 * C:\..\bar --> null 258 * ~/foo/../bar/ --> ~/bar 259 * ~/../bar --> null 260 * </pre> 261 * 262 * (Note the file separator returned will be correct for Windows/Unix) 263 * 264 * @param filename the filename to normalize, null returns null 265 * @return the normalized filename, or null if invalid. Null bytes inside string will be removed 266 */ 267 public static String normalizeNoEndSeparator(final String filename) { 268 return doNormalize(filename, SYSTEM_SEPARATOR, false); 269 } 270 271 /** 272 * Normalizes a path, removing double and single dot path steps, 273 * and removing any final directory separator. 274 * <p> 275 * This method normalizes a path to a standard format. 276 * The input may contain separators in either Unix or Windows format. 277 * The output will contain separators in the format specified. 278 * <p> 279 * A trailing slash will be removed. 280 * A double slash will be merged to a single slash (but UNC names are handled). 281 * A single dot path segment will be removed. 282 * A double dot will cause that path segment and the one before to be removed. 283 * If the double dot has no parent path segment to work with, {@code null} 284 * is returned. 285 * <p> 286 * The output will be the same on both Unix and Windows including 287 * the separator character. 288 * 289 * <pre> 290 * /foo// --> /foo 291 * /foo/./ --> /foo 292 * /foo/../bar --> /bar 293 * /foo/../bar/ --> /bar 294 * /foo/../bar/../baz --> /baz 295 * //foo//./bar --> /foo/bar 296 * /../ --> null 297 * ../foo --> null 298 * foo/bar/.. --> foo 299 * foo/../../bar --> null 300 * foo/../bar --> bar 301 * //server/foo/../bar --> //server/bar 302 * //server/../bar --> null 303 * C:\foo\..\bar --> C:\bar 304 * C:\..\bar --> null 305 * ~/foo/../bar/ --> ~/bar 306 * ~/../bar --> null 307 * </pre> 308 * 309 * @param filename the filename to normalize, null returns null 310 * @param unixSeparator {@code true} if a unix separator should 311 * be used or {@code false} if a windows separator should be used. 312 * @return the normalized filename, or null if invalid. Null bytes inside string will be removed 313 */ 314 public static String normalizeNoEndSeparator(final String filename, final boolean unixSeparator) { 315 final char separator = unixSeparator ? UNIX_SEPARATOR : WINDOWS_SEPARATOR; 316 return doNormalize(filename, separator, false); 317 } 318 319 // owolff: 320 // original 321 // code 322 /** 323 * Internal method to perform the normalization. 324 * 325 * @param filename the filename 326 * @param separator The separator character to use 327 * @param keepSeparator true to keep the final separator 328 * @return the normalized filename. Null bytes inside string will be removed. 329 */ 330 @SuppressWarnings({"squid:S3776", "squid:LabelsShouldNotBeUsedCheck", "squid:ForLoopCounterChangedCheck"}) 331 private static String doNormalize(final String filename, final char separator, final boolean keepSeparator) { 332 if (filename == null) { 333 return null; 334 } 335 failIfNullBytePresent(filename); 336 int size = filename.length(); 337 if (size == 0) { 338 return filename; 339 } 340 final int prefix = getPrefixLength(filename); 341 if (prefix < 0) { 342 return null; 343 } 344 final char[] array = new char[size + 2]; // +1 for possible extra slash, +2 for arraycopy 345 filename.getChars(0, filename.length(), array, 0); 346 // fix separators throughout 347 final char otherSeparator = separator == SYSTEM_SEPARATOR ? OTHER_SEPARATOR : SYSTEM_SEPARATOR; 348 for (int i = 0; i < array.length; i++) { 349 if (array[i] == otherSeparator) { 350 array[i] = separator; 351 } 352 } 353 // add extra separator on the end to simplify code below 354 boolean lastIsDirectory = true; 355 if (array[size - 1] != separator) { 356 array[size++] = separator; 357 lastIsDirectory = false; 358 } 359 // adjoining slashes 360 for (int i = prefix + 1; i < size; i++) { 361 if (array[i] == separator && array[i - 1] == separator) { 362 System.arraycopy(array, i, array, i - 1, size - i); 363 size--; 364 i--; 365 } 366 } 367 // dot slash 368 for (int i = prefix + 1; i < size; i++) { 369 if (array[i] == separator && array[i - 1] == '.' && (i == prefix + 1 || array[i - 2] == separator)) { 370 if (i == size - 1) { 371 lastIsDirectory = true; 372 } 373 System.arraycopy(array, i + 1, array, i - 1, size - i); 374 size -= 2; 375 i--; 376 } 377 } 378 // double dot slash 379 outer: 380 for (int i = prefix + 2; i < size; i++) { 381 if (array[i] == separator && array[i - 1] == '.' && array[i - 2] == '.' && (i == prefix + 2 || array[i - 3] == separator)) { 382 if (i == prefix + 2) { 383 return null; 384 } 385 if (i == size - 1) { 386 lastIsDirectory = true; 387 } 388 int j; 389 for (j = i - 4; j >= prefix; j--) { 390 if (array[j] == separator) { 391 // remove b/../ from a/b/../c 392 System.arraycopy(array, i + 1, array, j + 1, size - i); 393 size -= i - j; 394 i = j + 1; 395 continue outer; 396 } 397 } 398 // remove a/../ from a/../c 399 System.arraycopy(array, i + 1, array, prefix, size - i); 400 size -= i + 1 - prefix; 401 i = prefix + 1; 402 } 403 } 404 if (size <= 0) { 405 // should never be less than 0 406 return ""; 407 } 408 if ((size <= prefix) || (lastIsDirectory && keepSeparator)) { 409 return new String(array, 0, size); // keep trailing separator 410 } 411 return new String(array, 0, size - 1); // lose trailing separator 412 } 413 414 // ----------------------------------------------------------------------- 415 /** 416 * Concatenates a filename to a base path using normal command line style rules. 417 * <p> 418 * The effect is equivalent to resultant directory after changing 419 * directory to the first argument, followed by changing directory to 420 * the second argument. 421 * <p> 422 * The first argument is the base path, the second is the path to concatenate. 423 * The returned path is always normalized via {@link #normalize(String)}, 424 * thus <code>..</code> is handled. 425 * <p> 426 * If <code>pathToAdd</code> is absolute (has an absolute prefix), then 427 * it will be normalized and returned. 428 * Otherwise, the paths will be joined, normalized and returned. 429 * <p> 430 * The output will be the same on both Unix and Windows except 431 * for the separator character. 432 * 433 * <pre> 434 * /foo/ + bar --> /foo/bar 435 * /foo + bar --> /foo/bar 436 * /foo + /bar --> /bar 437 * /foo + C:/bar --> C:/bar 438 * /foo + C:bar --> C:bar (*) 439 * /foo/a/ + ../bar --> foo/bar 440 * /foo/ + ../../bar --> null 441 * /foo/ + /bar --> /bar 442 * /foo/.. + /bar --> /bar 443 * /foo + bar/c.txt --> /foo/bar/c.txt 444 * /foo/c.txt + bar --> /foo/c.txt/bar (!) 445 * </pre> 446 * 447 * (*) Note that the Windows relative drive prefix is unreliable when 448 * used with this method. 449 * (!) Note that the first parameter must be a path. If it ends with a name, then 450 * the name will be built into the concatenated path. If this might be a problem, 451 * use {@link #getFullPath(String)} on the base path argument. 452 * 453 * @param basePath the base path to attach to, always treated as a path 454 * @param fullFilenameToAdd the filename (or path) to attach to the base 455 * @return the concatenated path, or null if invalid. Null bytes inside string will be removed 456 */ 457 public static String concat(final String basePath, final String fullFilenameToAdd) { 458 final int prefix = getPrefixLength(fullFilenameToAdd); 459 if (prefix < 0) { 460 return null; 461 } 462 if (prefix > 0) { 463 return normalize(fullFilenameToAdd); 464 } 465 if (basePath == null) { 466 return null; 467 } 468 final int len = basePath.length(); 469 if (len == 0) { 470 return normalize(fullFilenameToAdd); 471 } 472 final char ch = basePath.charAt(len - 1); 473 if (isSeparator(ch)) { 474 return normalize(basePath + fullFilenameToAdd); 475 } else { 476 return normalize(basePath + '/' + fullFilenameToAdd); 477 } 478 } 479 480 /** 481 * Determines whether the {@code parent} directory contains the {@code child} element (a file or 482 * directory). 483 * <p> 484 * The files names are expected to be normalized. 485 * </p> 486 * 487 * Edge cases: 488 * <ul> 489 * <li>A {@code directory} must not be null: if null, throw IllegalArgumentException</li> 490 * <li>A directory does not contain itself: return false</li> 491 * <li>A null child file is not contained in any parent: return false</li> 492 * </ul> 493 * 494 * @param canonicalParent 495 * the file to consider as the parent. 496 * @param canonicalChild 497 * the file to consider as the child. 498 * @return true is the candidate leaf is under by the specified composite. False otherwise. 499 */ 500 public static boolean directoryContains(final String canonicalParent, final String canonicalChild) { 501 // Fail fast against NullPointerException 502 if (canonicalParent == null) { 503 throw new IllegalArgumentException("Directory must not be null"); 504 } 505 if ((canonicalChild == null) || IOCase.SYSTEM.checkEquals(canonicalParent, canonicalChild)) { 506 return false; 507 } 508 return IOCase.SYSTEM.checkStartsWith(canonicalChild, canonicalParent); 509 } 510 511 // ----------------------------------------------------------------------- 512 /** 513 * Converts all separators to the Unix separator of forward slash. 514 * 515 * @param path the path to be changed, null ignored 516 * @return the updated path 517 */ 518 public static String separatorsToUnix(final String path) { 519 if (path == null || path.indexOf(WINDOWS_SEPARATOR) == NOT_FOUND) { 520 return path; 521 } 522 return path.replace(WINDOWS_SEPARATOR, UNIX_SEPARATOR); 523 } 524 525 /** 526 * Converts all separators to the Windows separator of backslash. 527 * 528 * @param path the path to be changed, null ignored 529 * @return the updated path 530 */ 531 public static String separatorsToWindows(final String path) { 532 if (path == null || path.indexOf(UNIX_SEPARATOR) == NOT_FOUND) { 533 return path; 534 } 535 return path.replace(UNIX_SEPARATOR, WINDOWS_SEPARATOR); 536 } 537 538 /** 539 * Converts all separators to the system separator. 540 * 541 * @param path the path to be changed, null ignored 542 * @return the updated path 543 */ 544 public static String separatorsToSystem(final String path) { 545 if (path == null) { 546 return null; 547 } 548 if (isSystemWindows()) { 549 return separatorsToWindows(path); 550 } else { 551 return separatorsToUnix(path); 552 } 553 } 554 555 // ----------------------------------------------------------------------- 556 // owolff: original code 557 /** 558 * Returns the length of the filename prefix, such as <code>C:/</code> or <code>~/</code>. 559 * <p> 560 * This method will handle a file in either Unix or Windows format. 561 * <p> 562 * The prefix length includes the first slash in the full filename 563 * if applicable. Thus, it is possible that the length returned is greater 564 * than the length of the input string. 565 * 566 * <pre> 567 * Windows: 568 * a\b\c.txt --> "" --> relative 569 * \a\b\c.txt --> "\" --> current drive absolute 570 * C:a\b\c.txt --> "C:" --> drive relative 571 * C:\a\b\c.txt --> "C:\" --> absolute 572 * \\server\a\b\c.txt --> "\\server\" --> UNC 573 * \\\a\b\c.txt --> error, length = -1 574 * 575 * Unix: 576 * a/b/c.txt --> "" --> relative 577 * /a/b/c.txt --> "/" --> absolute 578 * ~/a/b/c.txt --> "~/" --> current user 579 * ~ --> "~/" --> current user (slash added) 580 * ~user/a/b/c.txt --> "~user/" --> named user 581 * ~user --> "~user/" --> named user (slash added) 582 * //server/a/b/c.txt --> "//server/" 583 * ///a/b/c.txt --> error, length = -1 584 * </pre> 585 * <p> 586 * The output will be the same irrespective of the machine that the code is running on. 587 * i.e. both Unix and Windows prefixes are matched regardless. 588 * <p> 589 * Note that a leading // (or \\) is used to indicate a UNC name on Windows. 590 * These must be followed by a server name, so double-slashes are not collapsed 591 * to a single slash at the start of the filename. 592 * 593 * @param filename the filename to find the prefix in, null returns -1 594 * @return the length of the prefix, -1 if invalid or null 595 */ 596 @SuppressWarnings({"squid:S3776"}) 597 public static int getPrefixLength(final String filename) { 598 if (filename == null) { 599 return NOT_FOUND; 600 } 601 final int len = filename.length(); 602 if (len == 0) { 603 return 0; 604 } 605 char ch0 = filename.charAt(0); 606 if (ch0 == ':') { 607 return NOT_FOUND; 608 } 609 if (len == 1) { 610 if (ch0 == '~') { 611 return 2; // return a length greater than the input 612 } 613 return isSeparator(ch0) ? 1 : 0; 614 } else { 615 if (ch0 == '~') { 616 int posUnix = filename.indexOf(UNIX_SEPARATOR, 1); 617 int posWin = filename.indexOf(WINDOWS_SEPARATOR, 1); 618 if (posUnix == NOT_FOUND && posWin == NOT_FOUND) { 619 return len + 1; // return a length greater than the input 620 } 621 posUnix = posUnix == NOT_FOUND ? posWin : posUnix; 622 posWin = posWin == NOT_FOUND ? posUnix : posWin; 623 return Math.min(posUnix, posWin) + 1; 624 } 625 final char ch1 = filename.charAt(1); 626 if (ch1 == ':') { 627 ch0 = Character.toUpperCase(ch0); 628 if (ch0 >= 'A' && ch0 <= 'Z') { 629 if (len == 2 || !isSeparator(filename.charAt(2))) { 630 return 2; 631 } 632 return 3; 633 } else if (ch0 == UNIX_SEPARATOR) { 634 return 1; 635 } 636 return NOT_FOUND; 637 } else if (isSeparator(ch0) && isSeparator(ch1)) { 638 int posUnix = filename.indexOf(UNIX_SEPARATOR, 2); 639 int posWin = filename.indexOf(WINDOWS_SEPARATOR, 2); 640 if (posUnix == NOT_FOUND && posWin == NOT_FOUND || posUnix == 2 || posWin == 2) { 641 return NOT_FOUND; 642 } 643 posUnix = posUnix == NOT_FOUND ? posWin : posUnix; 644 posWin = posWin == NOT_FOUND ? posUnix : posWin; 645 return Math.min(posUnix, posWin) + 1; 646 } else { 647 return isSeparator(ch0) ? 1 : 0; 648 } 649 } 650 } 651 652 /** 653 * Returns the index of the last directory separator character. 654 * <p> 655 * This method will handle a file in either Unix or Windows format. 656 * The position of the last forward or backslash is returned. 657 * <p> 658 * The output will be the same irrespective of the machine that the code is running on. 659 * 660 * @param filename the filename to find the last path separator in, null returns -1 661 * @return the index of the last separator character, or -1 if there 662 * is no such character 663 */ 664 public static int indexOfLastSeparator(final String filename) { 665 if (filename == null) { 666 return NOT_FOUND; 667 } 668 final int lastUnixPos = filename.lastIndexOf(UNIX_SEPARATOR); 669 final int lastWindowsPos = filename.lastIndexOf(WINDOWS_SEPARATOR); 670 return Math.max(lastUnixPos, lastWindowsPos); 671 } 672 673 /** 674 * Returns the index of the last extension separator character, which is a dot. 675 * <p> 676 * This method also checks that there is no directory separator after the last dot. To do this 677 * it uses 678 * {@link #indexOfLastSeparator(String)} which will handle a file in either Unix or Windows 679 * format. 680 * </p> 681 * <p> 682 * The output will be the same irrespective of the machine that the code is running on. 683 * </p> 684 * 685 * @param filename 686 * the filename to find the last extension separator in, null returns -1 687 * @return the index of the last extension separator character, or -1 if there is no such 688 * character 689 */ 690 public static int indexOfExtension(final String filename) { 691 if (filename == null) { 692 return NOT_FOUND; 693 } 694 final int extensionPos = filename.lastIndexOf(EXTENSION_SEPARATOR); 695 final int lastSeparator = indexOfLastSeparator(filename); 696 return lastSeparator > extensionPos ? NOT_FOUND : extensionPos; 697 } 698 699 // ----------------------------------------------------------------------- 700 /** 701 * Gets the prefix from a full filename, such as <code>C:/</code> 702 * or <code>~/</code>. 703 * <p> 704 * This method will handle a file in either Unix or Windows format. 705 * The prefix includes the first slash in the full filename where applicable. 706 * 707 * <pre> 708 * Windows: 709 * a\b\c.txt --> "" --> relative 710 * \a\b\c.txt --> "\" --> current drive absolute 711 * C:a\b\c.txt --> "C:" --> drive relative 712 * C:\a\b\c.txt --> "C:\" --> absolute 713 * \\server\a\b\c.txt --> "\\server\" --> UNC 714 * 715 * Unix: 716 * a/b/c.txt --> "" --> relative 717 * /a/b/c.txt --> "/" --> absolute 718 * ~/a/b/c.txt --> "~/" --> current user 719 * ~ --> "~/" --> current user (slash added) 720 * ~user/a/b/c.txt --> "~user/" --> named user 721 * ~user --> "~user/" --> named user (slash added) 722 * </pre> 723 * <p> 724 * The output will be the same irrespective of the machine that the code is running on. 725 * i.e. both Unix and Windows prefixes are matched regardless. 726 * 727 * @param filename the filename to query, null returns null 728 * @return the prefix of the file, null if invalid. Null bytes inside string will be removed 729 */ 730 public static String getPrefix(final String filename) { 731 if (filename == null) { 732 return null; 733 } 734 final int len = getPrefixLength(filename); 735 if (len < 0) { 736 return null; 737 } 738 if (len > filename.length()) { 739 failIfNullBytePresent(filename + UNIX_SEPARATOR); 740 return filename + UNIX_SEPARATOR; 741 } 742 final String path = filename.substring(0, len); 743 failIfNullBytePresent(path); 744 return path; 745 } 746 747 /** 748 * Gets the path from a full filename, which excludes the prefix. 749 * <p> 750 * This method will handle a file in either Unix or Windows format. 751 * The method is entirely text based, and returns the text before and 752 * including the last forward or backslash. 753 * 754 * <pre> 755 * C:\a\b\c.txt --> a\b\ 756 * ~/a/b/c.txt --> a/b/ 757 * a.txt --> "" 758 * a/b/c --> a/b/ 759 * a/b/c/ --> a/b/c/ 760 * </pre> 761 * <p> 762 * The output will be the same irrespective of the machine that the code is running on. 763 * <p> 764 * This method drops the prefix from the result. 765 * See {@link #getFullPath(String)} for the method that retains the prefix. 766 * 767 * @param filename the filename to query, null returns null 768 * @return the path of the file, an empty string if none exists, null if invalid. 769 * Null bytes inside string will be removed 770 */ 771 public static String getPath(final String filename) { 772 return doGetPath(filename, 1); 773 } 774 775 /** 776 * Gets the path from a full filename, which excludes the prefix, and 777 * also excluding the final directory separator. 778 * <p> 779 * This method will handle a file in either Unix or Windows format. 780 * The method is entirely text based, and returns the text before the 781 * last forward or backslash. 782 * 783 * <pre> 784 * C:\a\b\c.txt --> a\b 785 * ~/a/b/c.txt --> a/b 786 * a.txt --> "" 787 * a/b/c --> a/b 788 * a/b/c/ --> a/b/c 789 * </pre> 790 * <p> 791 * The output will be the same irrespective of the machine that the code is running on. 792 * <p> 793 * This method drops the prefix from the result. 794 * See {@link #getFullPathNoEndSeparator(String)} for the method that retains the prefix. 795 * 796 * @param filename the filename to query, null returns null 797 * @return the path of the file, an empty string if none exists, null if invalid. 798 * Null bytes inside string will be removed 799 */ 800 public static String getPathNoEndSeparator(final String filename) { 801 return doGetPath(filename, 0); 802 } 803 804 /** 805 * Does the work of getting the path. 806 * 807 * @param filename the filename 808 * @param separatorAdd 0 to omit the end separator, 1 to return it 809 * @return the path. Null bytes inside string will be removed 810 */ 811 private static String doGetPath(final String filename, final int separatorAdd) { 812 if (filename == null) { 813 return null; 814 } 815 final int prefix = getPrefixLength(filename); 816 if (prefix < 0) { 817 return null; 818 } 819 final int index = indexOfLastSeparator(filename); 820 final int endIndex = index + separatorAdd; 821 if (prefix >= filename.length() || index < 0 || prefix >= endIndex) { 822 return ""; 823 } 824 final String path = filename.substring(prefix, endIndex); 825 failIfNullBytePresent(path); 826 return path; 827 } 828 829 /** 830 * Gets the full path from a full filename, which is the prefix + path. 831 * <p> 832 * This method will handle a file in either Unix or Windows format. 833 * The method is entirely text based, and returns the text before and 834 * including the last forward or backslash. 835 * 836 * <pre> 837 * C:\a\b\c.txt --> C:\a\b\ 838 * ~/a/b/c.txt --> ~/a/b/ 839 * a.txt --> "" 840 * a/b/c --> a/b/ 841 * a/b/c/ --> a/b/c/ 842 * C: --> C: 843 * C:\ --> C:\ 844 * ~ --> ~/ 845 * ~/ --> ~/ 846 * ~user --> ~user/ 847 * ~user/ --> ~user/ 848 * </pre> 849 * <p> 850 * The output will be the same irrespective of the machine that the code is running on. 851 * 852 * @param filename the filename to query, null returns null 853 * @return the path of the file, an empty string if none exists, null if invalid 854 */ 855 public static String getFullPath(final String filename) { 856 return doGetFullPath(filename, true); 857 } 858 859 /** 860 * Gets the full path from a full filename, which is the prefix + path, 861 * and also excluding the final directory separator. 862 * <p> 863 * This method will handle a file in either Unix or Windows format. 864 * The method is entirely text based, and returns the text before the 865 * last forward or backslash. 866 * 867 * <pre> 868 * C:\a\b\c.txt --> C:\a\b 869 * ~/a/b/c.txt --> ~/a/b 870 * a.txt --> "" 871 * a/b/c --> a/b 872 * a/b/c/ --> a/b/c 873 * C: --> C: 874 * C:\ --> C:\ 875 * ~ --> ~ 876 * ~/ --> ~ 877 * ~user --> ~user 878 * ~user/ --> ~user 879 * </pre> 880 * <p> 881 * The output will be the same irrespective of the machine that the code is running on. 882 * 883 * @param filename the filename to query, null returns null 884 * @return the path of the file, an empty string if none exists, null if invalid 885 */ 886 public static String getFullPathNoEndSeparator(final String filename) { 887 return doGetFullPath(filename, false); 888 } 889 890 /** 891 * Does the work of getting the path. 892 * 893 * @param filename the filename 894 * @param includeSeparator true to include the end separator 895 * @return the path 896 */ 897 private static String doGetFullPath(final String filename, final boolean includeSeparator) { 898 if (filename == null) { 899 return null; 900 } 901 final int prefix = getPrefixLength(filename); 902 if (prefix < 0) { 903 return null; 904 } 905 if (prefix >= filename.length()) { 906 if (includeSeparator) { 907 return getPrefix(filename); // add end slash if necessary 908 } else { 909 return filename; 910 } 911 } 912 final int index = indexOfLastSeparator(filename); 913 if (index < 0) { 914 return filename.substring(0, prefix); 915 } 916 int end = index + (includeSeparator ? 1 : 0); 917 if (end == 0) { 918 end++; 919 } 920 return filename.substring(0, end); 921 } 922 923 /** 924 * Gets the name minus the path from a full filename. 925 * <p> 926 * This method will handle a file in either Unix or Windows format. 927 * The text after the last forward or backslash is returned. 928 * 929 * <pre> 930 * a/b/c.txt --> c.txt 931 * a.txt --> a.txt 932 * a/b/c --> c 933 * a/b/c/ --> "" 934 * </pre> 935 * <p> 936 * The output will be the same irrespective of the machine that the code is running on. 937 * 938 * @param filename the filename to query, null returns null 939 * @return the name of the file without the path, or an empty string if none exists. 940 * Null bytes inside string will be removed 941 */ 942 public static String getName(final String filename) { 943 if (filename == null) { 944 return null; 945 } 946 failIfNullBytePresent(filename); 947 final int index = indexOfLastSeparator(filename); 948 return filename.substring(index + 1); 949 } 950 951 /** 952 * Check the input for null bytes, a sign of unsanitized data being passed to file level 953 * functions. 954 * <p> 955 * This may be used for poison byte attacks. 956 * 957 * @param path the path to check 958 */ 959 private static void failIfNullBytePresent(final String path) { 960 final int len = path.length(); 961 for (int i = 0; i < len; i++) { 962 if (path.charAt(i) == 0) { 963 throw new IllegalArgumentException("Null byte present in file/path name. There are no " + "known legitimate use cases for such data, but several injection attacks may use it"); 964 } 965 } 966 } 967 968 /** 969 * Gets the base name, minus the full path and extension, from a full filename. 970 * <p> 971 * This method will handle a file in either Unix or Windows format. 972 * The text after the last forward or backslash and before the last dot is returned. 973 * 974 * <pre> 975 * a/b/c.txt --> c 976 * a.txt --> a 977 * a/b/c --> c 978 * a/b/c/ --> "" 979 * </pre> 980 * <p> 981 * The output will be the same irrespective of the machine that the code is running on. 982 * 983 * @param filename the filename to query, null returns null 984 * @return the name of the file without the path, or an empty string if none exists. Null bytes 985 * inside string 986 * will be removed 987 */ 988 public static String getBaseName(final String filename) { 989 return removeExtension(getName(filename)); 990 } 991 992 /** 993 * Gets the extension of a filename. 994 * <p> 995 * This method returns the textual part of the filename after the last dot. 996 * There must be no directory separator after the dot. 997 * 998 * <pre> 999 * foo.txt --> "txt" 1000 * a/b/c.jpg --> "jpg" 1001 * a/b.txt/c --> "" 1002 * a/b/c --> "" 1003 * </pre> 1004 * <p> 1005 * The output will be the same irrespective of the machine that the code is running on. 1006 * 1007 * @param filename the filename to retrieve the extension of. 1008 * @return the extension of the file or an empty string if none exists or {@code null} 1009 * if the filename is {@code null}. 1010 */ 1011 public static String getExtension(final String filename) { 1012 if (filename == null) { 1013 return null; 1014 } 1015 final int index = indexOfExtension(filename); 1016 if (index == NOT_FOUND) { 1017 return ""; 1018 } else { 1019 return filename.substring(index + 1); 1020 } 1021 } 1022 1023 // ----------------------------------------------------------------------- 1024 /** 1025 * Removes the extension from a filename. 1026 * <p> 1027 * This method returns the textual part of the filename before the last dot. 1028 * There must be no directory separator after the dot. 1029 * 1030 * <pre> 1031 * foo.txt --> foo 1032 * a\b\c.jpg --> a\b\c 1033 * a\b\c --> a\b\c 1034 * a.b\c --> a.b\c 1035 * </pre> 1036 * <p> 1037 * The output will be the same irrespective of the machine that the code is running on. 1038 * 1039 * @param filename the filename to query, null returns null 1040 * @return the filename minus the extension 1041 */ 1042 public static String removeExtension(final String filename) { 1043 if (filename == null) { 1044 return null; 1045 } 1046 failIfNullBytePresent(filename); 1047 final int index = indexOfExtension(filename); 1048 if (index == NOT_FOUND) { 1049 return filename; 1050 } else { 1051 return filename.substring(0, index); 1052 } 1053 } 1054 1055 // ----------------------------------------------------------------------- 1056 /** 1057 * Checks whether two filenames are equal exactly. 1058 * <p> 1059 * No processing is performed on the filenames other than comparison, 1060 * thus this is merely a null-safe case-sensitive equals. 1061 * 1062 * @param filename1 the first filename to query, may be null 1063 * @param filename2 the second filename to query, may be null 1064 * @return true if the filenames are equal, null equals null 1065 */ 1066 public static boolean equals(final String filename1, final String filename2) { 1067 return equals(filename1, filename2, false, IOCase.SENSITIVE); 1068 } 1069 1070 /** 1071 * Checks whether two filenames are equal using the case rules of the system. 1072 * <p> 1073 * No processing is performed on the filenames other than comparison. 1074 * The check is case-sensitive on Unix and case-insensitive on Windows. 1075 * 1076 * @param filename1 the first filename to query, may be null 1077 * @param filename2 the second filename to query, may be null 1078 * @return true if the filenames are equal, null equals null 1079 */ 1080 public static boolean equalsOnSystem(final String filename1, final String filename2) { 1081 return equals(filename1, filename2, false, IOCase.SYSTEM); 1082 } 1083 1084 // ----------------------------------------------------------------------- 1085 /** 1086 * Checks whether two filenames are equal after both have been normalized. 1087 * <p> 1088 * Both filenames are first passed to {@link #normalize(String)}. 1089 * The check is then performed in a case-sensitive manner. 1090 * 1091 * @param filename1 the first filename to query, may be null 1092 * @param filename2 the second filename to query, may be null 1093 * @return true if the filenames are equal, null equals null 1094 */ 1095 public static boolean equalsNormalized(final String filename1, final String filename2) { 1096 return equals(filename1, filename2, true, IOCase.SENSITIVE); 1097 } 1098 1099 /** 1100 * Checks whether two filenames are equal after both have been normalized 1101 * and using the case rules of the system. 1102 * <p> 1103 * Both filenames are first passed to {@link #normalize(String)}. 1104 * The check is then performed case-sensitive on Unix and 1105 * case-insensitive on Windows. 1106 * 1107 * @param filename1 the first filename to query, may be null 1108 * @param filename2 the second filename to query, may be null 1109 * @return true if the filenames are equal, null equals null 1110 */ 1111 public static boolean equalsNormalizedOnSystem(final String filename1, final String filename2) { 1112 return equals(filename1, filename2, true, IOCase.SYSTEM); 1113 } 1114 1115 /** 1116 * Checks whether two filenames are equal, optionally normalizing and providing 1117 * control over the case-sensitivity. 1118 * 1119 * @param filename1 the first filename to query, may be null 1120 * @param filename2 the second filename to query, may be null 1121 * @param normalized whether to normalize the filenames 1122 * @param caseSensitivity what case sensitivity rule to use, null means case-sensitive 1123 * @return true if the filenames are equal, null equals null 1124 */ 1125 public static boolean equals(String filename1, String filename2, final boolean normalized, IOCase caseSensitivity) { 1126 if (filename1 == null || filename2 == null) { 1127 return filename1 == null && filename2 == null; 1128 } 1129 if (normalized) { 1130 filename1 = normalize(filename1); 1131 filename2 = normalize(filename2); 1132 if (filename1 == null || filename2 == null) { 1133 throw new NullPointerException("Error normalizing one or both of the file names"); 1134 } 1135 } 1136 if (caseSensitivity == null) { 1137 caseSensitivity = IOCase.SENSITIVE; 1138 } 1139 return caseSensitivity.checkEquals(filename1, filename2); 1140 } 1141 1142 // ----------------------------------------------------------------------- 1143 /** 1144 * Checks whether the extension of the filename is that specified. 1145 * <p> 1146 * This method obtains the extension as the textual part of the filename 1147 * after the last dot. There must be no directory separator after the dot. 1148 * The extension check is case-sensitive on all platforms. 1149 * 1150 * @param filename the filename to query, null returns false 1151 * @param extension the extension to check for, null or empty checks for no extension 1152 * @return true if the filename has the specified extension 1153 * @throws java.lang.IllegalArgumentException if the supplied filename contains null bytes 1154 */ 1155 public static boolean isExtension(final String filename, final String extension) { 1156 if (filename == null) { 1157 return false; 1158 } 1159 failIfNullBytePresent(filename); 1160 if (extension == null || extension.isEmpty()) { 1161 return indexOfExtension(filename) == NOT_FOUND; 1162 } 1163 final String fileExt = getExtension(filename); 1164 return fileExt.equals(extension); 1165 } 1166 1167 /** 1168 * Checks whether the extension of the filename is one of those specified. 1169 * <p> 1170 * This method obtains the extension as the textual part of the filename 1171 * after the last dot. There must be no directory separator after the dot. 1172 * The extension check is case-sensitive on all platforms. 1173 * 1174 * @param filename the filename to query, null returns false 1175 * @param extensions the extensions to check for, null checks for no extension 1176 * @return true if the filename is one of the extensions 1177 * @throws java.lang.IllegalArgumentException if the supplied filename contains null bytes 1178 */ 1179 public static boolean isExtension(final String filename, final String... extensions) { 1180 if (filename == null) { 1181 return false; 1182 } 1183 failIfNullBytePresent(filename); 1184 if (extensions == null || extensions.length == 0) { 1185 return indexOfExtension(filename) == NOT_FOUND; 1186 } 1187 final String fileExt = getExtension(filename); 1188 for (final String extension : extensions) { 1189 if (fileExt.equals(extension)) { 1190 return true; 1191 } 1192 } 1193 return false; 1194 } 1195 1196 /** 1197 * Checks whether the extension of the filename is one of those specified. 1198 * <p> 1199 * This method obtains the extension as the textual part of the filename 1200 * after the last dot. There must be no directory separator after the dot. 1201 * The extension check is case-sensitive on all platforms. 1202 * 1203 * @param filename the filename to query, null returns false 1204 * @param extensions the extensions to check for, null checks for no extension 1205 * @return true if the filename is one of the extensions 1206 * @throws java.lang.IllegalArgumentException if the supplied filename contains null bytes 1207 */ 1208 public static boolean isExtension(final String filename, final Collection<String> extensions) { 1209 if (filename == null) { 1210 return false; 1211 } 1212 failIfNullBytePresent(filename); 1213 if (extensions == null || extensions.isEmpty()) { 1214 return indexOfExtension(filename) == NOT_FOUND; 1215 } 1216 final String fileExt = getExtension(filename); 1217 for (final String extension : extensions) { 1218 if (fileExt.equals(extension)) { 1219 return true; 1220 } 1221 } 1222 return false; 1223 } 1224 1225 // ----------------------------------------------------------------------- 1226 /** 1227 * Checks a filename to see if it matches the specified wildcard matcher, 1228 * always testing case-sensitive. 1229 * <p> 1230 * The wildcard matcher uses the characters '?' and '*' to represent a 1231 * single or multiple (zero or more) wildcard characters. 1232 * This is the same as often found on Dos/Unix command lines. 1233 * The check is case-sensitive always. 1234 * 1235 * <pre> 1236 * wildcardMatch("c.txt", "*.txt") --> true 1237 * wildcardMatch("c.txt", "*.jpg") --> false 1238 * wildcardMatch("a/b/c.txt", "a/b/*") --> true 1239 * wildcardMatch("c.txt", "*.???") --> true 1240 * wildcardMatch("c.txt", "*.????") --> false 1241 * </pre> 1242 * 1243 * N.B. the sequence "*?" does not work properly at present in match strings. 1244 * 1245 * @param filename the filename to match on 1246 * @param wildcardMatcher the wildcard string to match against 1247 * @return true if the filename matches the wildcard string 1248 */ 1249 public static boolean wildcardMatch(final String filename, final String wildcardMatcher) { 1250 return wildcardMatch(filename, wildcardMatcher, IOCase.SENSITIVE); 1251 } 1252 1253 /** 1254 * Checks a filename to see if it matches the specified wildcard matcher 1255 * using the case rules of the system. 1256 * <p> 1257 * The wildcard matcher uses the characters '?' and '*' to represent a 1258 * single or multiple (zero or more) wildcard characters. 1259 * This is the same as often found on Dos/Unix command lines. 1260 * The check is case-sensitive on Unix and case-insensitive on Windows. 1261 * 1262 * <pre> 1263 * wildcardMatch("c.txt", "*.txt") --> true 1264 * wildcardMatch("c.txt", "*.jpg") --> false 1265 * wildcardMatch("a/b/c.txt", "a/b/*") --> true 1266 * wildcardMatch("c.txt", "*.???") --> true 1267 * wildcardMatch("c.txt", "*.????") --> false 1268 * </pre> 1269 * 1270 * N.B. the sequence "*?" does not work properly at present in match strings. 1271 * 1272 * @param filename the filename to match on 1273 * @param wildcardMatcher the wildcard string to match against 1274 * @return true if the filename matches the wildcard string 1275 */ 1276 public static boolean wildcardMatchOnSystem(final String filename, final String wildcardMatcher) { 1277 return wildcardMatch(filename, wildcardMatcher, IOCase.SYSTEM); 1278 } 1279 1280 // owolff: original code 1281 /** 1282 * Checks a filename to see if it matches the specified wildcard matcher 1283 * allowing control over case-sensitivity. 1284 * <p> 1285 * The wildcard matcher uses the characters '?' and '*' to represent a 1286 * single or multiple (zero or more) wildcard characters. 1287 * N.B. the sequence "*?" does not work properly at present in match strings. 1288 * 1289 * @param filename the filename to match on 1290 * @param wildcardMatcher the wildcard string to match against 1291 * @param caseSensitivity what case sensitivity rule to use, null means case-sensitive 1292 * @return true if the filename matches the wildcard string 1293 */ 1294 @SuppressWarnings({"squid:S3776", "squid:S135"}) 1295 public static boolean wildcardMatch(final String filename, final String wildcardMatcher, IOCase caseSensitivity) { 1296 if (filename == null && wildcardMatcher == null) { 1297 return true; 1298 } 1299 if (filename == null || wildcardMatcher == null) { 1300 return false; 1301 } 1302 if (caseSensitivity == null) { 1303 caseSensitivity = IOCase.SENSITIVE; 1304 } 1305 final String[] wcs = splitOnTokens(wildcardMatcher); 1306 boolean anyChars = false; 1307 int textIdx = 0; 1308 int wcsIdx = 0; 1309 final Deque<int[]> backtrack = new ArrayDeque<>(); 1310 // loop around a backtrack stack, to handle complex * matching 1311 do { 1312 if (!backtrack.isEmpty()) { 1313 final int[] array = backtrack.pop(); 1314 wcsIdx = array[0]; 1315 textIdx = array[1]; 1316 anyChars = true; 1317 } 1318 // loop whilst tokens and text left to process 1319 while (wcsIdx < wcs.length) { 1320 if (wcs[wcsIdx].equals("?")) { 1321 // ? so move to next text char 1322 textIdx++; 1323 if (textIdx > filename.length()) { 1324 break; 1325 } 1326 anyChars = false; 1327 } else if (wcs[wcsIdx].equals("*")) { 1328 // set any chars status 1329 anyChars = true; 1330 if (wcsIdx == wcs.length - 1) { 1331 textIdx = filename.length(); 1332 } 1333 } else { 1334 // matching text token 1335 if (anyChars) { 1336 // any chars then try to locate text token 1337 textIdx = caseSensitivity.checkIndexOf(filename, textIdx, wcs[wcsIdx]); 1338 if (textIdx == NOT_FOUND) { 1339 // token not found 1340 break; 1341 } 1342 final int repeat = caseSensitivity.checkIndexOf(filename, textIdx + 1, wcs[wcsIdx]); 1343 if (repeat >= 0) { 1344 backtrack.push(new int[] {wcsIdx, repeat}); 1345 } 1346 } else { 1347 // matching from current position 1348 if (!caseSensitivity.checkRegionMatches(filename, textIdx, wcs[wcsIdx])) { 1349 // couldn't match token 1350 break; 1351 } 1352 } 1353 // matched text token, move text index to end of matched token 1354 textIdx += wcs[wcsIdx].length(); 1355 anyChars = false; 1356 } 1357 wcsIdx++; 1358 } 1359 // full match 1360 if (wcsIdx == wcs.length && textIdx == filename.length()) { 1361 return true; 1362 } 1363 } while (!backtrack.isEmpty()); 1364 return false; 1365 } 1366 1367 /** 1368 * Splits a string into a number of tokens. 1369 * The text is split by '?' and '*'. 1370 * Where multiple '*' occur consecutively they are collapsed into a single '*'. 1371 * 1372 * @param text the text to split 1373 * @return the array of tokens, never null 1374 */ 1375 static String[] splitOnTokens(final String text) { 1376 // used by wildcardMatch 1377 // package level so a unit test may run on this 1378 if (text.indexOf('?') == NOT_FOUND && text.indexOf('*') == NOT_FOUND) { 1379 return new String[] {text}; 1380 } 1381 final char[] array = text.toCharArray(); 1382 final ArrayList<String> list = new ArrayList<>(); 1383 final StringBuilder buffer = new StringBuilder(); 1384 char prevChar = 0; 1385 for (final char ch : array) { 1386 if (ch == '?' || ch == '*') { 1387 if (buffer.length() != 0) { 1388 list.add(buffer.toString()); 1389 buffer.setLength(0); 1390 } 1391 if (ch == '?') { 1392 list.add("?"); 1393 } else if (prevChar != '*') { 1394 // ch == '*' here; check if previous char was '*' 1395 list.add("*"); 1396 } 1397 } else { 1398 buffer.append(ch); 1399 } 1400 prevChar = ch; 1401 } 1402 if (buffer.length() != 0) { 1403 list.add(buffer.toString()); 1404 } 1405 return list.toArray(new String[list.size()]); 1406 } 1407 1408 @java.lang.SuppressWarnings("all") 1409 @lombok.Generated 1410 private FilenameUtils() { 1411 throw new java.lang.UnsupportedOperationException("This is a utility class and cannot be instantiated"); 1412 } 1413}