001/////////////////////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code and other text files for adherence to a set of rules. 003// Copyright (C) 2001-2026 the original author or authors. 004// 005// This library is free software; you can redistribute it and/or 006// modify it under the terms of the GNU Lesser General Public 007// License as published by the Free Software Foundation; either 008// version 2.1 of the License, or (at your option) any later version. 009// 010// This library is distributed in the hope that it will be useful, 011// but WITHOUT ANY WARRANTY; without even the implied warranty of 012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 013// Lesser General Public License for more details. 014// 015// You should have received a copy of the GNU Lesser General Public 016// License along with this library; if not, write to the Free Software 017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 018/////////////////////////////////////////////////////////////////////////////////////////////// 019 020package com.puppycrawl.tools.checkstyle.checks.imports; 021 022import java.util.Locale; 023import java.util.regex.Matcher; 024import java.util.regex.Pattern; 025 026import com.puppycrawl.tools.checkstyle.FileStatefulCheck; 027import com.puppycrawl.tools.checkstyle.api.AbstractCheck; 028import com.puppycrawl.tools.checkstyle.api.DetailAST; 029import com.puppycrawl.tools.checkstyle.api.FullIdent; 030import com.puppycrawl.tools.checkstyle.api.TokenTypes; 031import com.puppycrawl.tools.checkstyle.utils.CommonUtil; 032import com.puppycrawl.tools.checkstyle.utils.UnmodifiableCollectionUtil; 033 034/** 035 * <div> 036 * Checks the ordering/grouping of imports. Features are: 037 * </div> 038 * <ul> 039 * <li> 040 * groups type/static imports: ensures that groups of imports come in a specific order 041 * (e.g., java. comes first, javax. comes second, then everything else) 042 * </li> 043 * <li> 044 * adds a separation between type import groups : ensures that a blank line sit between each group 045 * </li> 046 * <li> 047 * type/static import groups aren't separated internally: ensures that each group aren't separated 048 * internally by blank line or comment 049 * </li> 050 * <li> 051 * sorts type/static imports inside each group: ensures that imports within each group are in 052 * lexicographic order 053 * </li> 054 * <li> 055 * sorts according to case: ensures that the comparison between imports is case-sensitive, in 056 * <a href="https://en.wikipedia.org/wiki/ASCII#Order">ASCII sort order</a> 057 * </li> 058 * <li> 059 * arrange static imports: ensures the relative order between type imports and static imports 060 * (see 061 * <a href="https://checkstyle.org/property-types.html#ImportOrderOption">ImportOrderOption</a>) 062 * </li> 063 * </ul> 064 * 065 * @since 3.2 066 */ 067@FileStatefulCheck 068public class ImportOrderCheck extends AbstractCheck { 069 070 /** 071 * A key is pointing to the warning message text in "messages.properties" file. 072 */ 073 public static final String MSG_SEPARATION = "import.separation"; 074 075 /** 076 * A key is pointing to the warning message text in "messages.properties" file. 077 */ 078 public static final String MSG_SEPARATED_IN_GROUP = "import.groups.separated.internally"; 079 080 /** 081 * A key is pointing to the warning message text in "messages.properties" file. 082 */ 083 public static final String MSG_ORDERING_LEX = "import.ordering.lex"; 084 085 /** 086 * A key is pointing to the warning message text in "messages.properties" file. 087 */ 088 public static final String MSG_ORDERING_STATIC = "import.ordering.static"; 089 090 /** 091 * A key is pointing to the warning message text in "messages.properties" file. 092 */ 093 public static final String MSG_ORDERING_GROUP = "import.ordering.group"; 094 095 /** The special wildcard that catches all remaining groups. */ 096 private static final String WILDCARD_GROUP_NAME = "*"; 097 098 /** The Forward slash. */ 099 private static final String FORWARD_SLASH = "/"; 100 101 /** Empty array of pattern type needed to initialize check. */ 102 private static final Pattern[] EMPTY_PATTERN_ARRAY = new Pattern[0]; 103 104 /** 105 * Specify list of <b>type import</b> groups. Every group identified either by a common prefix 106 * string, or by a regular expression enclosed in forward slashes (e.g. {@code /regexp/}). 107 * If an import matches two or more groups, 108 * the best match is selected (closest to the start, and the longest match). 109 * All type imports, which does not match any group, falls into an additional group, 110 * located at the end. Thus, the empty list of type groups (the default value) means one group 111 * for all type imports. 112 */ 113 private String[] groups = CommonUtil.EMPTY_STRING_ARRAY; 114 115 /** 116 * Specify list of <b>static</b> import groups. Every group identified either by a common prefix 117 * string, or by a regular expression enclosed in forward slashes (e.g. {@code /regexp/}). 118 * If an import matches two or more groups, 119 * the best match is selected (closest to the start, and the longest match). 120 * All static imports, which does not match any group, fall into an additional group, located 121 * at the end. Thus, the empty list of static groups (the default value) means one group for all 122 * static imports. This property has effect only when the property {@code option} is set to 123 * {@code top} or {@code bottom}. 124 */ 125 private String[] staticGroups = CommonUtil.EMPTY_STRING_ARRAY; 126 127 /** 128 * Control whether type import groups should be separated by, at least, one blank 129 * line or comment and aren't separated internally. It doesn't affect separations for static 130 * imports. 131 */ 132 private boolean separated; 133 134 /** 135 * Control whether static import groups should be separated by, at least, one blank 136 * line or comment and aren't separated internally. When the property {@code option} 137 * is set to {@code top} or {@code bottom}, this property requires {@code staticGroups} 138 * to be enabled. When the property {@code option} is set to {@code under} or 139 * {@code above}, this property allows blank lines between static and non-static 140 * imports within the same group. 141 */ 142 private boolean separatedStaticGroups; 143 144 /** 145 * Control whether type imports within each group should be sorted. 146 * It doesn't affect sorting for static imports. 147 */ 148 private boolean ordered = true; 149 150 /** 151 * Control whether string comparison should be case-sensitive or not. Case-sensitive 152 * sorting is in <a href="https://en.wikipedia.org/wiki/ASCII#Order">ASCII sort order</a>. 153 * It affects both type imports and static imports. 154 */ 155 private boolean caseSensitive = true; 156 157 /** Last imported group. */ 158 private int lastGroup; 159 /** Line number of last import. */ 160 private int lastImportLine; 161 /** Name of last import. */ 162 private String lastImport; 163 /** If last import was static. */ 164 private boolean lastImportStatic; 165 /** Whether there were any imports. */ 166 private boolean beforeFirstImport; 167 /** 168 * Whether static and type import groups should be split apart. 169 * When the {@code option} property is set to {@code INFLOW}, {@code ABOVE} or {@code UNDER}, 170 * both the type and static imports use the properties {@code groups} and {@code separated}. 171 * When the {@code option} property is set to {@code TOP} or {@code BOTTOM}, static imports 172 * uses the properties {@code staticGroups} and {@code separatedStaticGroups}. 173 */ 174 private boolean staticImportsApart; 175 176 /** 177 * Control whether <b>static imports</b> located at <b>top</b> or <b>bottom</b> are 178 * sorted within the group. 179 */ 180 private boolean sortStaticImportsAlphabetically; 181 182 /** 183 * Control whether to use container ordering (Eclipse IDE term) for static imports or not. 184 */ 185 private boolean useContainerOrderingForStatic; 186 187 /** 188 * Specify policy on the relative order between type imports and static imports. 189 */ 190 private ImportOrderOption option = ImportOrderOption.UNDER; 191 192 /** 193 * Complied array of patterns for property {@code groups}. 194 */ 195 private Pattern[] groupsReg = EMPTY_PATTERN_ARRAY; 196 197 /** 198 * Complied array of patterns for property {@code staticGroups}. 199 */ 200 private Pattern[] staticGroupsReg = EMPTY_PATTERN_ARRAY; 201 202 /** 203 * Creates a new {@code ImportOrderCheck} instance. 204 */ 205 public ImportOrderCheck() { 206 // no code by default 207 } 208 209 /** 210 * Setter to specify policy on the relative order between type imports and static imports. 211 * 212 * @param optionStr string to decode option from 213 * @throws IllegalArgumentException if unable to decode 214 * @since 5.0 215 */ 216 public void setOption(String optionStr) { 217 option = ImportOrderOption.valueOf(optionStr.trim().toUpperCase(Locale.ENGLISH)); 218 } 219 220 /** 221 * Setter to specify list of <b>type import</b> groups. Every group identified either by a 222 * common prefix string, or by a regular expression enclosed in forward slashes 223 * (e.g. {@code /regexp/}). If an import matches two or more groups, 224 * the best match is selected (closest to the start, and the longest match). 225 * All type imports, which does not match any group, falls into an 226 * additional group, located at the end. Thus, the empty list of type groups (the default value) 227 * means one group for all type imports. 228 * 229 * @param packageGroups a comma-separated list of package names/prefixes. 230 * @since 3.2 231 */ 232 public void setGroups(String... packageGroups) { 233 groups = UnmodifiableCollectionUtil.copyOfArray(packageGroups, packageGroups.length); 234 groupsReg = compilePatterns(packageGroups); 235 } 236 237 /** 238 * Setter to specify list of <b>static</b> import groups. Every group identified either by a 239 * common prefix string, or by a regular expression enclosed in forward slashes 240 * (e.g. {@code /regexp/}). If an import matches two or more groups, 241 * the best match is selected (closest to the start, and the longest match). 242 * All static imports, which does not match any group, fall into an 243 * additional group, located at the end. Thus, the empty list of static groups (the default 244 * value) means one group for all static imports. This property has effect only when 245 * the property {@code option} is set to {@code top} or {@code bottom}. 246 * 247 * @param packageGroups a comma-separated list of package names/prefixes. 248 * @since 8.12 249 */ 250 public void setStaticGroups(String... packageGroups) { 251 staticGroups = UnmodifiableCollectionUtil.copyOfArray(packageGroups, packageGroups.length); 252 staticGroupsReg = compilePatterns(packageGroups); 253 } 254 255 /** 256 * Setter to control whether type imports within each group should be sorted. 257 * It doesn't affect sorting for static imports. 258 * 259 * @param ordered 260 * whether lexicographic ordering of imports within a group required or not. 261 * @since 3.2 262 */ 263 public void setOrdered(boolean ordered) { 264 this.ordered = ordered; 265 } 266 267 /** 268 * Setter to control whether type import groups should be separated by, at least, 269 * one blank line or comment and aren't separated internally. 270 * It doesn't affect separations for static imports. 271 * 272 * @param separated 273 * whether groups should be separated by one blank line or comment. 274 * @since 3.2 275 */ 276 public void setSeparated(boolean separated) { 277 this.separated = separated; 278 } 279 280 /** 281 * Setter to control whether static import groups should be separated by, at least, 282 * one blank line or comment and aren't separated internally. 283 * When the property {@code option} is set to {@code top} or {@code bottom}, 284 * this property requires {@code staticGroups} to be enabled. 285 * When the property {@code option} is set to {@code under} or {@code above}, 286 * this property allows blank lines between static and non-static imports within the same group. 287 * 288 * @param separatedStaticGroups 289 * whether groups should be separated by one blank line or comment. 290 * @since 8.12 291 */ 292 public void setSeparatedStaticGroups(boolean separatedStaticGroups) { 293 this.separatedStaticGroups = separatedStaticGroups; 294 } 295 296 /** 297 * Setter to control whether string comparison should be case-sensitive or not. 298 * Case-sensitive sorting is in 299 * <a href="https://en.wikipedia.org/wiki/ASCII#Order">ASCII sort order</a>. 300 * It affects both type imports and static imports. 301 * 302 * @param caseSensitive 303 * whether string comparison should be case-sensitive. 304 * @since 3.3 305 */ 306 public void setCaseSensitive(boolean caseSensitive) { 307 this.caseSensitive = caseSensitive; 308 } 309 310 /** 311 * Setter to control whether <b>static imports</b> located at <b>top</b> or 312 * <b>bottom</b> are sorted within the group. 313 * 314 * @param sortAlphabetically true or false. 315 * @since 6.5 316 */ 317 public void setSortStaticImportsAlphabetically(boolean sortAlphabetically) { 318 sortStaticImportsAlphabetically = sortAlphabetically; 319 } 320 321 /** 322 * Setter to control whether to use container ordering (Eclipse IDE term) for static 323 * imports or not. 324 * 325 * @param useContainerOrdering whether to use container ordering for static imports or not. 326 * @since 7.1 327 */ 328 public void setUseContainerOrderingForStatic(boolean useContainerOrdering) { 329 useContainerOrderingForStatic = useContainerOrdering; 330 } 331 332 @Override 333 public int[] getDefaultTokens() { 334 return getRequiredTokens(); 335 } 336 337 @Override 338 public int[] getAcceptableTokens() { 339 return getRequiredTokens(); 340 } 341 342 @Override 343 public int[] getRequiredTokens() { 344 return new int[] {TokenTypes.IMPORT, TokenTypes.STATIC_IMPORT}; 345 } 346 347 @Override 348 public void beginTree(DetailAST rootAST) { 349 lastGroup = Integer.MIN_VALUE; 350 lastImportLine = Integer.MIN_VALUE; 351 lastImportStatic = false; 352 beforeFirstImport = true; 353 staticImportsApart = 354 option == ImportOrderOption.TOP || option == ImportOrderOption.BOTTOM; 355 } 356 357 // -@cs[CyclomaticComplexity] SWITCH was transformed into IF-ELSE. 358 @Override 359 public void visitToken(DetailAST ast) { 360 final FullIdent ident; 361 final boolean isStatic; 362 363 if (ast.getType() == TokenTypes.IMPORT) { 364 ident = FullIdent.createFullIdentBelow(ast); 365 isStatic = false; 366 } 367 else { 368 ident = FullIdent.createFullIdent(ast.getFirstChild() 369 .getNextSibling()); 370 isStatic = true; 371 } 372 373 // using set of IF instead of SWITCH to analyze Enum options to satisfy coverage. 374 // https://github.com/checkstyle/checkstyle/issues/1387 375 if (option == ImportOrderOption.TOP || option == ImportOrderOption.ABOVE) { 376 final boolean isStaticAndNotLastImport = isStatic && !lastImportStatic; 377 doVisitToken(ident, isStatic, isStaticAndNotLastImport, ast); 378 } 379 else if (option == ImportOrderOption.BOTTOM || option == ImportOrderOption.UNDER) { 380 final boolean isLastImportAndNonStatic = lastImportStatic && !isStatic; 381 doVisitToken(ident, isStatic, isLastImportAndNonStatic, ast); 382 } 383 else if (option == ImportOrderOption.INFLOW) { 384 // "previous" argument is useless here 385 doVisitToken(ident, isStatic, true, ast); 386 } 387 else { 388 throw new IllegalStateException( 389 String.format(Locale.ROOT, "Unexpected option for static imports: %s", option)); 390 } 391 392 lastImportLine = ast.findFirstToken(TokenTypes.SEMI).getLineNo(); 393 lastImportStatic = isStatic; 394 beforeFirstImport = false; 395 } 396 397 /** 398 * Shares processing... 399 * 400 * @param ident the import to process. 401 * @param isStatic whether the token is static or not. 402 * @param previous previous non-static but current is static (above), or 403 * previous static but current is non-static (under). 404 * @param ast node of the AST. 405 */ 406 private void doVisitToken(FullIdent ident, boolean isStatic, boolean previous, DetailAST ast) { 407 final String name = ident.getText(); 408 final int groupIdx = getGroupNumber(isStatic && staticImportsApart, name); 409 410 if (groupIdx > lastGroup) { 411 handleGreaterGroup(isStatic, ast, name); 412 } 413 else if (groupIdx == lastGroup) { 414 doVisitTokenInSameGroup(isStatic, previous, name, ast); 415 416 if (separatedStaticGroups 417 && isStatic != lastImportStatic 418 && !isSeparatorBeforeImport(ast.getLineNo())) { 419 log(ast, MSG_SEPARATION, name); 420 } 421 } 422 else { 423 handleLowerGroup(previous, ast, name); 424 } 425 if (isSeparatorInGroup(groupIdx, isStatic, ast.getLineNo())) { 426 log(ast, MSG_SEPARATED_IN_GROUP, name); 427 } 428 429 lastGroup = groupIdx; 430 lastImport = name; 431 } 432 433 /** 434 * Handles the case when the current import belongs to a group 435 * that comes after the previous group. Verifies whether a separator between groups is required. 436 * 437 * @param isStatic whether the current import is static 438 * @param ast the AST node of the current import 439 * @param name the fully qualified name of the current import 440 */ 441 private void handleGreaterGroup(boolean isStatic, DetailAST ast, String name) { 442 if (!beforeFirstImport 443 && ast.getLineNo() - lastImportLine < 2 444 && needSeparator(isStatic)) { 445 log(ast, MSG_SEPARATION, name); 446 } 447 } 448 449 /** 450 * Handles the case when the current import belongs to a group 451 * that should appear before the previous group. Logs either 452 * a static-order violation or a group-order violation. 453 * 454 * @param previous indicates a static/non-static ordering transition 455 * @param ast the AST node of the current import 456 * @param name the fully qualified name of the current import 457 */ 458 private void handleLowerGroup(boolean previous, DetailAST ast, String name) { 459 if (previous 460 && (option == ImportOrderOption.TOP || option == ImportOrderOption.BOTTOM)) { 461 log(ast, MSG_ORDERING_STATIC, name); 462 } 463 else { 464 log(ast, MSG_ORDERING_GROUP, name); 465 } 466 } 467 468 /** 469 * Checks whether import groups should be separated. 470 * 471 * @param isStatic whether the token is static or not. 472 * @return true if imports groups should be separated. 473 */ 474 private boolean needSeparator(boolean isStatic) { 475 final boolean typeImportSeparator = !isStatic && separated; 476 final boolean staticImportSeparator; 477 if (staticImportsApart) { 478 staticImportSeparator = isStatic && separatedStaticGroups; 479 } 480 else { 481 staticImportSeparator = separated; 482 } 483 final boolean separatorBetween = isStatic != lastImportStatic 484 && (separated || separatedStaticGroups); 485 486 return typeImportSeparator || staticImportSeparator || separatorBetween; 487 } 488 489 /** 490 * Checks whether imports group separated internally. 491 * 492 * @param groupIdx group number. 493 * @param isStatic whether the token is static or not. 494 * @param line the line of the current import. 495 * @return true if imports group are separated internally. 496 */ 497 private boolean isSeparatorInGroup(int groupIdx, boolean isStatic, int line) { 498 final boolean inSameGroup = groupIdx == lastGroup; 499 final boolean result; 500 if (inSameGroup) { 501 result = (isStatic == lastImportStatic || !separatedStaticGroups) 502 && isSeparatorBeforeImport(line); 503 } 504 else { 505 result = !needSeparator(isStatic) && isSeparatorBeforeImport(line); 506 } 507 return result; 508 } 509 510 /** 511 * Checks whether there is any separator before current import. 512 * 513 * @param line the line of the current import. 514 * @return true if there is separator before current import which isn't the first import. 515 */ 516 private boolean isSeparatorBeforeImport(int line) { 517 return line - lastImportLine > 1; 518 } 519 520 /** 521 * Shares processing... 522 * 523 * @param isStatic whether the token is static or not. 524 * @param previous previous non-static but current is static (above), or 525 * previous static but current is non-static (under). 526 * @param name the name of the current import. 527 * @param ast node of the AST. 528 */ 529 private void doVisitTokenInSameGroup(boolean isStatic, 530 boolean previous, String name, DetailAST ast) { 531 if (ordered) { 532 if (option == ImportOrderOption.INFLOW) { 533 if (isWrongOrder(name, isStatic)) { 534 log(ast, MSG_ORDERING_LEX, name, lastImport); 535 } 536 } 537 else { 538 if (previous) { 539 // static vs non-static placement violation 540 log(ast, MSG_ORDERING_STATIC, name); 541 } 542 else if (lastImportStatic == isStatic && isWrongOrder(name, isStatic)) { 543 // lexicographical violation 544 log(ast, MSG_ORDERING_LEX, name, lastImport); 545 } 546 } 547 } 548 } 549 550 /** 551 * Checks whether import name is in wrong order. 552 * 553 * @param name import name. 554 * @param isStatic whether it is a static import name. 555 * @return true if import name is in wrong order. 556 */ 557 private boolean isWrongOrder(String name, boolean isStatic) { 558 final boolean result; 559 if (isStatic) { 560 if (useContainerOrderingForStatic) { 561 result = compareContainerOrder(lastImport, name, caseSensitive) > 0; 562 } 563 else if (staticImportsApart) { 564 result = sortStaticImportsAlphabetically 565 && compare(lastImport, name, caseSensitive) > 0; 566 } 567 else { 568 result = compare(lastImport, name, caseSensitive) > 0; 569 } 570 } 571 else { 572 // out of lexicographic order 573 result = compare(lastImport, name, caseSensitive) > 0; 574 } 575 return result; 576 } 577 578 /** 579 * Compares two import strings. 580 * We first compare the container of the static import, container being the type enclosing 581 * the static element being imported. When this returns 0, we compare the qualified 582 * import name. For e.g. this is what is considered to be container names: 583 * {@snippet lang="text" : 584 * import static HttpConstants.COLON => HttpConstants 585 * import static HttpHeaders.addHeader => HttpHeaders 586 * import static HttpHeaders.setHeader => HttpHeaders 587 * import static HttpHeaders.Names.DATE => HttpHeaders.Names 588 * } 589 * 590 * <p> 591 * According to this logic, HttpHeaders.Names would come after HttpHeaders. 592 * For more details, see <a href="https://bugs.eclipse.org/bugs/show_bug.cgi?id=473629#c3"> 593 * static imports comparison method</a> in Eclipse. 594 * </p> 595 * 596 * @param importName1 first import name 597 * @param importName2 second import name 598 * @param caseSensitive whether the comparison of fully qualified import names is case-sensitive 599 * @return the value {@code 0} if str1 is equal to str2; a value 600 * less than {@code 0} if str is less than the str2 (container order 601 * or lexicographical); and a value greater than {@code 0} if str1 is greater than str2 602 * (container order or lexicographically) 603 */ 604 private static int compareContainerOrder(String importName1, String importName2, 605 boolean caseSensitive) { 606 final String container1 = getImportContainer(importName1); 607 final String container2 = getImportContainer(importName2); 608 final int compareContainersOrderResult; 609 if (caseSensitive) { 610 compareContainersOrderResult = container1.compareTo(container2); 611 } 612 else { 613 compareContainersOrderResult = container1.compareToIgnoreCase(container2); 614 } 615 final int result; 616 if (compareContainersOrderResult == 0) { 617 result = compare(importName1, importName2, caseSensitive); 618 } 619 else { 620 result = compareContainersOrderResult; 621 } 622 return result; 623 } 624 625 /** 626 * Extracts import container name from fully qualified import name. 627 * An import container name is the type which encloses the static element being imported. 628 * For example, HttpConstants, HttpHeaders, HttpHeaders.Names are import container names: 629 * {@snippet lang="text" : 630 * import static HttpConstants.COLON => HttpConstants 631 * import static HttpHeaders.addHeader => HttpHeaders 632 * import static HttpHeaders.setHeader => HttpHeaders 633 * import static HttpHeaders.Names.DATE => HttpHeaders.Names 634 * } 635 * 636 * @param qualifiedImportName fully qualified import name. 637 * @return import container name. 638 */ 639 private static String getImportContainer(String qualifiedImportName) { 640 final int lastDotIndex = qualifiedImportName.lastIndexOf('.'); 641 return qualifiedImportName.substring(0, lastDotIndex); 642 } 643 644 /** 645 * Finds out what group the specified import belongs to. 646 * 647 * @param isStatic whether the token is static or not. 648 * @param name the import name to find. 649 * @return group number for given import name. 650 */ 651 private int getGroupNumber(boolean isStatic, String name) { 652 final Pattern[] patterns; 653 if (isStatic) { 654 patterns = staticGroupsReg; 655 } 656 else { 657 patterns = groupsReg; 658 } 659 660 int number = getGroupNumber(patterns, name); 661 662 if (isStatic && option == ImportOrderOption.BOTTOM) { 663 number += groups.length + 1; 664 } 665 else if (!isStatic && option == ImportOrderOption.TOP) { 666 number += staticGroups.length + 1; 667 } 668 return number; 669 } 670 671 /** 672 * Finds out what group the specified import belongs to. 673 * 674 * @param patterns groups to check. 675 * @param name the import name to find. 676 * @return group number for given import name. 677 */ 678 private static int getGroupNumber(Pattern[] patterns, String name) { 679 int bestIndex = patterns.length; 680 int bestEnd = -1; 681 int bestPos = Integer.MAX_VALUE; 682 683 // find out what group this belongs in 684 // loop over patterns and get index 685 for (int index = 0; index < patterns.length; index++) { 686 final Matcher matcher = patterns[index].matcher(name); 687 if (matcher.find()) { 688 if (matcher.start() < bestPos) { 689 bestIndex = index; 690 bestEnd = matcher.end(); 691 bestPos = matcher.start(); 692 } 693 else if (matcher.start() == bestPos && matcher.end() > bestEnd) { 694 bestIndex = index; 695 bestEnd = matcher.end(); 696 } 697 } 698 } 699 return bestIndex; 700 } 701 702 /** 703 * Compares two strings. 704 * 705 * @param string1 706 * the first string 707 * @param string2 708 * the second string 709 * @param caseSensitive 710 * whether the comparison is case-sensitive 711 * @return the value {@code 0} if string1 is equal to string2; a value 712 * less than {@code 0} if string1 is lexicographically less 713 * than the string2; and a value greater than {@code 0} if 714 * string1 is lexicographically greater than string2 715 */ 716 private static int compare(String string1, String string2, 717 boolean caseSensitive) { 718 final int result; 719 if (caseSensitive) { 720 result = string1.compareTo(string2); 721 } 722 else { 723 result = string1.compareToIgnoreCase(string2); 724 } 725 726 return result; 727 } 728 729 /** 730 * Compiles the list of package groups and the order they should occur in the file. 731 * 732 * @param packageGroups a comma-separated list of package names/prefixes. 733 * @return array of compiled patterns. 734 * @throws IllegalArgumentException if any of the package groups are not valid. 735 */ 736 private static Pattern[] compilePatterns(String... packageGroups) { 737 final Pattern[] patterns = new Pattern[packageGroups.length]; 738 for (int index = 0; index < packageGroups.length; index++) { 739 String pkg = packageGroups[index]; 740 final Pattern grp; 741 742 // if the pkg name is the wildcard, make it match zero chars 743 // from any name, so it will always be used as last resort. 744 if (WILDCARD_GROUP_NAME.equals(pkg)) { 745 // matches any package 746 grp = Pattern.compile(""); 747 } 748 else if (pkg.startsWith(FORWARD_SLASH)) { 749 if (!pkg.endsWith(FORWARD_SLASH)) { 750 throw new IllegalArgumentException("Invalid group: " + pkg); 751 } 752 pkg = pkg.substring(1, pkg.length() - 1); 753 grp = Pattern.compile(pkg); 754 } 755 else { 756 final StringBuilder pkgBuilder = new StringBuilder(pkg); 757 if (!pkg.endsWith(".")) { 758 pkgBuilder.append('.'); 759 } 760 grp = Pattern.compile("^" + Pattern.quote(pkgBuilder.toString())); 761 } 762 763 patterns[index] = grp; 764 } 765 return patterns; 766 } 767 768}