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