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