1 //////////////////////////////////////////////////////////////////////////////// 2 // checkstyle: Checks Java source code for adherence to a set of rules. 3 // Copyright (C) 2001-2019 the original author or authors. 4 // 5 // This library is free software; you can redistribute it and/or 6 // modify it under the terms of the GNU Lesser General Public 7 // License as published by the Free Software Foundation; either 8 // version 2.1 of the License, or (at your option) any later version. 9 // 10 // This library is distributed in the hope that it will be useful, 11 // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 // Lesser General Public License for more details. 14 // 15 // You should have received a copy of the GNU Lesser General Public 16 // License along with this library; if not, write to the Free Software 17 // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 18 //////////////////////////////////////////////////////////////////////////////// 19 20 package com.puppycrawl.tools.checkstyle.checks.imports; 21 22 import java.util.ArrayList; 23 import java.util.List; 24 import java.util.StringTokenizer; 25 import java.util.regex.Matcher; 26 import java.util.regex.Pattern; 27 28 import com.puppycrawl.tools.checkstyle.FileStatefulCheck; 29 import com.puppycrawl.tools.checkstyle.api.AbstractCheck; 30 import com.puppycrawl.tools.checkstyle.api.DetailAST; 31 import com.puppycrawl.tools.checkstyle.api.FullIdent; 32 import com.puppycrawl.tools.checkstyle.api.TokenTypes; 33 import com.puppycrawl.tools.checkstyle.utils.CommonUtil; 34 35 /** 36 * <p> 37 * Checks that the groups of import declarations appear in the order specified 38 * by the user. If there is an import but its group is not specified in the 39 * configuration such an import should be placed at the end of the import list. 40 * </p> 41 * <p> 42 * The rule consists of: 43 * </p> 44 * <ol> 45 * <li> 46 * STATIC group. This group sets the ordering of static imports. 47 * </li> 48 * <li> 49 * SAME_PACKAGE(n) group. This group sets the ordering of the same package imports. 50 * Imports are considered on SAME_PACKAGE group if <b>n</b> first domains in package 51 * name and import name are identical: 52 * <pre> 53 * package java.util.concurrent.locks; 54 * 55 * import java.io.File; 56 * import java.util.*; //#1 57 * import java.util.List; //#2 58 * import java.util.StringTokenizer; //#3 59 * import java.util.concurrent.*; //#4 60 * import java.util.concurrent.AbstractExecutorService; //#5 61 * import java.util.concurrent.locks.LockSupport; //#6 62 * import java.util.regex.Pattern; //#7 63 * import java.util.regex.Matcher; //#8 64 * </pre> 65 * If we have SAME_PACKAGE(3) on configuration file, imports #4-6 will be considered as 66 * a SAME_PACKAGE group (java.util.concurrent.*, java.util.concurrent.AbstractExecutorService, 67 * java.util.concurrent.locks.LockSupport). SAME_PACKAGE(2) will include #1-8. 68 * SAME_PACKAGE(4) will include only #6. SAME_PACKAGE(5) will result in no imports assigned 69 * to SAME_PACKAGE group because actual package java.util.concurrent.locks has only 4 domains. 70 * </li> 71 * <li> 72 * THIRD_PARTY_PACKAGE group. This group sets ordering of third party imports. 73 * Third party imports are all imports except STATIC, SAME_PACKAGE(n), STANDARD_JAVA_PACKAGE and 74 * SPECIAL_IMPORTS. 75 * </li> 76 * <li> 77 * STANDARD_JAVA_PACKAGE group. By default this group sets ordering of standard java/javax imports. 78 * </li> 79 * <li> 80 * SPECIAL_IMPORTS group. This group may contains some imports that have particular meaning for the 81 * user. 82 * </li> 83 * </ol> 84 * <p> 85 * Use the separator '###' between rules. 86 * </p> 87 * <p> 88 * To set RegExps for THIRD_PARTY_PACKAGE and STANDARD_JAVA_PACKAGE groups use 89 * thirdPartyPackageRegExp and standardPackageRegExp options. 90 * </p> 91 * <p> 92 * Pretty often one import can match more than one group. For example, static import from standard 93 * package or regular expressions are configured to allow one import match multiple groups. 94 * In this case, group will be assigned according to priorities: 95 * </p> 96 * <ol> 97 * <li> 98 * STATIC has top priority 99 * </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 list of order declaration customizing by user. 139 * Default value is {@code {}}. 140 * </li> 141 * <li> 142 * Property {@code standardPackageRegExp} - Specify RegExp for STANDARD_JAVA_PACKAGE group imports. 143 * Default value is {@code "^(java|javax)\."}. 144 * </li> 145 * <li> 146 * Property {@code thirdPartyPackageRegExp} - Specify RegExp for THIRD_PARTY_PACKAGE group imports. 147 * Default value is {@code ".*"}. 148 * </li> 149 * <li> 150 * Property {@code specialImportsRegExp} - Specify RegExp for SPECIAL_IMPORTS group imports. 151 * Default value is {@code "^$" (empty)}. 152 * </li> 153 * <li> 154 * Property {@code separateLineBetweenGroups} - Force empty line separator between 155 * import groups. 156 * Default value is {@code true}. 157 * </li> 158 * <li> 159 * Property {@code sortImportsInGroupAlphabetically} - Force grouping alphabetically, 160 * in <a href="https://en.wikipedia.org/wiki/ASCII#Order">ASCII sort order</a>. 161 * Default value is {@code false}. 162 * </li> 163 * </ul> 164 * <p> 165 * To configure the check so that it matches default Eclipse formatter configuration 166 * (tested on Kepler and Luna releases): 167 * </p> 168 * <ul> 169 * <li> 170 * group of static imports is on the top 171 * </li> 172 * <li> 173 * groups of non-static imports: "java" and "javax" packages first, then "org" and then all other 174 * imports 175 * </li> 176 * <li> 177 * imports will be sorted in the groups 178 * </li> 179 * <li> 180 * groups are separated by single blank line 181 * </li> 182 * </ul> 183 * <p> 184 * Notes: 185 * </p> 186 * <ul> 187 * <li> 188 * "com" package is not mentioned on configuration, because it is ignored by Eclipse Kepler and Luna 189 * (looks like Eclipse defect) 190 * </li> 191 * <li> 192 * configuration below doesn't work in all 100% cases due to inconsistent behavior prior to Mars 193 * release, but covers most scenarios 194 * </li> 195 * </ul> 196 * <pre> 197 * <module name="CustomImportOrder"> 198 * <property name="customImportOrderRules" 199 * value="STATIC###STANDARD_JAVA_PACKAGE###SPECIAL_IMPORTS"/> 200 * <property name="specialImportsRegExp" value="^org\."/> 201 * <property name="sortImportsInGroupAlphabetically" value="true"/> 202 * <property name="separateLineBetweenGroups" value="true"/> 203 * </module> 204 * </pre> 205 * <p> 206 * To configure the check so that it matches default Eclipse formatter configuration 207 * (tested on Mars release): 208 * </p> 209 * <ul> 210 * <li> 211 * group of static imports is on the top 212 * </li> 213 * <li> 214 * groups of non-static imports: "java" and "javax" packages first, then "org" and "com", 215 * then all other imports as one group 216 * </li> 217 * <li> 218 * imports will be sorted in the groups 219 * </li> 220 * <li> 221 * groups are separated by, at least, one blank line 222 * </li> 223 * </ul> 224 * <pre> 225 * <module name="CustomImportOrder"> 226 * <property name="customImportOrderRules" 227 * value="STATIC###STANDARD_JAVA_PACKAGE###SPECIAL_IMPORTS###THIRD_PARTY_PACKAGE"/> 228 * <property name="specialImportsRegExp" value="^org\."/> 229 * <property name="thirdPartyPackageRegExp" value="^com\."/> 230 * <property name="sortImportsInGroupAlphabetically" value="true"/> 231 * <property name="separateLineBetweenGroups" value="true"/> 232 * </module> 233 * </pre> 234 * <p> 235 * To configure the check so that it matches default IntelliJ IDEA formatter configuration 236 * (tested on v14): 237 * </p> 238 * <ul> 239 * <li> 240 * group of static imports is on the bottom 241 * </li> 242 * <li> 243 * groups of non-static imports: all imports except of "javax" and "java", then "javax" and "java" 244 * </li> 245 * <li> 246 * imports will be sorted in the groups 247 * </li> 248 * <li> 249 * groups are separated by, at least, one blank line 250 * </li> 251 * </ul> 252 * <p> 253 * Note: "separated" option is disabled because IDEA default has blank line between "java" and 254 * static imports, and no blank line between "javax" and "java" 255 * </p> 256 * <pre> 257 * <module name="CustomImportOrder"> 258 * <property name="customImportOrderRules" 259 * value="THIRD_PARTY_PACKAGE###SPECIAL_IMPORTS###STANDARD_JAVA_PACKAGE###STATIC"/> 260 * <property name="specialImportsRegExp" value="^javax\."/> 261 * <property name="standardPackageRegExp" value="^java\."/> 262 * <property name="sortImportsInGroupAlphabetically" value="true"/> 263 * <property name="separateLineBetweenGroups" value="false"/> 264 * </module> 265 * </pre> 266 * <p> 267 * To configure the check so that it matches default NetBeans formatter configuration 268 * (tested on v8): 269 * </p> 270 * <ul> 271 * <li> 272 * groups of non-static imports are not defined, all imports will be sorted as a one group 273 * </li> 274 * <li> 275 * static imports are not separated, they will be sorted along with other imports 276 * </li> 277 * </ul> 278 * <pre> 279 * <module name="CustomImportOrder"/> 280 * </pre> 281 * <p> 282 * To set RegExps for THIRD_PARTY_PACKAGE and STANDARD_JAVA_PACKAGE groups use 283 * thirdPartyPackageRegExp and standardPackageRegExp options. 284 * </p> 285 * <pre> 286 * <module name="CustomImportOrder"> 287 * <property name="customImportOrderRules" 288 * value="STATIC###SAME_PACKAGE(3)###THIRD_PARTY_PACKAGE###STANDARD_JAVA_PACKAGE"/> 289 * <property name="thirdPartyPackageRegExp" value="^(com|org)\."/> 290 * <property name="standardPackageRegExp" value="^(java|javax)\."/> 291 * </module> 292 * </pre> 293 * <p> 294 * Also, this check can be configured to force empty line separator between 295 * import groups. For example. 296 * </p> 297 * <pre> 298 * <module name="CustomImportOrder"> 299 * <property name="separateLineBetweenGroups" value="true"/> 300 * </module> 301 * </pre> 302 * <p> 303 * It is possible to enforce 304 * <a href="https://en.wikipedia.org/wiki/ASCII#Order">ASCII sort order</a> 305 * of imports in groups using the following configuration: 306 * </p> 307 * <pre> 308 * <module name="CustomImportOrder"> 309 * <property name="sortImportsInGroupAlphabetically" value="true"/> 310 * </module> 311 * </pre> 312 * <p> 313 * Example of ASCII order: 314 * </p> 315 * <pre> 316 * import java.awt.Dialog; 317 * import java.awt.Window; 318 * import java.awt.color.ColorSpace; 319 * import java.awt.Frame; // violation here - in ASCII order 'F' should go before 'c', 320 * // as all uppercase come before lowercase letters 321 * </pre> 322 * <p> 323 * To force checking imports sequence such as: 324 * </p> 325 * <pre> 326 * package com.puppycrawl.tools.checkstyle.imports; 327 * 328 * import com.google.common.annotations.GwtCompatible; 329 * import com.google.common.annotations.Beta; 330 * import com.google.common.annotations.VisibleForTesting; 331 * 332 * import org.abego.treelayout.Configuration; 333 * 334 * import static sun.tools.util.ModifierFilter.ALL_ACCESS; 335 * 336 * import com.google.common.annotations.GwtCompatible; // violation here - should be in the 337 * // THIRD_PARTY_PACKAGE group 338 * import android.*; 339 * </pre> 340 * <p> 341 * configure as follows: 342 * </p> 343 * <pre> 344 * <module name="CustomImportOrder"> 345 * <property name="customImportOrderRules" 346 * value="SAME_PACKAGE(3)###THIRD_PARTY_PACKAGE###STATIC###SPECIAL_IMPORTS"/> 347 * <property name="specialImportsRegExp" value="^android\."/> 348 * </module> 349 * </pre> 350 * 351 * @since 5.8 352 */ 353 @FileStatefulCheck 354 public class CustomImportOrderCheck extends AbstractCheck { 355 356 /** 357 * A key is pointing to the warning message text in "messages.properties" 358 * file. 359 */ 360 public static final String MSG_LINE_SEPARATOR = "custom.import.order.line.separator"; 361 362 /** 363 * A key is pointing to the warning message text in "messages.properties" 364 * file. 365 */ 366 public static final String MSG_LEX = "custom.import.order.lex"; 367 368 /** 369 * A key is pointing to the warning message text in "messages.properties" 370 * file. 371 */ 372 public static final String MSG_NONGROUP_IMPORT = "custom.import.order.nonGroup.import"; 373 374 /** 375 * A key is pointing to the warning message text in "messages.properties" 376 * file. 377 */ 378 public static final String MSG_NONGROUP_EXPECTED = "custom.import.order.nonGroup.expected"; 379 380 /** 381 * A key is pointing to the warning message text in "messages.properties" 382 * file. 383 */ 384 public static final String MSG_ORDER = "custom.import.order"; 385 386 /** STATIC group name. */ 387 public static final String STATIC_RULE_GROUP = "STATIC"; 388 389 /** SAME_PACKAGE group name. */ 390 public static final String SAME_PACKAGE_RULE_GROUP = "SAME_PACKAGE"; 391 392 /** THIRD_PARTY_PACKAGE group name. */ 393 public static final String THIRD_PARTY_PACKAGE_RULE_GROUP = "THIRD_PARTY_PACKAGE"; 394 395 /** STANDARD_JAVA_PACKAGE group name. */ 396 public static final String STANDARD_JAVA_PACKAGE_RULE_GROUP = "STANDARD_JAVA_PACKAGE"; 397 398 /** SPECIAL_IMPORTS group name. */ 399 public static final String SPECIAL_IMPORTS_RULE_GROUP = "SPECIAL_IMPORTS"; 400 401 /** NON_GROUP group name. */ 402 private static final String NON_GROUP_RULE_GROUP = "NOT_ASSIGNED_TO_ANY_GROUP"; 403 404 /** Pattern used to separate groups of imports. */ 405 private static final Pattern GROUP_SEPARATOR_PATTERN = Pattern.compile("\\s*###\\s*"); 406 407 /** Specify list of order declaration customizing by user. */ 408 private final List<String> customImportOrderRules = new ArrayList<>(); 409 410 /** Contains objects with import attributes. */ 411 private final List<ImportDetails> importToGroupList = new ArrayList<>(); 412 413 /** Specify RegExp for SAME_PACKAGE group imports. */ 414 private String samePackageDomainsRegExp = ""; 415 416 /** Specify RegExp for STANDARD_JAVA_PACKAGE group imports. */ 417 private Pattern standardPackageRegExp = Pattern.compile("^(java|javax)\\."); 418 419 /** Specify RegExp for THIRD_PARTY_PACKAGE group imports. */ 420 private Pattern thirdPartyPackageRegExp = Pattern.compile(".*"); 421 422 /** Specify RegExp for SPECIAL_IMPORTS group imports. */ 423 private Pattern specialImportsRegExp = Pattern.compile("^$"); 424 425 /** Force empty line separator between import groups. */ 426 private boolean separateLineBetweenGroups = true; 427 428 /** 429 * Force grouping alphabetically, 430 * in <a href="https://en.wikipedia.org/wiki/ASCII#Order"> ASCII sort order</a>. 431 */ 432 private boolean sortImportsInGroupAlphabetically; 433 434 /** Number of first domains for SAME_PACKAGE group. */ 435 private int samePackageMatchingDepth = 2; 436 437 /** 438 * Setter to specify RegExp for STANDARD_JAVA_PACKAGE group imports. 439 * @param regexp 440 * user value. 441 */ 442 public final void setStandardPackageRegExp(Pattern regexp) { 443 standardPackageRegExp = regexp; 444 } 445 446 /** 447 * Setter to specify RegExp for THIRD_PARTY_PACKAGE group imports. 448 * @param regexp 449 * user value. 450 */ 451 public final void setThirdPartyPackageRegExp(Pattern regexp) { 452 thirdPartyPackageRegExp = regexp; 453 } 454 455 /** 456 * Setter to specify RegExp for SPECIAL_IMPORTS group imports. 457 * @param regexp 458 * user value. 459 */ 460 public final void setSpecialImportsRegExp(Pattern regexp) { 461 specialImportsRegExp = regexp; 462 } 463 464 /** 465 * Setter to force empty line separator between import groups. 466 * @param value 467 * user value. 468 */ 469 public final void setSeparateLineBetweenGroups(boolean value) { 470 separateLineBetweenGroups = value; 471 } 472 473 /** 474 * Setter to force grouping alphabetically, in 475 * <a href="https://en.wikipedia.org/wiki/ASCII#Order">ASCII sort order</a>. 476 * @param value 477 * user value. 478 */ 479 public final void setSortImportsInGroupAlphabetically(boolean value) { 480 sortImportsInGroupAlphabetically = value; 481 } 482 483 /** 484 * Setter to specify list of order declaration customizing by user. 485 * @param inputCustomImportOrder 486 * user value. 487 */ 488 public final void setCustomImportOrderRules(final String inputCustomImportOrder) { 489 for (String currentState : GROUP_SEPARATOR_PATTERN.split(inputCustomImportOrder)) { 490 addRulesToList(currentState); 491 } 492 customImportOrderRules.add(NON_GROUP_RULE_GROUP); 493 } 494 495 @Override 496 public int[] getDefaultTokens() { 497 return getRequiredTokens(); 498 } 499 500 @Override 501 public int[] getAcceptableTokens() { 502 return getRequiredTokens(); 503 } 504 505 @Override 506 public int[] getRequiredTokens() { 507 return new int[] { 508 TokenTypes.IMPORT, 509 TokenTypes.STATIC_IMPORT, 510 TokenTypes.PACKAGE_DEF, 511 }; 512 } 513 514 @Override 515 public void beginTree(DetailAST rootAST) { 516 importToGroupList.clear(); 517 } 518 519 @Override 520 public void visitToken(DetailAST ast) { 521 if (ast.getType() == TokenTypes.PACKAGE_DEF) { 522 if (customImportOrderRules.contains(SAME_PACKAGE_RULE_GROUP)) { 523 samePackageDomainsRegExp = createSamePackageRegexp( 524 samePackageMatchingDepth, ast); 525 } 526 } 527 else { 528 final String importFullPath = getFullImportIdent(ast); 529 final int lineNo = ast.getLineNo(); 530 final boolean isStatic = ast.getType() == TokenTypes.STATIC_IMPORT; 531 importToGroupList.add(new ImportDetails(importFullPath, 532 lineNo, getImportGroup(isStatic, importFullPath), 533 isStatic)); 534 } 535 } 536 537 @Override 538 public void finishTree(DetailAST rootAST) { 539 if (!importToGroupList.isEmpty()) { 540 finishImportList(); 541 } 542 } 543 544 /** Examine the order of all the imports and log any violations. */ 545 private void finishImportList() { 546 final ImportDetails firstImport = importToGroupList.get(0); 547 String currentGroup = getImportGroup(firstImport.isStaticImport(), 548 firstImport.getImportFullPath()); 549 int currentGroupNumber = customImportOrderRules.indexOf(currentGroup); 550 String previousImportFromCurrentGroup = null; 551 552 for (ImportDetails importObject : importToGroupList) { 553 final String importGroup = importObject.getImportGroup(); 554 final String fullImportIdent = importObject.getImportFullPath(); 555 556 if (getCountOfEmptyLinesBefore(importObject.getLineNumber()) > 1) { 557 log(importObject.getLineNumber(), MSG_LINE_SEPARATOR, fullImportIdent); 558 } 559 if (importGroup.equals(currentGroup)) { 560 if (sortImportsInGroupAlphabetically 561 && previousImportFromCurrentGroup != null 562 && compareImports(fullImportIdent, previousImportFromCurrentGroup) < 0) { 563 log(importObject.getLineNumber(), MSG_LEX, 564 fullImportIdent, previousImportFromCurrentGroup); 565 } 566 else { 567 previousImportFromCurrentGroup = fullImportIdent; 568 } 569 } 570 else { 571 //not the last group, last one is always NON_GROUP 572 if (customImportOrderRules.size() > currentGroupNumber + 1) { 573 final String nextGroup = getNextImportGroup(currentGroupNumber + 1); 574 if (importGroup.equals(nextGroup)) { 575 if (separateLineBetweenGroups 576 && getCountOfEmptyLinesBefore(importObject.getLineNumber()) == 0) { 577 log(importObject.getLineNumber(), MSG_LINE_SEPARATOR, fullImportIdent); 578 } 579 currentGroup = nextGroup; 580 currentGroupNumber = customImportOrderRules.indexOf(nextGroup); 581 previousImportFromCurrentGroup = fullImportIdent; 582 } 583 else { 584 logWrongImportGroupOrder(importObject.getLineNumber(), 585 importGroup, nextGroup, fullImportIdent); 586 } 587 } 588 else { 589 logWrongImportGroupOrder(importObject.getLineNumber(), 590 importGroup, currentGroup, fullImportIdent); 591 } 592 } 593 } 594 } 595 596 /** 597 * Log wrong import group order. 598 * @param currentImportLine 599 * line number of current import current import. 600 * @param importGroup 601 * import group. 602 * @param currentGroupNumber 603 * current group number we are checking. 604 * @param fullImportIdent 605 * full import name. 606 */ 607 private void logWrongImportGroupOrder(int currentImportLine, String importGroup, 608 String currentGroupNumber, String fullImportIdent) { 609 if (NON_GROUP_RULE_GROUP.equals(importGroup)) { 610 log(currentImportLine, MSG_NONGROUP_IMPORT, fullImportIdent); 611 } 612 else if (NON_GROUP_RULE_GROUP.equals(currentGroupNumber)) { 613 log(currentImportLine, MSG_NONGROUP_EXPECTED, importGroup, fullImportIdent); 614 } 615 else { 616 log(currentImportLine, MSG_ORDER, importGroup, currentGroupNumber, fullImportIdent); 617 } 618 } 619 620 /** 621 * Get next import group. 622 * @param currentGroupNumber 623 * current group number. 624 * @return 625 * next import group. 626 */ 627 private String getNextImportGroup(int currentGroupNumber) { 628 int nextGroupNumber = currentGroupNumber; 629 630 while (customImportOrderRules.size() > nextGroupNumber + 1) { 631 if (hasAnyImportInCurrentGroup(customImportOrderRules.get(nextGroupNumber))) { 632 break; 633 } 634 nextGroupNumber++; 635 } 636 return customImportOrderRules.get(nextGroupNumber); 637 } 638 639 /** 640 * Checks if current group contains any import. 641 * @param currentGroup 642 * current group. 643 * @return 644 * true, if current group contains at least one import. 645 */ 646 private boolean hasAnyImportInCurrentGroup(String currentGroup) { 647 boolean result = false; 648 for (ImportDetails currentImport : importToGroupList) { 649 if (currentGroup.equals(currentImport.getImportGroup())) { 650 result = true; 651 break; 652 } 653 } 654 return result; 655 } 656 657 /** 658 * Get import valid group. 659 * @param isStatic 660 * is static import. 661 * @param importPath 662 * full import path. 663 * @return import valid group. 664 */ 665 private String getImportGroup(boolean isStatic, String importPath) { 666 RuleMatchForImport bestMatch = new RuleMatchForImport(NON_GROUP_RULE_GROUP, 0, 0); 667 if (isStatic && customImportOrderRules.contains(STATIC_RULE_GROUP)) { 668 bestMatch.group = STATIC_RULE_GROUP; 669 bestMatch.matchLength = importPath.length(); 670 } 671 else if (customImportOrderRules.contains(SAME_PACKAGE_RULE_GROUP)) { 672 final String importPathTrimmedToSamePackageDepth = 673 getFirstDomainsFromIdent(samePackageMatchingDepth, importPath); 674 if (samePackageDomainsRegExp.equals(importPathTrimmedToSamePackageDepth)) { 675 bestMatch.group = SAME_PACKAGE_RULE_GROUP; 676 bestMatch.matchLength = importPath.length(); 677 } 678 } 679 if (bestMatch.group.equals(NON_GROUP_RULE_GROUP)) { 680 for (String group : customImportOrderRules) { 681 if (STANDARD_JAVA_PACKAGE_RULE_GROUP.equals(group)) { 682 bestMatch = findBetterPatternMatch(importPath, 683 STANDARD_JAVA_PACKAGE_RULE_GROUP, standardPackageRegExp, bestMatch); 684 } 685 if (SPECIAL_IMPORTS_RULE_GROUP.equals(group)) { 686 bestMatch = findBetterPatternMatch(importPath, 687 group, specialImportsRegExp, bestMatch); 688 } 689 } 690 } 691 if (bestMatch.group.equals(NON_GROUP_RULE_GROUP) 692 && customImportOrderRules.contains(THIRD_PARTY_PACKAGE_RULE_GROUP) 693 && thirdPartyPackageRegExp.matcher(importPath).find()) { 694 bestMatch.group = THIRD_PARTY_PACKAGE_RULE_GROUP; 695 } 696 return bestMatch.group; 697 } 698 699 /** Tries to find better matching regular expression: 700 * longer matching substring wins; in case of the same length, 701 * lower position of matching substring wins. 702 * @param importPath 703 * Full import identifier 704 * @param group 705 * Import group we are trying to assign the import 706 * @param regExp 707 * Regular expression for import group 708 * @param currentBestMatch 709 * object with currently best match 710 * @return better match (if found) or the same (currentBestMatch) 711 */ 712 private static RuleMatchForImport findBetterPatternMatch(String importPath, String group, 713 Pattern regExp, RuleMatchForImport currentBestMatch) { 714 RuleMatchForImport betterMatchCandidate = currentBestMatch; 715 final Matcher matcher = regExp.matcher(importPath); 716 while (matcher.find()) { 717 final int length = matcher.end() - matcher.start(); 718 if (length > betterMatchCandidate.matchLength 719 || length == betterMatchCandidate.matchLength 720 && matcher.start() < betterMatchCandidate.matchPosition) { 721 betterMatchCandidate = new RuleMatchForImport(group, length, matcher.start()); 722 } 723 } 724 return betterMatchCandidate; 725 } 726 727 /** 728 * Checks compare two import paths. 729 * @param import1 730 * current import. 731 * @param import2 732 * previous import. 733 * @return a negative integer, zero, or a positive integer as the 734 * specified String is greater than, equal to, or less 735 * than this String, ignoring case considerations. 736 */ 737 private static int compareImports(String import1, String import2) { 738 int result = 0; 739 final String separator = "\\."; 740 final String[] import1Tokens = import1.split(separator); 741 final String[] import2Tokens = import2.split(separator); 742 for (int i = 0; i != import1Tokens.length && i != import2Tokens.length; i++) { 743 final String import1Token = import1Tokens[i]; 744 final String import2Token = import2Tokens[i]; 745 result = import1Token.compareTo(import2Token); 746 if (result != 0) { 747 break; 748 } 749 } 750 if (result == 0) { 751 result = Integer.compare(import1Tokens.length, import2Tokens.length); 752 } 753 return result; 754 } 755 756 /** 757 * Counts empty lines before given. 758 * @param lineNo 759 * Line number of current import. 760 * @return count of empty lines before given. 761 */ 762 private int getCountOfEmptyLinesBefore(int lineNo) { 763 int result = 0; 764 final String[] lines = getLines(); 765 // [lineNo - 2] is the number of the previous line 766 // because the numbering starts from zero. 767 int lineBeforeIndex = lineNo - 2; 768 while (lineBeforeIndex >= 0 769 && CommonUtil.isBlank(lines[lineBeforeIndex])) { 770 lineBeforeIndex--; 771 result++; 772 } 773 return result; 774 } 775 776 /** 777 * Forms import full path. 778 * @param token 779 * current token. 780 * @return full path or null. 781 */ 782 private static String getFullImportIdent(DetailAST token) { 783 String ident = ""; 784 if (token != null) { 785 ident = FullIdent.createFullIdent(token.findFirstToken(TokenTypes.DOT)).getText(); 786 } 787 return ident; 788 } 789 790 /** 791 * Parses ordering rule and adds it to the list with rules. 792 * @param ruleStr 793 * String with rule. 794 */ 795 private void addRulesToList(String ruleStr) { 796 if (STATIC_RULE_GROUP.equals(ruleStr) 797 || THIRD_PARTY_PACKAGE_RULE_GROUP.equals(ruleStr) 798 || STANDARD_JAVA_PACKAGE_RULE_GROUP.equals(ruleStr) 799 || SPECIAL_IMPORTS_RULE_GROUP.equals(ruleStr)) { 800 customImportOrderRules.add(ruleStr); 801 } 802 else if (ruleStr.startsWith(SAME_PACKAGE_RULE_GROUP)) { 803 final String rule = ruleStr.substring(ruleStr.indexOf('(') + 1, 804 ruleStr.indexOf(')')); 805 samePackageMatchingDepth = Integer.parseInt(rule); 806 if (samePackageMatchingDepth <= 0) { 807 throw new IllegalArgumentException( 808 "SAME_PACKAGE rule parameter should be positive integer: " + ruleStr); 809 } 810 customImportOrderRules.add(SAME_PACKAGE_RULE_GROUP); 811 } 812 else { 813 throw new IllegalStateException("Unexpected rule: " + ruleStr); 814 } 815 } 816 817 /** 818 * Creates samePackageDomainsRegExp of the first package domains. 819 * @param firstPackageDomainsCount 820 * number of first package domains. 821 * @param packageNode 822 * package node. 823 * @return same package regexp. 824 */ 825 private static String createSamePackageRegexp(int firstPackageDomainsCount, 826 DetailAST packageNode) { 827 final String packageFullPath = getFullImportIdent(packageNode); 828 return getFirstDomainsFromIdent(firstPackageDomainsCount, packageFullPath); 829 } 830 831 /** 832 * Extracts defined amount of domains from the left side of package/import identifier. 833 * @param firstPackageDomainsCount 834 * number of first package domains. 835 * @param packageFullPath 836 * full identifier containing path to package or imported object. 837 * @return String with defined amount of domains or full identifier 838 * (if full identifier had less domain than specified) 839 */ 840 private static String getFirstDomainsFromIdent( 841 final int firstPackageDomainsCount, final String packageFullPath) { 842 final StringBuilder builder = new StringBuilder(256); 843 final StringTokenizer tokens = new StringTokenizer(packageFullPath, "."); 844 int count = firstPackageDomainsCount; 845 846 while (count > 0 && tokens.hasMoreTokens()) { 847 builder.append(tokens.nextToken()).append('.'); 848 count--; 849 } 850 return builder.toString(); 851 } 852 853 /** 854 * Contains import attributes as line number, import full path, import 855 * group. 856 */ 857 private static class ImportDetails { 858 859 /** Import full path. */ 860 private final String importFullPath; 861 862 /** Import line number. */ 863 private final int lineNumber; 864 865 /** Import group. */ 866 private final String importGroup; 867 868 /** Is static import. */ 869 private final boolean staticImport; 870 871 /** 872 * Initialise importFullPath, lineNumber, importGroup, staticImport. 873 * @param importFullPath 874 * import full path. 875 * @param lineNumber 876 * import line number. 877 * @param importGroup 878 * import group. 879 * @param staticImport 880 * if import is static. 881 */ 882 /* package */ ImportDetails(String importFullPath, 883 int lineNumber, String importGroup, boolean staticImport) { 884 this.importFullPath = importFullPath; 885 this.lineNumber = lineNumber; 886 this.importGroup = importGroup; 887 this.staticImport = staticImport; 888 } 889 890 /** 891 * Get import full path variable. 892 * @return import full path variable. 893 */ 894 public String getImportFullPath() { 895 return importFullPath; 896 } 897 898 /** 899 * Get import line number. 900 * @return import line. 901 */ 902 public int getLineNumber() { 903 return lineNumber; 904 } 905 906 /** 907 * Get import group. 908 * @return import group. 909 */ 910 public String getImportGroup() { 911 return importGroup; 912 } 913 914 /** 915 * Checks if import is static. 916 * @return true, if import is static. 917 */ 918 public boolean isStaticImport() { 919 return staticImport; 920 } 921 922 } 923 924 /** 925 * Contains matching attributes assisting in definition of "best matching" 926 * group for import. 927 */ 928 private static class RuleMatchForImport { 929 930 /** Position of matching string for current best match. */ 931 private final int matchPosition; 932 /** Length of matching string for current best match. */ 933 private int matchLength; 934 /** Import group for current best match. */ 935 private String group; 936 937 /** Constructor to initialize the fields. 938 * @param group 939 * Matched group. 940 * @param length 941 * Matching length. 942 * @param position 943 * Matching position. 944 */ 945 /* package */ RuleMatchForImport(String group, int length, int position) { 946 this.group = group; 947 matchLength = length; 948 matchPosition = position; 949 } 950 951 } 952 953 }