001/////////////////////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code and other text files for adherence to a set of rules. 003// Copyright (C) 2001-2025 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.ArrayList; 023import java.util.Arrays; 024import java.util.List; 025import java.util.StringTokenizer; 026import java.util.regex.Matcher; 027import java.util.regex.Pattern; 028 029import com.puppycrawl.tools.checkstyle.FileStatefulCheck; 030import com.puppycrawl.tools.checkstyle.api.AbstractCheck; 031import com.puppycrawl.tools.checkstyle.api.DetailAST; 032import com.puppycrawl.tools.checkstyle.api.FullIdent; 033import com.puppycrawl.tools.checkstyle.api.TokenTypes; 034import com.puppycrawl.tools.checkstyle.utils.CommonUtil; 035 036/** 037 * <div> 038 * Checks that the groups of import declarations appear in the order specified 039 * by the user. If there is an import but its group is not specified in the 040 * configuration such an import should be placed at the end of the import list. 041 * </div> 042 * 043 * <p> 044 * The rule consists of: 045 * </p> 046 * <ol> 047 * <li> 048 * STATIC group. This group sets the ordering of static imports. 049 * </li> 050 * <li> 051 * SAME_PACKAGE(n) group. This group sets the ordering of the same package imports. 052 * Imports are considered on SAME_PACKAGE group if <b>n</b> first domains in package 053 * name and import name are identical: 054 * <div class="wrapper"><pre class="prettyprint"><code class="language-java"> 055 * package java.util.concurrent.locks; 056 * 057 * import java.io.File; 058 * import java.util.*; //#1 059 * import java.util.List; //#2 060 * import java.util.StringTokenizer; //#3 061 * import java.util.concurrent.*; //#4 062 * import java.util.concurrent.AbstractExecutorService; //#5 063 * import java.util.concurrent.locks.LockSupport; //#6 064 * import java.util.regex.Pattern; //#7 065 * import java.util.regex.Matcher; //#8 066 * </code></pre></div> 067 * If we have SAME_PACKAGE(3) on configuration file, imports #4-6 will be considered as 068 * a SAME_PACKAGE group (java.util.concurrent.*, java.util.concurrent.AbstractExecutorService, 069 * java.util.concurrent.locks.LockSupport). SAME_PACKAGE(2) will include #1-8. 070 * SAME_PACKAGE(4) will include only #6. SAME_PACKAGE(5) will result in no imports assigned 071 * to SAME_PACKAGE group because actual package java.util.concurrent.locks has only 4 domains. 072 * </li> 073 * <li> 074 * THIRD_PARTY_PACKAGE group. This group sets ordering of third party imports. 075 * Third party imports are all imports except STATIC, SAME_PACKAGE(n), STANDARD_JAVA_PACKAGE and 076 * SPECIAL_IMPORTS. 077 * </li> 078 * <li> 079 * STANDARD_JAVA_PACKAGE group. By default, this group sets ordering of standard java/javax imports. 080 * </li> 081 * <li> 082 * SPECIAL_IMPORTS group. This group may contain some imports that have particular meaning for the 083 * user. 084 * </li> 085 * </ol> 086 * 087 * <p> 088 * Notes: 089 * Rules are configured as a comma-separated ordered list. 090 * </p> 091 * 092 * <p> 093 * Note: '###' group separator is deprecated (in favor of a comma-separated list), 094 * but is currently supported for backward compatibility. 095 * </p> 096 * 097 * <p> 098 * To set RegExps for THIRD_PARTY_PACKAGE and STANDARD_JAVA_PACKAGE groups use 099 * thirdPartyPackageRegExp and standardPackageRegExp options. 100 * </p> 101 * 102 * <p> 103 * Pretty often one import can match more than one group. For example, static import from standard 104 * package or regular expressions are configured to allow one import match multiple groups. 105 * In this case, group will be assigned according to priorities: 106 * </p> 107 * <ol> 108 * <li> 109 * STATIC has top priority 110 * </li> 111 * <li> 112 * SAME_PACKAGE has second priority 113 * </li> 114 * <li> 115 * STANDARD_JAVA_PACKAGE and SPECIAL_IMPORTS will compete using "best match" rule: longer 116 * matching substring wins; in case of the same length, lower position of matching substring 117 * wins; if position is the same, order of rules in configuration solves the puzzle. 118 * </li> 119 * <li> 120 * THIRD_PARTY has the least priority 121 * </li> 122 * </ol> 123 * 124 * <p> 125 * Few examples to illustrate "best match": 126 * </p> 127 * 128 * <p> 129 * 1. patterns STANDARD_JAVA_PACKAGE = "Check", SPECIAL_IMPORTS="ImportOrderCheck" and input file: 130 * </p> 131 * <div class="wrapper"><pre class="prettyprint"><code class="language-java"> 132 * import com.puppycrawl.tools.checkstyle.checks.imports.CustomImportOrderCheck; 133 * import com.puppycrawl.tools.checkstyle.checks.imports.ImportOrderCheck; 134 * </code></pre></div> 135 * 136 * <p> 137 * Result: imports will be assigned to SPECIAL_IMPORTS, because matching substring length is 16. 138 * Matching substring for STANDARD_JAVA_PACKAGE is 5. 139 * </p> 140 * 141 * <p> 142 * 2. patterns STANDARD_JAVA_PACKAGE = "Check", SPECIAL_IMPORTS="Avoid" and file: 143 * </p> 144 * <div class="wrapper"><pre class="prettyprint"><code class="language-java"> 145 * import com.puppycrawl.tools.checkstyle.checks.imports.AvoidStarImportCheck; 146 * </code></pre></div> 147 * 148 * <p> 149 * Result: import will be assigned to SPECIAL_IMPORTS. Matching substring length is 5 for both 150 * patterns. However, "Avoid" position is lower than "Check" position. 151 * </p> 152 * <ul> 153 * <li> 154 * Property {@code customImportOrderRules} - Specify ordered list of import groups. 155 * Type is {@code java.lang.String[]}. 156 * Default value is {@code ""}. 157 * </li> 158 * <li> 159 * Property {@code separateLineBetweenGroups} - Force empty line separator between 160 * import groups. 161 * Type is {@code boolean}. 162 * Default value is {@code true}. 163 * </li> 164 * <li> 165 * Property {@code sortImportsInGroupAlphabetically} - Force grouping alphabetically, 166 * in <a href="https://en.wikipedia.org/wiki/ASCII#Order">ASCII sort order</a>. 167 * Type is {@code boolean}. 168 * Default value is {@code false}. 169 * </li> 170 * <li> 171 * Property {@code specialImportsRegExp} - Specify RegExp for SPECIAL_IMPORTS group imports. 172 * Type is {@code java.util.regex.Pattern}. 173 * Default value is {@code "^$"}. 174 * </li> 175 * <li> 176 * Property {@code standardPackageRegExp} - Specify RegExp for STANDARD_JAVA_PACKAGE group imports. 177 * Type is {@code java.util.regex.Pattern}. 178 * Default value is {@code "^(java|javax)\."}. 179 * </li> 180 * <li> 181 * Property {@code thirdPartyPackageRegExp} - Specify RegExp for THIRD_PARTY_PACKAGE group imports. 182 * Type is {@code java.util.regex.Pattern}. 183 * Default value is {@code ".*"}. 184 * </li> 185 * </ul> 186 * 187 * <p> 188 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker} 189 * </p> 190 * 191 * <p> 192 * Violation Message Keys: 193 * </p> 194 * <ul> 195 * <li> 196 * {@code custom.import.order} 197 * </li> 198 * <li> 199 * {@code custom.import.order.lex} 200 * </li> 201 * <li> 202 * {@code custom.import.order.line.separator} 203 * </li> 204 * <li> 205 * {@code custom.import.order.nonGroup.expected} 206 * </li> 207 * <li> 208 * {@code custom.import.order.nonGroup.import} 209 * </li> 210 * <li> 211 * {@code custom.import.order.separated.internally} 212 * </li> 213 * </ul> 214 * 215 * @since 5.8 216 */ 217@FileStatefulCheck 218public class CustomImportOrderCheck extends AbstractCheck { 219 220 /** 221 * A key is pointing to the warning message text in "messages.properties" 222 * file. 223 */ 224 public static final String MSG_LINE_SEPARATOR = "custom.import.order.line.separator"; 225 226 /** 227 * A key is pointing to the warning message text in "messages.properties" 228 * file. 229 */ 230 public static final String MSG_SEPARATED_IN_GROUP = "custom.import.order.separated.internally"; 231 232 /** 233 * A key is pointing to the warning message text in "messages.properties" 234 * file. 235 */ 236 public static final String MSG_LEX = "custom.import.order.lex"; 237 238 /** 239 * A key is pointing to the warning message text in "messages.properties" 240 * file. 241 */ 242 public static final String MSG_NONGROUP_IMPORT = "custom.import.order.nonGroup.import"; 243 244 /** 245 * A key is pointing to the warning message text in "messages.properties" 246 * file. 247 */ 248 public static final String MSG_NONGROUP_EXPECTED = "custom.import.order.nonGroup.expected"; 249 250 /** 251 * A key is pointing to the warning message text in "messages.properties" 252 * file. 253 */ 254 public static final String MSG_ORDER = "custom.import.order"; 255 256 /** STATIC group name. */ 257 public static final String STATIC_RULE_GROUP = "STATIC"; 258 259 /** SAME_PACKAGE group name. */ 260 public static final String SAME_PACKAGE_RULE_GROUP = "SAME_PACKAGE"; 261 262 /** THIRD_PARTY_PACKAGE group name. */ 263 public static final String THIRD_PARTY_PACKAGE_RULE_GROUP = "THIRD_PARTY_PACKAGE"; 264 265 /** STANDARD_JAVA_PACKAGE group name. */ 266 public static final String STANDARD_JAVA_PACKAGE_RULE_GROUP = "STANDARD_JAVA_PACKAGE"; 267 268 /** SPECIAL_IMPORTS group name. */ 269 public static final String SPECIAL_IMPORTS_RULE_GROUP = "SPECIAL_IMPORTS"; 270 271 /** NON_GROUP group name. */ 272 private static final String NON_GROUP_RULE_GROUP = "NOT_ASSIGNED_TO_ANY_GROUP"; 273 274 /** Pattern used to separate groups of imports. */ 275 private static final Pattern GROUP_SEPARATOR_PATTERN = Pattern.compile("\\s*###\\s*"); 276 277 /** Specify ordered list of import groups. */ 278 private final List<String> customImportOrderRules = new ArrayList<>(); 279 280 /** Contains objects with import attributes. */ 281 private final List<ImportDetails> importToGroupList = new ArrayList<>(); 282 283 /** Specify RegExp for SAME_PACKAGE group imports. */ 284 private String samePackageDomainsRegExp = ""; 285 286 /** Specify RegExp for STANDARD_JAVA_PACKAGE group imports. */ 287 private Pattern standardPackageRegExp = Pattern.compile("^(java|javax)\\."); 288 289 /** Specify RegExp for THIRD_PARTY_PACKAGE group imports. */ 290 private Pattern thirdPartyPackageRegExp = Pattern.compile(".*"); 291 292 /** Specify RegExp for SPECIAL_IMPORTS group imports. */ 293 private Pattern specialImportsRegExp = Pattern.compile("^$"); 294 295 /** Force empty line separator between import groups. */ 296 private boolean separateLineBetweenGroups = true; 297 298 /** 299 * Force grouping alphabetically, 300 * in <a href="https://en.wikipedia.org/wiki/ASCII#Order"> ASCII sort order</a>. 301 */ 302 private boolean sortImportsInGroupAlphabetically; 303 304 /** Number of first domains for SAME_PACKAGE group. */ 305 private int samePackageMatchingDepth; 306 307 /** 308 * Setter to specify RegExp for STANDARD_JAVA_PACKAGE group imports. 309 * 310 * @param regexp 311 * user value. 312 * @since 5.8 313 */ 314 public final void setStandardPackageRegExp(Pattern regexp) { 315 standardPackageRegExp = regexp; 316 } 317 318 /** 319 * Setter to specify RegExp for THIRD_PARTY_PACKAGE group imports. 320 * 321 * @param regexp 322 * user value. 323 * @since 5.8 324 */ 325 public final void setThirdPartyPackageRegExp(Pattern regexp) { 326 thirdPartyPackageRegExp = regexp; 327 } 328 329 /** 330 * Setter to specify RegExp for SPECIAL_IMPORTS group imports. 331 * 332 * @param regexp 333 * user value. 334 * @since 5.8 335 */ 336 public final void setSpecialImportsRegExp(Pattern regexp) { 337 specialImportsRegExp = regexp; 338 } 339 340 /** 341 * Setter to force empty line separator between import groups. 342 * 343 * @param value 344 * user value. 345 * @since 5.8 346 */ 347 public final void setSeparateLineBetweenGroups(boolean value) { 348 separateLineBetweenGroups = value; 349 } 350 351 /** 352 * Setter to force grouping alphabetically, in 353 * <a href="https://en.wikipedia.org/wiki/ASCII#Order">ASCII sort order</a>. 354 * 355 * @param value 356 * user value. 357 * @since 5.8 358 */ 359 public final void setSortImportsInGroupAlphabetically(boolean value) { 360 sortImportsInGroupAlphabetically = value; 361 } 362 363 /** 364 * Setter to specify ordered list of import groups. 365 * 366 * @param rules 367 * user value. 368 * @since 5.8 369 */ 370 public final void setCustomImportOrderRules(String... rules) { 371 Arrays.stream(rules) 372 .map(GROUP_SEPARATOR_PATTERN::split) 373 .flatMap(Arrays::stream) 374 .forEach(this::addRulesToList); 375 376 customImportOrderRules.add(NON_GROUP_RULE_GROUP); 377 } 378 379 @Override 380 public int[] getDefaultTokens() { 381 return getRequiredTokens(); 382 } 383 384 @Override 385 public int[] getAcceptableTokens() { 386 return getRequiredTokens(); 387 } 388 389 @Override 390 public int[] getRequiredTokens() { 391 return new int[] { 392 TokenTypes.IMPORT, 393 TokenTypes.STATIC_IMPORT, 394 TokenTypes.PACKAGE_DEF, 395 }; 396 } 397 398 @Override 399 public void beginTree(DetailAST rootAST) { 400 importToGroupList.clear(); 401 } 402 403 @Override 404 public void visitToken(DetailAST ast) { 405 if (ast.getType() == TokenTypes.PACKAGE_DEF) { 406 samePackageDomainsRegExp = createSamePackageRegexp( 407 samePackageMatchingDepth, ast); 408 } 409 else { 410 final String importFullPath = getFullImportIdent(ast); 411 final boolean isStatic = ast.getType() == TokenTypes.STATIC_IMPORT; 412 importToGroupList.add(new ImportDetails(importFullPath, 413 getImportGroup(isStatic, importFullPath), isStatic, ast)); 414 } 415 } 416 417 @Override 418 public void finishTree(DetailAST rootAST) { 419 if (!importToGroupList.isEmpty()) { 420 finishImportList(); 421 } 422 } 423 424 /** Examine the order of all the imports and log any violations. */ 425 private void finishImportList() { 426 String currentGroup = getFirstGroup(); 427 int currentGroupNumber = customImportOrderRules.lastIndexOf(currentGroup); 428 ImportDetails previousImportObjectFromCurrentGroup = null; 429 String previousImportFromCurrentGroup = null; 430 431 for (ImportDetails importObject : importToGroupList) { 432 final String importGroup = importObject.getImportGroup(); 433 final String fullImportIdent = importObject.getImportFullPath(); 434 435 if (importGroup.equals(currentGroup)) { 436 validateExtraEmptyLine(previousImportObjectFromCurrentGroup, 437 importObject, fullImportIdent); 438 if (isAlphabeticalOrderBroken(previousImportFromCurrentGroup, fullImportIdent)) { 439 log(importObject.getImportAST(), MSG_LEX, 440 fullImportIdent, previousImportFromCurrentGroup); 441 } 442 else { 443 previousImportFromCurrentGroup = fullImportIdent; 444 } 445 previousImportObjectFromCurrentGroup = importObject; 446 } 447 else { 448 // not the last group, last one is always NON_GROUP 449 if (customImportOrderRules.size() > currentGroupNumber + 1) { 450 final String nextGroup = getNextImportGroup(currentGroupNumber + 1); 451 if (importGroup.equals(nextGroup)) { 452 validateMissedEmptyLine(previousImportObjectFromCurrentGroup, 453 importObject, fullImportIdent); 454 currentGroup = nextGroup; 455 currentGroupNumber = customImportOrderRules.lastIndexOf(nextGroup); 456 previousImportFromCurrentGroup = fullImportIdent; 457 } 458 else { 459 logWrongImportGroupOrder(importObject.getImportAST(), 460 importGroup, nextGroup, fullImportIdent); 461 } 462 previousImportObjectFromCurrentGroup = importObject; 463 } 464 else { 465 logWrongImportGroupOrder(importObject.getImportAST(), 466 importGroup, currentGroup, fullImportIdent); 467 } 468 } 469 } 470 } 471 472 /** 473 * Log violation if empty line is missed. 474 * 475 * @param previousImport previous import from current group. 476 * @param importObject current import. 477 * @param fullImportIdent full import identifier. 478 */ 479 private void validateMissedEmptyLine(ImportDetails previousImport, 480 ImportDetails importObject, String fullImportIdent) { 481 if (isEmptyLineMissed(previousImport, importObject)) { 482 log(importObject.getImportAST(), MSG_LINE_SEPARATOR, fullImportIdent); 483 } 484 } 485 486 /** 487 * Log violation if extra empty line is present. 488 * 489 * @param previousImport previous import from current group. 490 * @param importObject current import. 491 * @param fullImportIdent full import identifier. 492 */ 493 private void validateExtraEmptyLine(ImportDetails previousImport, 494 ImportDetails importObject, String fullImportIdent) { 495 if (isSeparatedByExtraEmptyLine(previousImport, importObject)) { 496 log(importObject.getImportAST(), MSG_SEPARATED_IN_GROUP, fullImportIdent); 497 } 498 } 499 500 /** 501 * Get first import group. 502 * 503 * @return 504 * first import group of file. 505 */ 506 private String getFirstGroup() { 507 final ImportDetails firstImport = importToGroupList.get(0); 508 return getImportGroup(firstImport.isStaticImport(), 509 firstImport.getImportFullPath()); 510 } 511 512 /** 513 * Examine alphabetical order of imports. 514 * 515 * @param previousImport 516 * previous import of current group. 517 * @param currentImport 518 * current import. 519 * @return 520 * true, if previous and current import are not in alphabetical order. 521 */ 522 private boolean isAlphabeticalOrderBroken(String previousImport, 523 String currentImport) { 524 return sortImportsInGroupAlphabetically 525 && previousImport != null 526 && compareImports(currentImport, previousImport) < 0; 527 } 528 529 /** 530 * Examine empty lines between groups. 531 * 532 * @param previousImportObject 533 * previous import in current group. 534 * @param currentImportObject 535 * current import. 536 * @return 537 * true, if current import NOT separated from previous import by empty line. 538 */ 539 private boolean isEmptyLineMissed(ImportDetails previousImportObject, 540 ImportDetails currentImportObject) { 541 return separateLineBetweenGroups 542 && getCountOfEmptyLinesBetween( 543 previousImportObject.getEndLineNumber(), 544 currentImportObject.getStartLineNumber()) != 1; 545 } 546 547 /** 548 * Examine that imports separated by more than one empty line. 549 * 550 * @param previousImportObject 551 * previous import in current group. 552 * @param currentImportObject 553 * current import. 554 * @return 555 * true, if current import separated from previous by more than one empty line. 556 */ 557 private boolean isSeparatedByExtraEmptyLine(ImportDetails previousImportObject, 558 ImportDetails currentImportObject) { 559 return previousImportObject != null 560 && getCountOfEmptyLinesBetween( 561 previousImportObject.getEndLineNumber(), 562 currentImportObject.getStartLineNumber()) > 0; 563 } 564 565 /** 566 * Log wrong import group order. 567 * 568 * @param importAST 569 * import ast. 570 * @param importGroup 571 * import group. 572 * @param currentGroupNumber 573 * current group number we are checking. 574 * @param fullImportIdent 575 * full import name. 576 */ 577 private void logWrongImportGroupOrder(DetailAST importAST, String importGroup, 578 String currentGroupNumber, String fullImportIdent) { 579 if (NON_GROUP_RULE_GROUP.equals(importGroup)) { 580 log(importAST, MSG_NONGROUP_IMPORT, fullImportIdent); 581 } 582 else if (NON_GROUP_RULE_GROUP.equals(currentGroupNumber)) { 583 log(importAST, MSG_NONGROUP_EXPECTED, importGroup, fullImportIdent); 584 } 585 else { 586 log(importAST, MSG_ORDER, importGroup, currentGroupNumber, fullImportIdent); 587 } 588 } 589 590 /** 591 * Get next import group. 592 * 593 * @param currentGroupNumber 594 * current group number. 595 * @return 596 * next import group. 597 */ 598 private String getNextImportGroup(int currentGroupNumber) { 599 int nextGroupNumber = currentGroupNumber; 600 601 while (customImportOrderRules.size() > nextGroupNumber + 1) { 602 if (hasAnyImportInCurrentGroup(customImportOrderRules.get(nextGroupNumber))) { 603 break; 604 } 605 nextGroupNumber++; 606 } 607 return customImportOrderRules.get(nextGroupNumber); 608 } 609 610 /** 611 * Checks if current group contains any import. 612 * 613 * @param currentGroup 614 * current group. 615 * @return 616 * true, if current group contains at least one import. 617 */ 618 private boolean hasAnyImportInCurrentGroup(String currentGroup) { 619 boolean result = false; 620 for (ImportDetails currentImport : importToGroupList) { 621 if (currentGroup.equals(currentImport.getImportGroup())) { 622 result = true; 623 break; 624 } 625 } 626 return result; 627 } 628 629 /** 630 * Get import valid group. 631 * 632 * @param isStatic 633 * is static import. 634 * @param importPath 635 * full import path. 636 * @return import valid group. 637 */ 638 private String getImportGroup(boolean isStatic, String importPath) { 639 RuleMatchForImport bestMatch = new RuleMatchForImport(NON_GROUP_RULE_GROUP, 0, 0); 640 if (isStatic && customImportOrderRules.contains(STATIC_RULE_GROUP)) { 641 bestMatch.group = STATIC_RULE_GROUP; 642 bestMatch.matchLength = importPath.length(); 643 } 644 else if (customImportOrderRules.contains(SAME_PACKAGE_RULE_GROUP)) { 645 final String importPathTrimmedToSamePackageDepth = 646 getFirstDomainsFromIdent(samePackageMatchingDepth, importPath); 647 if (samePackageDomainsRegExp.equals(importPathTrimmedToSamePackageDepth)) { 648 bestMatch.group = SAME_PACKAGE_RULE_GROUP; 649 bestMatch.matchLength = importPath.length(); 650 } 651 } 652 for (String group : customImportOrderRules) { 653 if (STANDARD_JAVA_PACKAGE_RULE_GROUP.equals(group)) { 654 bestMatch = findBetterPatternMatch(importPath, 655 STANDARD_JAVA_PACKAGE_RULE_GROUP, standardPackageRegExp, bestMatch); 656 } 657 if (SPECIAL_IMPORTS_RULE_GROUP.equals(group)) { 658 bestMatch = findBetterPatternMatch(importPath, 659 group, specialImportsRegExp, bestMatch); 660 } 661 } 662 663 if (NON_GROUP_RULE_GROUP.equals(bestMatch.group) 664 && customImportOrderRules.contains(THIRD_PARTY_PACKAGE_RULE_GROUP) 665 && thirdPartyPackageRegExp.matcher(importPath).find()) { 666 bestMatch.group = THIRD_PARTY_PACKAGE_RULE_GROUP; 667 } 668 return bestMatch.group; 669 } 670 671 /** 672 * Tries to find better matching regular expression: 673 * longer matching substring wins; in case of the same length, 674 * lower position of matching substring wins. 675 * 676 * @param importPath 677 * Full import identifier 678 * @param group 679 * Import group we are trying to assign the import 680 * @param regExp 681 * Regular expression for import group 682 * @param currentBestMatch 683 * object with currently best match 684 * @return better match (if found) or the same (currentBestMatch) 685 */ 686 private static RuleMatchForImport findBetterPatternMatch(String importPath, String group, 687 Pattern regExp, RuleMatchForImport currentBestMatch) { 688 RuleMatchForImport betterMatchCandidate = currentBestMatch; 689 final Matcher matcher = regExp.matcher(importPath); 690 while (matcher.find()) { 691 final int matchStart = matcher.start(); 692 final int length = matcher.end() - matchStart; 693 if (length > betterMatchCandidate.matchLength 694 || length == betterMatchCandidate.matchLength 695 && matchStart < betterMatchCandidate.matchPosition) { 696 betterMatchCandidate = new RuleMatchForImport(group, length, matchStart); 697 } 698 } 699 return betterMatchCandidate; 700 } 701 702 /** 703 * Checks compare two import paths. 704 * 705 * @param import1 706 * current import. 707 * @param import2 708 * previous import. 709 * @return a negative integer, zero, or a positive integer as the 710 * specified String is greater than, equal to, or less 711 * than this String, ignoring case considerations. 712 */ 713 private static int compareImports(String import1, String import2) { 714 int result = 0; 715 final String separator = "\\."; 716 final String[] import1Tokens = import1.split(separator); 717 final String[] import2Tokens = import2.split(separator); 718 for (int i = 0; i != import1Tokens.length && i != import2Tokens.length; i++) { 719 final String import1Token = import1Tokens[i]; 720 final String import2Token = import2Tokens[i]; 721 result = import1Token.compareTo(import2Token); 722 if (result != 0) { 723 break; 724 } 725 } 726 if (result == 0) { 727 result = Integer.compare(import1Tokens.length, import2Tokens.length); 728 } 729 return result; 730 } 731 732 /** 733 * Counts empty lines between given parameters. 734 * 735 * @param fromLineNo 736 * One-based line number of previous import. 737 * @param toLineNo 738 * One-based line number of current import. 739 * @return count of empty lines between given parameters, exclusive, 740 * eg., (fromLineNo, toLineNo). 741 */ 742 private int getCountOfEmptyLinesBetween(int fromLineNo, int toLineNo) { 743 int result = 0; 744 final String[] lines = getLines(); 745 746 for (int i = fromLineNo + 1; i <= toLineNo - 1; i++) { 747 // "- 1" because the numbering is one-based 748 if (CommonUtil.isBlank(lines[i - 1])) { 749 result++; 750 } 751 } 752 return result; 753 } 754 755 /** 756 * Forms import full path. 757 * 758 * @param token 759 * current token. 760 * @return full path or null. 761 */ 762 private static String getFullImportIdent(DetailAST token) { 763 String ident = ""; 764 if (token != null) { 765 ident = FullIdent.createFullIdent(token.findFirstToken(TokenTypes.DOT)).getText(); 766 } 767 return ident; 768 } 769 770 /** 771 * Parses ordering rule and adds it to the list with rules. 772 * 773 * @param ruleStr 774 * String with rule. 775 * @throws IllegalArgumentException when SAME_PACKAGE rule parameter is not positive integer 776 * @throws IllegalStateException when ruleStr is unexpected value 777 */ 778 private void addRulesToList(String ruleStr) { 779 if (STATIC_RULE_GROUP.equals(ruleStr) 780 || THIRD_PARTY_PACKAGE_RULE_GROUP.equals(ruleStr) 781 || STANDARD_JAVA_PACKAGE_RULE_GROUP.equals(ruleStr) 782 || SPECIAL_IMPORTS_RULE_GROUP.equals(ruleStr)) { 783 customImportOrderRules.add(ruleStr); 784 } 785 else if (ruleStr.startsWith(SAME_PACKAGE_RULE_GROUP)) { 786 final String rule = ruleStr.substring(ruleStr.indexOf('(') + 1, 787 ruleStr.indexOf(')')); 788 samePackageMatchingDepth = Integer.parseInt(rule); 789 if (samePackageMatchingDepth <= 0) { 790 throw new IllegalArgumentException( 791 "SAME_PACKAGE rule parameter should be positive integer: " + ruleStr); 792 } 793 customImportOrderRules.add(SAME_PACKAGE_RULE_GROUP); 794 } 795 else { 796 throw new IllegalStateException("Unexpected rule: " + ruleStr); 797 } 798 } 799 800 /** 801 * Creates samePackageDomainsRegExp of the first package domains. 802 * 803 * @param firstPackageDomainsCount 804 * number of first package domains. 805 * @param packageNode 806 * package node. 807 * @return same package regexp. 808 */ 809 private static String createSamePackageRegexp(int firstPackageDomainsCount, 810 DetailAST packageNode) { 811 final String packageFullPath = getFullImportIdent(packageNode); 812 return getFirstDomainsFromIdent(firstPackageDomainsCount, packageFullPath); 813 } 814 815 /** 816 * Extracts defined amount of domains from the left side of package/import identifier. 817 * 818 * @param firstPackageDomainsCount 819 * number of first package domains. 820 * @param packageFullPath 821 * full identifier containing path to package or imported object. 822 * @return String with defined amount of domains or full identifier 823 * (if full identifier had less domain than specified) 824 */ 825 private static String getFirstDomainsFromIdent( 826 final int firstPackageDomainsCount, final String packageFullPath) { 827 final StringBuilder builder = new StringBuilder(256); 828 final StringTokenizer tokens = new StringTokenizer(packageFullPath, "."); 829 int count = firstPackageDomainsCount; 830 831 while (count > 0 && tokens.hasMoreTokens()) { 832 builder.append(tokens.nextToken()); 833 count--; 834 } 835 return builder.toString(); 836 } 837 838 /** 839 * Contains import attributes as line number, import full path, import 840 * group. 841 */ 842 private static final class ImportDetails { 843 844 /** Import full path. */ 845 private final String importFullPath; 846 847 /** Import group. */ 848 private final String importGroup; 849 850 /** Is static import. */ 851 private final boolean staticImport; 852 853 /** Import AST. */ 854 private final DetailAST importAST; 855 856 /** 857 * Initialise importFullPath, importGroup, staticImport, importAST. 858 * 859 * @param importFullPath 860 * import full path. 861 * @param importGroup 862 * import group. 863 * @param staticImport 864 * if import is static. 865 * @param importAST 866 * import ast 867 */ 868 private ImportDetails(String importFullPath, String importGroup, boolean staticImport, 869 DetailAST importAST) { 870 this.importFullPath = importFullPath; 871 this.importGroup = importGroup; 872 this.staticImport = staticImport; 873 this.importAST = importAST; 874 } 875 876 /** 877 * Get import full path variable. 878 * 879 * @return import full path variable. 880 */ 881 public String getImportFullPath() { 882 return importFullPath; 883 } 884 885 /** 886 * Get import start line number from ast. 887 * 888 * @return import start line from ast. 889 */ 890 public int getStartLineNumber() { 891 return importAST.getLineNo(); 892 } 893 894 /** 895 * Get import end line number from ast. 896 * 897 * <p> 898 * <b>Note:</b> It can be different from <b>startLineNumber</b> when import statement span 899 * multiple lines. 900 * </p> 901 * 902 * @return import end line from ast. 903 */ 904 public int getEndLineNumber() { 905 return importAST.getLastChild().getLineNo(); 906 } 907 908 /** 909 * Get import group. 910 * 911 * @return import group. 912 */ 913 public String getImportGroup() { 914 return importGroup; 915 } 916 917 /** 918 * Checks if import is static. 919 * 920 * @return true, if import is static. 921 */ 922 public boolean isStaticImport() { 923 return staticImport; 924 } 925 926 /** 927 * Get import ast. 928 * 929 * @return import ast. 930 */ 931 public DetailAST getImportAST() { 932 return importAST; 933 } 934 935 } 936 937 /** 938 * Contains matching attributes assisting in definition of "best matching" 939 * group for import. 940 */ 941 private static final class RuleMatchForImport { 942 943 /** Position of matching string for current best match. */ 944 private final int matchPosition; 945 /** Length of matching string for current best match. */ 946 private int matchLength; 947 /** Import group for current best match. */ 948 private String group; 949 950 /** 951 * Constructor to initialize the fields. 952 * 953 * @param group 954 * Matched group. 955 * @param length 956 * Matching length. 957 * @param position 958 * Matching position. 959 */ 960 private RuleMatchForImport(String group, int length, int position) { 961 this.group = group; 962 matchLength = length; 963 matchPosition = position; 964 } 965 966 } 967 968}