001//////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code for adherence to a set of rules. 003// Copyright (C) 2001-2015 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.Check; 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 Check { 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 /** RegExp for SAME_PACKAGE group imports. */ 359 private String samePackageDomainsRegExp = ""; 360 361 /** RegExp for STANDARD_JAVA_PACKAGE group imports. */ 362 private Pattern standardPackageRegExp = Pattern.compile("^(java|javax)\\."); 363 364 /** RegExp for THIRDPARTY_PACKAGE group imports. */ 365 private Pattern thirdPartyPackageRegExp = Pattern.compile(".*"); 366 367 /** RegExp for SPECIAL_IMPORTS group imports. */ 368 private Pattern specialImportsRegExp = Pattern.compile("^$"); 369 370 /** Force empty line separator between import groups. */ 371 private boolean separateLineBetweenGroups = true; 372 373 /** Force grouping alphabetically, in ASCII order. */ 374 private boolean sortImportsInGroupAlphabetically; 375 376 /** List of order declaration customizing by user. */ 377 private final List<String> customImportOrderRules = new ArrayList<>(); 378 379 /** Number of first domains for SAME_PACKAGE group. */ 380 private int samePackageMatchingDepth = 2; 381 382 /** Contains objects with import attributes. */ 383 private final List<ImportDetails> importToGroupList = new ArrayList<>(); 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 return; 497 } 498 499 final ImportDetails firstImport = importToGroupList.get(0); 500 String currentGroup = getImportGroup(firstImport.isStaticImport(), 501 firstImport.getImportFullPath()); 502 int currentGroupNumber = customImportOrderRules.indexOf(currentGroup); 503 String previousImportFromCurrentGroup = null; 504 505 for (ImportDetails importObject : importToGroupList) { 506 final String importGroup = importObject.getImportGroup(); 507 final String fullImportIdent = importObject.getImportFullPath(); 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 && !hasEmptyLineBefore(importObject.getLineNumber())) { 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 for (ImportDetails currentImport : importToGroupList) { 598 if (currentGroup.equals(currentImport.getImportGroup())) { 599 return true; 600 } 601 } 602 return false; 603 } 604 605 /** 606 * Get import valid group. 607 * @param isStatic 608 * is static import. 609 * @param importPath 610 * full import path. 611 * @return import valid group. 612 */ 613 private String getImportGroup(boolean isStatic, String importPath) { 614 RuleMatchForImport bestMatch = new RuleMatchForImport(NON_GROUP_RULE_GROUP, 0, 0); 615 if (isStatic && customImportOrderRules.contains(STATIC_RULE_GROUP)) { 616 bestMatch.group = STATIC_RULE_GROUP; 617 bestMatch.matchLength = importPath.length(); 618 } 619 else if (customImportOrderRules.contains(SAME_PACKAGE_RULE_GROUP)) { 620 final String importPathTrimmedToSamePackageDepth = 621 getFirstNDomainsFromIdent(samePackageMatchingDepth, importPath); 622 if (samePackageDomainsRegExp.equals(importPathTrimmedToSamePackageDepth)) { 623 bestMatch.group = SAME_PACKAGE_RULE_GROUP; 624 bestMatch.matchLength = importPath.length(); 625 } 626 } 627 if (bestMatch.group.equals(NON_GROUP_RULE_GROUP)) { 628 for (String group : customImportOrderRules) { 629 if (STANDARD_JAVA_PACKAGE_RULE_GROUP.equals(group)) { 630 bestMatch = findBetterPatternMatch(importPath, 631 STANDARD_JAVA_PACKAGE_RULE_GROUP, standardPackageRegExp, bestMatch); 632 } 633 if (SPECIAL_IMPORTS_RULE_GROUP.equals(group)) { 634 bestMatch = findBetterPatternMatch(importPath, 635 SPECIAL_IMPORTS_RULE_GROUP, specialImportsRegExp, bestMatch); 636 } 637 } 638 } 639 if (bestMatch.group.equals(NON_GROUP_RULE_GROUP) 640 && customImportOrderRules.contains(THIRD_PARTY_PACKAGE_RULE_GROUP) 641 && thirdPartyPackageRegExp.matcher(importPath).find()) { 642 bestMatch.group = THIRD_PARTY_PACKAGE_RULE_GROUP; 643 } 644 return bestMatch.group; 645 } 646 647 /** Tries to find better matching regular expression: 648 * longer matching substring wins; in case of the same length, 649 * lower position of matching substring wins. 650 * @param importPath 651 * Full import identifier 652 * @param group 653 * Import group we are trying to assign the import 654 * @param regExp 655 * Regular expression for import group 656 * @param currentBestMatch 657 * object with currently best match 658 * @return better match (if found) or the same (currentBestMatch) 659 */ 660 private static RuleMatchForImport findBetterPatternMatch(String importPath, String group, 661 Pattern regExp, RuleMatchForImport currentBestMatch) { 662 RuleMatchForImport betterMatchCandidate = currentBestMatch; 663 final Matcher matcher = regExp.matcher(importPath); 664 while (matcher.find()) { 665 final int length = matcher.end() - matcher.start(); 666 if (length > betterMatchCandidate.matchLength 667 || length == betterMatchCandidate.matchLength 668 && matcher.start() < betterMatchCandidate.matchPosition) { 669 betterMatchCandidate = new RuleMatchForImport(group, length, matcher.start()); 670 } 671 } 672 return betterMatchCandidate; 673 } 674 675 /** 676 * Checks compare two import paths. 677 * @param import1 678 * current import. 679 * @param import2 680 * previous import. 681 * @return a negative integer, zero, or a positive integer as the 682 * specified String is greater than, equal to, or less 683 * than this String, ignoring case considerations. 684 */ 685 private static int compareImports(String import1, String import2) { 686 int result = 0; 687 final String separator = "\\."; 688 final String[] import1Tokens = import1.split(separator); 689 final String[] import2Tokens = import2.split(separator); 690 for (int i = 0; i < import1Tokens.length && i != import2Tokens.length; i++) { 691 final String import1Token = import1Tokens[i]; 692 final String import2Token = import2Tokens[i]; 693 result = import1Token.compareTo(import2Token); 694 if (result != 0) { 695 break; 696 } 697 } 698 return result; 699 } 700 701 /** 702 * Checks if a token has a empty line before. 703 * @param lineNo 704 * Line number of current import. 705 * @return true, if token have empty line before. 706 */ 707 private boolean hasEmptyLineBefore(int lineNo) { 708 // [lineNo - 2] is the number of the previous line 709 // because the numbering starts from zero. 710 final String lineBefore = getLine(lineNo - 2); 711 return lineBefore.trim().isEmpty(); 712 } 713 714 /** 715 * Forms import full path. 716 * @param token 717 * current token. 718 * @return full path or null. 719 */ 720 private static String getFullImportIdent(DetailAST token) { 721 if (token == null) { 722 return ""; 723 } 724 else { 725 return FullIdent.createFullIdent(token.findFirstToken(TokenTypes.DOT)).getText(); 726 } 727 } 728 729 /** 730 * Parses ordering rule and adds it to the list with rules. 731 * @param ruleStr 732 * String with rule. 733 */ 734 private void addRulesToList(String ruleStr) { 735 if (STATIC_RULE_GROUP.equals(ruleStr) 736 || THIRD_PARTY_PACKAGE_RULE_GROUP.equals(ruleStr) 737 || STANDARD_JAVA_PACKAGE_RULE_GROUP.equals(ruleStr) 738 || SPECIAL_IMPORTS_RULE_GROUP.equals(ruleStr)) { 739 customImportOrderRules.add(ruleStr); 740 741 } 742 else if (ruleStr.startsWith(SAME_PACKAGE_RULE_GROUP)) { 743 744 final String rule = ruleStr.substring(ruleStr.indexOf('(') + 1, 745 ruleStr.indexOf(')')); 746 samePackageMatchingDepth = Integer.parseInt(rule); 747 if (samePackageMatchingDepth <= 0) { 748 throw new IllegalArgumentException( 749 "SAME_PACKAGE rule parameter should be positive integer: " + ruleStr); 750 } 751 customImportOrderRules.add(SAME_PACKAGE_RULE_GROUP); 752 753 } 754 else { 755 throw new IllegalStateException("Unexpected rule: " + ruleStr); 756 } 757 } 758 759 /** 760 * Creates samePackageDomainsRegExp of the first package domains. 761 * @param firstPackageDomainsCount 762 * number of first package domains. 763 * @param packageNode 764 * package node. 765 * @return same package regexp. 766 */ 767 private static String createSamePackageRegexp(int firstPackageDomainsCount, 768 DetailAST packageNode) { 769 final String packageFullPath = getFullImportIdent(packageNode); 770 return getFirstNDomainsFromIdent(firstPackageDomainsCount, packageFullPath); 771 } 772 773 /** 774 * Extracts defined amount of domains from the left side of package/import identifier 775 * @param firstPackageDomainsCount 776 * number of first package domains. 777 * @param packageFullPath 778 * full identifier containing path to package or imported object. 779 * @return String with defined amount of domains or full identifier 780 * (if full identifier had less domain then specified) 781 */ 782 private static String getFirstNDomainsFromIdent( 783 final int firstPackageDomainsCount, final String packageFullPath) { 784 final StringBuilder builder = new StringBuilder(); 785 final StringTokenizer tokens = new StringTokenizer(packageFullPath, "."); 786 int count = firstPackageDomainsCount; 787 788 while (tokens.hasMoreTokens() && count > 0) { 789 builder.append(tokens.nextToken()).append('.'); 790 count--; 791 } 792 return builder.toString(); 793 } 794 795 /** 796 * Contains import attributes as line number, import full path, import 797 * group. 798 * @author max 799 */ 800 private static class ImportDetails { 801 /** Import full path. */ 802 private final String importFullPath; 803 804 /** Import line number. */ 805 private final int lineNumber; 806 807 /** Import group. */ 808 private final String importGroup; 809 810 /** Is static import. */ 811 private final boolean staticImport; 812 813 /** 814 * @param importFullPath 815 * import full path. 816 * @param lineNumber 817 * import line number. 818 * @param importGroup 819 * import group. 820 * @param staticImport 821 * if import is static. 822 */ 823 ImportDetails(String importFullPath, 824 int lineNumber, String importGroup, boolean staticImport) { 825 this.importFullPath = importFullPath; 826 this.lineNumber = lineNumber; 827 this.importGroup = importGroup; 828 this.staticImport = staticImport; 829 } 830 831 /** 832 * Get import full path variable. 833 * @return import full path variable. 834 */ 835 public String getImportFullPath() { 836 return importFullPath; 837 } 838 839 /** 840 * Get import line number. 841 * @return import line. 842 */ 843 public int getLineNumber() { 844 return lineNumber; 845 } 846 847 /** 848 * Get import group. 849 * @return import group. 850 */ 851 public String getImportGroup() { 852 return importGroup; 853 } 854 855 /** 856 * Checks if import is static. 857 * @return true, if import is static. 858 */ 859 public boolean isStaticImport() { 860 return staticImport; 861 } 862 } 863 864 /** 865 * Contains matching attributes assisting in definition of "best matching" 866 * group for import. 867 * @author ivanov-alex 868 */ 869 private static class RuleMatchForImport { 870 /** Import group for current best match. */ 871 private String group; 872 /** Length of matching string for current best match. */ 873 private int matchLength; 874 /** Position of matching string for current best match. */ 875 private final int matchPosition; 876 877 /** Constructor to initialize the fields. 878 * @param group 879 * Matched group. 880 * @param length 881 * Matching length. 882 * @param position 883 * Matching position. 884 */ 885 RuleMatchForImport(String group, int length, int position) { 886 this.group = group; 887 matchLength = length; 888 matchPosition = position; 889 } 890 } 891}