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