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