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