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