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