001//////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code for adherence to a set of rules. 003// Copyright (C) 2001-2016 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 THIRDPARTY_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 THIRDPARTY_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 * @throws org.apache.commons.beanutils.ConversionException 390 * if unable to create Pattern object. 391 */ 392 public final void setStandardPackageRegExp(String regexp) { 393 standardPackageRegExp = CommonUtils.createPattern(regexp); 394 } 395 396 /** 397 * Sets thirdPartyRegExp specified by user. 398 * @param regexp 399 * user value. 400 * @throws org.apache.commons.beanutils.ConversionException 401 * if unable to create Pattern object. 402 */ 403 public final void setThirdPartyPackageRegExp(String regexp) { 404 thirdPartyPackageRegExp = CommonUtils.createPattern(regexp); 405 } 406 407 /** 408 * Sets specialImportsRegExp specified by user. 409 * @param regexp 410 * user value. 411 * @throws org.apache.commons.beanutils.ConversionException 412 * if unable to create Pattern object. 413 */ 414 public final void setSpecialImportsRegExp(String regexp) { 415 specialImportsRegExp = CommonUtils.createPattern(regexp); 416 } 417 418 /** 419 * Sets separateLineBetweenGroups specified by user. 420 * @param value 421 * user value. 422 */ 423 public final void setSeparateLineBetweenGroups(boolean value) { 424 separateLineBetweenGroups = value; 425 } 426 427 /** 428 * Sets sortImportsInGroupAlphabetically specified by user. 429 * @param value 430 * user value. 431 */ 432 public final void setSortImportsInGroupAlphabetically(boolean value) { 433 sortImportsInGroupAlphabetically = value; 434 } 435 436 /** 437 * Sets a custom import order from the rules in the string format specified 438 * by user. 439 * @param inputCustomImportOrder 440 * user value. 441 */ 442 public final void setCustomImportOrderRules(final String inputCustomImportOrder) { 443 customImportOrderRules.clear(); 444 for (String currentState : GROUP_SEPARATOR_PATTERN.split(inputCustomImportOrder)) { 445 addRulesToList(currentState); 446 } 447 customImportOrderRules.add(NON_GROUP_RULE_GROUP); 448 } 449 450 @Override 451 public int[] getDefaultTokens() { 452 return getAcceptableTokens(); 453 } 454 455 @Override 456 public int[] getAcceptableTokens() { 457 return new int[] { 458 TokenTypes.IMPORT, 459 TokenTypes.STATIC_IMPORT, 460 TokenTypes.PACKAGE_DEF, 461 }; 462 } 463 464 @Override 465 public int[] getRequiredTokens() { 466 return getAcceptableTokens(); 467 } 468 469 @Override 470 public void beginTree(DetailAST rootAST) { 471 importToGroupList.clear(); 472 } 473 474 @Override 475 public void visitToken(DetailAST ast) { 476 if (ast.getType() == TokenTypes.PACKAGE_DEF) { 477 if (customImportOrderRules.contains(SAME_PACKAGE_RULE_GROUP)) { 478 samePackageDomainsRegExp = createSamePackageRegexp( 479 samePackageMatchingDepth, ast); 480 } 481 } 482 else { 483 final String importFullPath = getFullImportIdent(ast); 484 final int lineNo = ast.getLineNo(); 485 final boolean isStatic = ast.getType() == TokenTypes.STATIC_IMPORT; 486 importToGroupList.add(new ImportDetails(importFullPath, 487 lineNo, getImportGroup(isStatic, importFullPath), 488 isStatic)); 489 } 490 } 491 492 @Override 493 public void finishTree(DetailAST rootAST) { 494 495 if (!importToGroupList.isEmpty()) { 496 finishImportList(); 497 } 498 } 499 500 /** Examine the order of all the imports and log any violations. */ 501 private void finishImportList() { 502 final ImportDetails firstImport = importToGroupList.get(0); 503 String currentGroup = getImportGroup(firstImport.isStaticImport(), 504 firstImport.getImportFullPath()); 505 int currentGroupNumber = customImportOrderRules.indexOf(currentGroup); 506 String previousImportFromCurrentGroup = null; 507 508 for (ImportDetails importObject : importToGroupList) { 509 final String importGroup = importObject.getImportGroup(); 510 final String fullImportIdent = importObject.getImportFullPath(); 511 512 if (importGroup.equals(currentGroup)) { 513 if (sortImportsInGroupAlphabetically 514 && previousImportFromCurrentGroup != null 515 && compareImports(fullImportIdent, previousImportFromCurrentGroup) < 0) { 516 log(importObject.getLineNumber(), MSG_LEX, 517 fullImportIdent, previousImportFromCurrentGroup); 518 } 519 else { 520 previousImportFromCurrentGroup = fullImportIdent; 521 } 522 } 523 else { 524 //not the last group, last one is always NON_GROUP 525 if (customImportOrderRules.size() > currentGroupNumber + 1) { 526 final String nextGroup = getNextImportGroup(currentGroupNumber + 1); 527 if (importGroup.equals(nextGroup)) { 528 if (separateLineBetweenGroups 529 && !hasEmptyLineBefore(importObject.getLineNumber())) { 530 log(importObject.getLineNumber(), MSG_LINE_SEPARATOR, fullImportIdent); 531 } 532 currentGroup = nextGroup; 533 currentGroupNumber = customImportOrderRules.indexOf(nextGroup); 534 previousImportFromCurrentGroup = fullImportIdent; 535 } 536 else { 537 logWrongImportGroupOrder(importObject.getLineNumber(), 538 importGroup, nextGroup, fullImportIdent); 539 } 540 } 541 else { 542 logWrongImportGroupOrder(importObject.getLineNumber(), 543 importGroup, currentGroup, fullImportIdent); 544 } 545 } 546 } 547 } 548 549 /** 550 * Log wrong import group order. 551 * @param currentImportLine 552 * line number of current import current import. 553 * @param importGroup 554 * import group. 555 * @param currentGroupNumber 556 * current group number we are checking. 557 * @param fullImportIdent 558 * full import name. 559 */ 560 private void logWrongImportGroupOrder(int currentImportLine, String importGroup, 561 String currentGroupNumber, String fullImportIdent) { 562 if (NON_GROUP_RULE_GROUP.equals(importGroup)) { 563 log(currentImportLine, MSG_NONGROUP_IMPORT, fullImportIdent); 564 } 565 else if (NON_GROUP_RULE_GROUP.equals(currentGroupNumber)) { 566 log(currentImportLine, MSG_NONGROUP_EXPECTED, importGroup, fullImportIdent); 567 } 568 else { 569 log(currentImportLine, MSG_ORDER, importGroup, currentGroupNumber, fullImportIdent); 570 } 571 } 572 573 /** 574 * Get next import group. 575 * @param currentGroupNumber 576 * current group number. 577 * @return 578 * next import group. 579 */ 580 private String getNextImportGroup(int currentGroupNumber) { 581 int nextGroupNumber = currentGroupNumber; 582 583 while (customImportOrderRules.size() > nextGroupNumber + 1) { 584 if (hasAnyImportInCurrentGroup(customImportOrderRules.get(nextGroupNumber))) { 585 break; 586 } 587 nextGroupNumber++; 588 } 589 return customImportOrderRules.get(nextGroupNumber); 590 } 591 592 /** 593 * Checks if current group contains any import. 594 * @param currentGroup 595 * current group. 596 * @return 597 * true, if current group contains at least one import. 598 */ 599 private boolean hasAnyImportInCurrentGroup(String currentGroup) { 600 for (ImportDetails currentImport : importToGroupList) { 601 if (currentGroup.equals(currentImport.getImportGroup())) { 602 return true; 603 } 604 } 605 return false; 606 } 607 608 /** 609 * Get import valid group. 610 * @param isStatic 611 * is static import. 612 * @param importPath 613 * full import path. 614 * @return import valid group. 615 */ 616 private String getImportGroup(boolean isStatic, String importPath) { 617 RuleMatchForImport bestMatch = new RuleMatchForImport(NON_GROUP_RULE_GROUP, 0, 0); 618 if (isStatic && customImportOrderRules.contains(STATIC_RULE_GROUP)) { 619 bestMatch.group = STATIC_RULE_GROUP; 620 bestMatch.matchLength = importPath.length(); 621 } 622 else if (customImportOrderRules.contains(SAME_PACKAGE_RULE_GROUP)) { 623 final String importPathTrimmedToSamePackageDepth = 624 getFirstNDomainsFromIdent(samePackageMatchingDepth, importPath); 625 if (samePackageDomainsRegExp.equals(importPathTrimmedToSamePackageDepth)) { 626 bestMatch.group = SAME_PACKAGE_RULE_GROUP; 627 bestMatch.matchLength = importPath.length(); 628 } 629 } 630 if (bestMatch.group.equals(NON_GROUP_RULE_GROUP)) { 631 for (String group : customImportOrderRules) { 632 if (STANDARD_JAVA_PACKAGE_RULE_GROUP.equals(group)) { 633 bestMatch = findBetterPatternMatch(importPath, 634 STANDARD_JAVA_PACKAGE_RULE_GROUP, standardPackageRegExp, bestMatch); 635 } 636 if (SPECIAL_IMPORTS_RULE_GROUP.equals(group)) { 637 bestMatch = findBetterPatternMatch(importPath, 638 SPECIAL_IMPORTS_RULE_GROUP, specialImportsRegExp, bestMatch); 639 } 640 } 641 } 642 if (bestMatch.group.equals(NON_GROUP_RULE_GROUP) 643 && customImportOrderRules.contains(THIRD_PARTY_PACKAGE_RULE_GROUP) 644 && thirdPartyPackageRegExp.matcher(importPath).find()) { 645 bestMatch.group = THIRD_PARTY_PACKAGE_RULE_GROUP; 646 } 647 return bestMatch.group; 648 } 649 650 /** Tries to find better matching regular expression: 651 * longer matching substring wins; in case of the same length, 652 * lower position of matching substring wins. 653 * @param importPath 654 * Full import identifier 655 * @param group 656 * Import group we are trying to assign the import 657 * @param regExp 658 * Regular expression for import group 659 * @param currentBestMatch 660 * object with currently best match 661 * @return better match (if found) or the same (currentBestMatch) 662 */ 663 private static RuleMatchForImport findBetterPatternMatch(String importPath, String group, 664 Pattern regExp, RuleMatchForImport currentBestMatch) { 665 RuleMatchForImport betterMatchCandidate = currentBestMatch; 666 final Matcher matcher = regExp.matcher(importPath); 667 while (matcher.find()) { 668 final int length = matcher.end() - matcher.start(); 669 if (length > betterMatchCandidate.matchLength 670 || length == betterMatchCandidate.matchLength 671 && matcher.start() < betterMatchCandidate.matchPosition) { 672 betterMatchCandidate = new RuleMatchForImport(group, length, matcher.start()); 673 } 674 } 675 return betterMatchCandidate; 676 } 677 678 /** 679 * Checks compare two import paths. 680 * @param import1 681 * current import. 682 * @param import2 683 * previous import. 684 * @return a negative integer, zero, or a positive integer as the 685 * specified String is greater than, equal to, or less 686 * than this String, ignoring case considerations. 687 */ 688 private static int compareImports(String import1, String import2) { 689 int result = 0; 690 final String separator = "\\."; 691 final String[] import1Tokens = import1.split(separator); 692 final String[] import2Tokens = import2.split(separator); 693 for (int i = 0; i < import1Tokens.length && i != import2Tokens.length; i++) { 694 final String import1Token = import1Tokens[i]; 695 final String import2Token = import2Tokens[i]; 696 result = import1Token.compareTo(import2Token); 697 if (result != 0) { 698 break; 699 } 700 } 701 return result; 702 } 703 704 /** 705 * Checks if a token has a empty line before. 706 * @param lineNo 707 * Line number of current import. 708 * @return true, if token have empty line before. 709 */ 710 private boolean hasEmptyLineBefore(int lineNo) { 711 // [lineNo - 2] is the number of the previous line 712 // because the numbering starts from zero. 713 final String lineBefore = getLine(lineNo - 2); 714 return lineBefore.trim().isEmpty(); 715 } 716 717 /** 718 * Forms import full path. 719 * @param token 720 * current token. 721 * @return full path or null. 722 */ 723 private static String getFullImportIdent(DetailAST token) { 724 if (token == null) { 725 return ""; 726 } 727 else { 728 return FullIdent.createFullIdent(token.findFirstToken(TokenTypes.DOT)).getText(); 729 } 730 } 731 732 /** 733 * Parses ordering rule and adds it to the list with rules. 734 * @param ruleStr 735 * String with rule. 736 */ 737 private void addRulesToList(String ruleStr) { 738 if (STATIC_RULE_GROUP.equals(ruleStr) 739 || THIRD_PARTY_PACKAGE_RULE_GROUP.equals(ruleStr) 740 || STANDARD_JAVA_PACKAGE_RULE_GROUP.equals(ruleStr) 741 || SPECIAL_IMPORTS_RULE_GROUP.equals(ruleStr)) { 742 customImportOrderRules.add(ruleStr); 743 744 } 745 else if (ruleStr.startsWith(SAME_PACKAGE_RULE_GROUP)) { 746 747 final String rule = ruleStr.substring(ruleStr.indexOf('(') + 1, 748 ruleStr.indexOf(')')); 749 samePackageMatchingDepth = Integer.parseInt(rule); 750 if (samePackageMatchingDepth <= 0) { 751 throw new IllegalArgumentException( 752 "SAME_PACKAGE rule parameter should be positive integer: " + ruleStr); 753 } 754 customImportOrderRules.add(SAME_PACKAGE_RULE_GROUP); 755 756 } 757 else { 758 throw new IllegalStateException("Unexpected rule: " + ruleStr); 759 } 760 } 761 762 /** 763 * Creates samePackageDomainsRegExp of the first package domains. 764 * @param firstPackageDomainsCount 765 * number of first package domains. 766 * @param packageNode 767 * package node. 768 * @return same package regexp. 769 */ 770 private static String createSamePackageRegexp(int firstPackageDomainsCount, 771 DetailAST packageNode) { 772 final String packageFullPath = getFullImportIdent(packageNode); 773 return getFirstNDomainsFromIdent(firstPackageDomainsCount, packageFullPath); 774 } 775 776 /** 777 * Extracts defined amount of domains from the left side of package/import identifier 778 * @param firstPackageDomainsCount 779 * number of first package domains. 780 * @param packageFullPath 781 * full identifier containing path to package or imported object. 782 * @return String with defined amount of domains or full identifier 783 * (if full identifier had less domain then specified) 784 */ 785 private static String getFirstNDomainsFromIdent( 786 final int firstPackageDomainsCount, final String packageFullPath) { 787 final StringBuilder builder = new StringBuilder(); 788 final StringTokenizer tokens = new StringTokenizer(packageFullPath, "."); 789 int count = firstPackageDomainsCount; 790 791 while (count > 0 && tokens.hasMoreTokens()) { 792 builder.append(tokens.nextToken()).append('.'); 793 count--; 794 } 795 return builder.toString(); 796 } 797 798 /** 799 * Contains import attributes as line number, import full path, import 800 * group. 801 * @author max 802 */ 803 private static class ImportDetails { 804 /** Import full path. */ 805 private final String importFullPath; 806 807 /** Import line number. */ 808 private final int lineNumber; 809 810 /** Import group. */ 811 private final String importGroup; 812 813 /** Is static import. */ 814 private final boolean staticImport; 815 816 /** 817 * @param importFullPath 818 * import full path. 819 * @param lineNumber 820 * import line number. 821 * @param importGroup 822 * import group. 823 * @param staticImport 824 * if import is static. 825 */ 826 ImportDetails(String importFullPath, 827 int lineNumber, String importGroup, boolean staticImport) { 828 this.importFullPath = importFullPath; 829 this.lineNumber = lineNumber; 830 this.importGroup = importGroup; 831 this.staticImport = staticImport; 832 } 833 834 /** 835 * Get import full path variable. 836 * @return import full path variable. 837 */ 838 public String getImportFullPath() { 839 return importFullPath; 840 } 841 842 /** 843 * Get import line number. 844 * @return import line. 845 */ 846 public int getLineNumber() { 847 return lineNumber; 848 } 849 850 /** 851 * Get import group. 852 * @return import group. 853 */ 854 public String getImportGroup() { 855 return importGroup; 856 } 857 858 /** 859 * Checks if import is static. 860 * @return true, if import is static. 861 */ 862 public boolean isStaticImport() { 863 return staticImport; 864 } 865 } 866 867 /** 868 * Contains matching attributes assisting in definition of "best matching" 869 * group for import. 870 * @author ivanov-alex 871 */ 872 private static class RuleMatchForImport { 873 /** Position of matching string for current best match. */ 874 private final int matchPosition; 875 /** Length of matching string for current best match. */ 876 private int matchLength; 877 /** Import group for current best match. */ 878 private String group; 879 880 /** Constructor to initialize the fields. 881 * @param group 882 * Matched group. 883 * @param length 884 * Matching length. 885 * @param position 886 * Matching position. 887 */ 888 RuleMatchForImport(String group, int length, int position) { 889 this.group = group; 890 matchLength = length; 891 matchPosition = position; 892 } 893 } 894}