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>samePackageMatchingDepth</td><td>Number of first domains for SAME_PACKAGE group. 166 * </td><td>Integer</td><td>2</td></tr> 167 * <tr><td>separateLineBetweenGroups</td><td>Force empty line separator between import groups. 168 * </td><td>boolean</td><td>true</td></tr> 169 * <tr><td>sortImportsInGroupAlphabetically</td><td>Force grouping alphabetically, 170 * in ASCII sort order.</td><td>boolean</td><td>false</td></tr> 171 * </table> 172 * 173 * <p> 174 * For example: 175 * </p> 176 * <p>To configure the check so that it matches default Eclipse formatter configuration 177 * (tested on Kepler, Luna and Mars):</p> 178 * <ul> 179 * <li>group of static imports is on the top</li> 180 * <li>groups of non-static imports: "java" and "javax" packages 181 * first, then "org" and then all other imports</li> 182 * <li>imports will be sorted in the groups</li> 183 * <li>groups are separated by, at least, one blank line</li> 184 * </ul> 185 * <pre> 186 * <module name="CustomImportOrder"> 187 * <property name="customImportOrderRules" 188 * value="STATIC###STANDARD_JAVA_PACKAGE###SPECIAL_IMPORTS"/> 189 * <property name="specialImportsRegExp" value="org"/> 190 * <property name="sortImportsInGroupAlphabetically" value="true"/> 191 * <property name="separateLineBetweenGroups" value="true"/> 192 * </module> 193 * </pre> 194 * 195 * <p>To configure the check so that it matches default IntelliJ IDEA formatter 196 * configuration (tested on v14):</p> 197 * <ul> 198 * <li>group of static imports is on the bottom</li> 199 * <li>groups of non-static imports: all imports except of "javax" 200 * and "java", then "javax" and "java"</li> 201 * <li>imports will be sorted in the groups</li> 202 * <li>groups are separated by, at least, one blank line</li> 203 * </ul> 204 * 205 * <p> 206 * Note: "separated" option is disabled because IDEA default has blank line 207 * between "java" and static imports, and no blank line between 208 * "javax" and "java" 209 * </p> 210 * 211 * <pre> 212 * <module name="CustomImportOrder"> 213 * <property name="customImportOrderRules" 214 * value="THIRD_PARTY_PACKAGE###SPECIAL_IMPORTS###STANDARD_JAVA_PACKAGE 215 * ###STATIC"/> 216 * <property name="specialImportsRegExp" value="^javax\."/> 217 * <property name="standardPackageRegExp" value="^java\."/> 218 * <property name="sortImportsInGroupAlphabetically" value="true"/> 219 * <property name="separateLineBetweenGroups" value="false"/> 220 *</module> 221 * </pre> 222 * 223 * <p>To configure the check so that it matches default NetBeans formatter 224 * configuration (tested on v8):</p> 225 * <ul> 226 * <li>groups of non-static imports are not defined, all imports will be sorted as a one 227 * group</li> 228 * <li>static imports are not separated, they will be sorted along with other imports</li> 229 * </ul> 230 * 231 * <pre> 232 *<module name="CustomImportOrder"/> 233 * </pre> 234 * <p>To set RegExps for THIRD_PARTY_PACKAGE and STANDARD_JAVA_PACKAGE groups use 235 * thirdPartyPackageRegExp and standardPackageRegExp options.</p> 236 * <pre> 237 * <module name="CustomImportOrder"> 238 * <property name="customImportOrderRules" 239 * value="STATIC###SAME_PACKAGE(3)###THIRD_PARTY_PACKAGE###STANDARD_JAVA_PACKAGE"/> 240 * <property name="thirdPartyPackageRegExp" value="com|org"/> 241 * <property name="standardPackageRegExp" value="^(java|javax)\."/> 242 * </module> 243 * </pre> 244 * <p> 245 * Also, this check can be configured to force empty line separator between 246 * import groups. For example 247 * </p> 248 * 249 * <pre> 250 * <module name="CustomImportOrder"> 251 * <property name="separateLineBetweenGroups" value="true"/> 252 * </module> 253 * </pre> 254 * <p> 255 * It is possible to enforce 256 * <a href="https://en.wikipedia.org/wiki/ASCII#Order">ASCII sort order</a> 257 * of imports in groups using the following configuration: 258 * </p> 259 * <pre> 260 * <module name="CustomImportOrder"> 261 * <property name="sortImportsInGroupAlphabetically" value="true"/> 262 * </module> 263 * </pre> 264 * <p> 265 * Example of ASCII order: 266 * </p> 267 * <pre> 268 * {@code 269 *import java.awt.Dialog; 270 *import java.awt.Window; 271 *import java.awt.color.ColorSpace; 272 *import java.awt.Frame; // violation here - in ASCII order 'F' should go before 'c', 273 * // as all uppercase come before lowercase letters} 274 * </pre> 275 * <p> 276 * To force checking imports sequence such as: 277 * </p> 278 * 279 * <pre> 280 * {@code 281 * package com.puppycrawl.tools.checkstyle.imports; 282 * 283 * import com.google.common.annotations.GwtCompatible; 284 * import com.google.common.annotations.Beta; 285 * import com.google.common.annotations.VisibleForTesting; 286 * 287 * import org.abego.treelayout.Configuration; 288 * 289 * import static sun.tools.util.ModifierFilter.ALL_ACCESS; 290 * 291 * import com.google.common.annotations.GwtCompatible; // violation here - should be in the 292 * // THIRD_PARTY_PACKAGE group 293 * import android.*;} 294 * </pre> 295 * configure as follows: 296 * <pre> 297 * <module name="CustomImportOrder"> 298 * <property name="customImportOrderRules" 299 * value="SAME_PACKAGE(3)###THIRD_PARTY_PACKAGE###STATIC###SPECIAL_IMPORTS"/> 300 * <property name="specialImportsRegExp" value="android.*"/> 301 * </module> 302 * </pre> 303 * 304 * @author maxvetrenko 305 * @author <a href="mailto:nesterenko-aleksey@list.ru">Aleksey Nesterenko</a> 306 */ 307public class CustomImportOrderCheck extends Check { 308 309 /** 310 * A key is pointing to the warning message text in "messages.properties" 311 * file. 312 */ 313 public static final String MSG_LINE_SEPARATOR = "custom.import.order.line.separator"; 314 315 /** 316 * A key is pointing to the warning message text in "messages.properties" 317 * file. 318 */ 319 public static final String MSG_LEX = "custom.import.order.lex"; 320 321 /** 322 * A key is pointing to the warning message text in "messages.properties" 323 * file. 324 */ 325 public static final String MSG_NONGROUP_IMPORT = "custom.import.order.nonGroup.import"; 326 327 /** 328 * A key is pointing to the warning message text in "messages.properties" 329 * file. 330 */ 331 public static final String MSG_NONGROUP_EXPECTED = "custom.import.order.nonGroup.expected"; 332 333 /** 334 * A key is pointing to the warning message text in "messages.properties" 335 * file. 336 */ 337 public static final String MSG_ORDER = "custom.import.order"; 338 339 /** STATIC group name. */ 340 public static final String STATIC_RULE_GROUP = "STATIC"; 341 342 /** SAME_PACKAGE group name. */ 343 public static final String SAME_PACKAGE_RULE_GROUP = "SAME_PACKAGE"; 344 345 /** THIRD_PARTY_PACKAGE group name. */ 346 public static final String THIRD_PARTY_PACKAGE_RULE_GROUP = "THIRD_PARTY_PACKAGE"; 347 348 /** STANDARD_JAVA_PACKAGE group name. */ 349 public static final String STANDARD_JAVA_PACKAGE_RULE_GROUP = "STANDARD_JAVA_PACKAGE"; 350 351 /** SPECIAL_IMPORTS group name. */ 352 public static final String SPECIAL_IMPORTS_RULE_GROUP = "SPECIAL_IMPORTS"; 353 354 /** NON_GROUP group name. */ 355 private static final String NON_GROUP_RULE_GROUP = "NOT_ASSIGNED_TO_ANY_GROUP"; 356 357 /** Pattern used to separate groups of imports. */ 358 private static final Pattern GROUP_SEPARATOR_PATTERN = Pattern.compile("\\s*###\\s*"); 359 360 /** RegExp for SAME_PACKAGE group imports. */ 361 private String samePackageDomainsRegExp = ""; 362 363 /** RegExp for STANDARD_JAVA_PACKAGE group imports. */ 364 private Pattern standardPackageRegExp = Pattern.compile("^(java|javax)\\."); 365 366 /** RegExp for THIRDPARTY_PACKAGE group imports. */ 367 private Pattern thirdPartyPackageRegExp = Pattern.compile(".*"); 368 369 /** RegExp for SPECIAL_IMPORTS group imports. */ 370 private Pattern specialImportsRegExp = Pattern.compile("^$"); 371 372 /** Force empty line separator between import groups. */ 373 private boolean separateLineBetweenGroups = true; 374 375 /** Force grouping alphabetically, in ASCII order. */ 376 private boolean sortImportsInGroupAlphabetically; 377 378 /** List of order declaration customizing by user. */ 379 private final List<String> customImportOrderRules = new ArrayList<>(); 380 381 /** Number of first domains for SAME_PACKAGE group. */ 382 private int samePackageMatchingDepth = 2; 383 384 /** Contains objects with import attributes. */ 385 private final List<ImportDetails> importToGroupList = new ArrayList<>(); 386 387 /** 388 * Sets standardRegExp specified by user. 389 * @param regexp 390 * user value. 391 * @throws org.apache.commons.beanutils.ConversionException 392 * if unable to create Pattern object. 393 */ 394 public final void setStandardPackageRegExp(String regexp) { 395 standardPackageRegExp = CommonUtils.createPattern(regexp); 396 } 397 398 /** 399 * Sets thirdPartyRegExp specified by user. 400 * @param regexp 401 * user value. 402 * @throws org.apache.commons.beanutils.ConversionException 403 * if unable to create Pattern object. 404 */ 405 public final void setThirdPartyPackageRegExp(String regexp) { 406 thirdPartyPackageRegExp = CommonUtils.createPattern(regexp); 407 } 408 409 /** 410 * Sets specialImportsRegExp specified by user. 411 * @param regexp 412 * user value. 413 * @throws org.apache.commons.beanutils.ConversionException 414 * if unable to create Pattern object. 415 */ 416 public final void setSpecialImportsRegExp(String regexp) { 417 specialImportsRegExp = CommonUtils.createPattern(regexp); 418 } 419 420 /** 421 * Sets separateLineBetweenGroups specified by user. 422 * @param value 423 * user value. 424 */ 425 public final void setSeparateLineBetweenGroups(boolean value) { 426 separateLineBetweenGroups = value; 427 } 428 429 /** 430 * Sets sortImportsInGroupAlphabetically specified by user. 431 * @param value 432 * user value. 433 */ 434 public final void setSortImportsInGroupAlphabetically(boolean value) { 435 sortImportsInGroupAlphabetically = value; 436 } 437 438 /** 439 * Sets a custom import order from the rules in the string format specified 440 * by user. 441 * @param inputCustomImportOrder 442 * user value. 443 */ 444 public final void setCustomImportOrderRules(final String inputCustomImportOrder) { 445 customImportOrderRules.clear(); 446 for (String currentState : GROUP_SEPARATOR_PATTERN.split(inputCustomImportOrder)) { 447 addRulesToList(currentState); 448 } 449 customImportOrderRules.add(NON_GROUP_RULE_GROUP); 450 } 451 452 @Override 453 public int[] getDefaultTokens() { 454 return getAcceptableTokens(); 455 } 456 457 @Override 458 public int[] getAcceptableTokens() { 459 return new int[] { 460 TokenTypes.IMPORT, 461 TokenTypes.STATIC_IMPORT, 462 TokenTypes.PACKAGE_DEF, 463 }; 464 } 465 466 @Override 467 public int[] getRequiredTokens() { 468 return getAcceptableTokens(); 469 } 470 471 @Override 472 public void beginTree(DetailAST rootAST) { 473 importToGroupList.clear(); 474 } 475 476 @Override 477 public void visitToken(DetailAST ast) { 478 if (ast.getType() == TokenTypes.PACKAGE_DEF) { 479 if (customImportOrderRules.contains(SAME_PACKAGE_RULE_GROUP)) { 480 samePackageDomainsRegExp = createSamePackageRegexp( 481 samePackageMatchingDepth, ast); 482 } 483 } 484 else { 485 final String importFullPath = getFullImportIdent(ast); 486 final int lineNo = ast.getLineNo(); 487 final boolean isStatic = ast.getType() == TokenTypes.STATIC_IMPORT; 488 importToGroupList.add(new ImportDetails(importFullPath, 489 lineNo, getImportGroup(isStatic, importFullPath), 490 isStatic)); 491 } 492 } 493 494 @Override 495 public void finishTree(DetailAST rootAST) { 496 497 if (importToGroupList.isEmpty()) { 498 return; 499 } 500 501 final ImportDetails firstImport = importToGroupList.get(0); 502 String currentGroup = getImportGroup(firstImport.isStaticImport(), 503 firstImport.getImportFullPath()); 504 int currentGroupNumber = customImportOrderRules.indexOf(currentGroup); 505 String previousImportFromCurrentGroup = null; 506 507 for (ImportDetails importObject : importToGroupList) { 508 final String importGroup = importObject.getImportGroup(); 509 final String fullImportIdent = importObject.getImportFullPath(); 510 511 if (importGroup.equals(currentGroup)) { 512 if (sortImportsInGroupAlphabetically 513 && previousImportFromCurrentGroup != null 514 && compareImports(fullImportIdent, previousImportFromCurrentGroup) < 0) { 515 log(importObject.getLineNumber(), MSG_LEX, 516 fullImportIdent, previousImportFromCurrentGroup); 517 } 518 else { 519 previousImportFromCurrentGroup = fullImportIdent; 520 } 521 } 522 else { 523 //not the last group, last one is always NON_GROUP 524 if (customImportOrderRules.size() > currentGroupNumber + 1) { 525 final String nextGroup = getNextImportGroup(currentGroupNumber + 1); 526 if (importGroup.equals(nextGroup)) { 527 if (separateLineBetweenGroups 528 && !hasEmptyLineBefore(importObject.getLineNumber())) { 529 log(importObject.getLineNumber(), MSG_LINE_SEPARATOR, fullImportIdent); 530 } 531 currentGroup = nextGroup; 532 currentGroupNumber = customImportOrderRules.indexOf(nextGroup); 533 previousImportFromCurrentGroup = fullImportIdent; 534 } 535 else { 536 logWrongImportGroupOrder(importObject.getLineNumber(), 537 importGroup, nextGroup, fullImportIdent); 538 } 539 } 540 else { 541 logWrongImportGroupOrder(importObject.getLineNumber(), 542 importGroup, currentGroup, fullImportIdent); 543 } 544 } 545 } 546 } 547 548 /** 549 * Log wrong import group order. 550 * @param currentImportLine 551 * line number of current import current import. 552 * @param importGroup 553 * import group. 554 * @param currentGroupNumber 555 * current group number we are checking. 556 * @param fullImportIdent 557 * full import name. 558 */ 559 private void logWrongImportGroupOrder(int currentImportLine, String importGroup, 560 String currentGroupNumber, String fullImportIdent) { 561 if (NON_GROUP_RULE_GROUP.equals(importGroup)) { 562 log(currentImportLine, MSG_NONGROUP_IMPORT, fullImportIdent); 563 } 564 else if (NON_GROUP_RULE_GROUP.equals(currentGroupNumber)) { 565 log(currentImportLine, MSG_NONGROUP_EXPECTED, importGroup, fullImportIdent); 566 } 567 else { 568 log(currentImportLine, MSG_ORDER, importGroup, currentGroupNumber, fullImportIdent); 569 } 570 } 571 572 /** 573 * Get next import group. 574 * @param currentGroupNumber 575 * current group number. 576 * @return 577 * next import group. 578 */ 579 private String getNextImportGroup(int currentGroupNumber) { 580 int nextGroupNumber = currentGroupNumber; 581 582 while (customImportOrderRules.size() > nextGroupNumber + 1) { 583 if (hasAnyImportInCurrentGroup(customImportOrderRules.get(nextGroupNumber))) { 584 break; 585 } 586 nextGroupNumber++; 587 } 588 return customImportOrderRules.get(nextGroupNumber); 589 } 590 591 /** 592 * Checks if current group contains any import. 593 * @param currentGroup 594 * current group. 595 * @return 596 * true, if current group contains at least one import. 597 */ 598 private boolean hasAnyImportInCurrentGroup(String currentGroup) { 599 for (ImportDetails currentImport : importToGroupList) { 600 if (currentGroup.equals(currentImport.getImportGroup())) { 601 return true; 602 } 603 } 604 return false; 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 getFirstNDomainsFromIdent(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 * Checks if a token has a empty line before. 705 * @param lineNo 706 * Line number of current import. 707 * @return true, if token have empty line before. 708 */ 709 private boolean hasEmptyLineBefore(int lineNo) { 710 // [lineNo - 2] is the number of the previous line 711 // because the numbering starts from zero. 712 final String lineBefore = getLine(lineNo - 2); 713 return lineBefore.trim().isEmpty(); 714 } 715 716 /** 717 * Forms import full path. 718 * @param token 719 * current token. 720 * @return full path or null. 721 */ 722 private static String getFullImportIdent(DetailAST token) { 723 if (token == null) { 724 return ""; 725 } 726 else { 727 return FullIdent.createFullIdent(token.findFirstToken(TokenTypes.DOT)).getText(); 728 } 729 } 730 731 /** 732 * Parses ordering rule and adds it to the list with rules. 733 * @param ruleStr 734 * String with rule. 735 */ 736 private void addRulesToList(String ruleStr) { 737 if (STATIC_RULE_GROUP.equals(ruleStr) 738 || THIRD_PARTY_PACKAGE_RULE_GROUP.equals(ruleStr) 739 || STANDARD_JAVA_PACKAGE_RULE_GROUP.equals(ruleStr) 740 || SPECIAL_IMPORTS_RULE_GROUP.equals(ruleStr)) { 741 customImportOrderRules.add(ruleStr); 742 743 } 744 else if (ruleStr.startsWith(SAME_PACKAGE_RULE_GROUP)) { 745 746 final String rule = ruleStr.substring(ruleStr.indexOf('(') + 1, 747 ruleStr.indexOf(')')); 748 samePackageMatchingDepth = Integer.parseInt(rule); 749 if (samePackageMatchingDepth <= 0) { 750 throw new IllegalArgumentException( 751 "SAME_PACKAGE rule parameter should be positive integer: " + ruleStr); 752 } 753 customImportOrderRules.add(SAME_PACKAGE_RULE_GROUP); 754 755 } 756 else { 757 throw new IllegalStateException("Unexpected rule: " + ruleStr); 758 } 759 } 760 761 /** 762 * Creates samePackageDomainsRegExp of the first package domains. 763 * @param firstPackageDomainsCount 764 * number of first package domains. 765 * @param packageNode 766 * package node. 767 * @return same package regexp. 768 */ 769 private static String createSamePackageRegexp(int firstPackageDomainsCount, 770 DetailAST packageNode) { 771 final String packageFullPath = getFullImportIdent(packageNode); 772 return getFirstNDomainsFromIdent(firstPackageDomainsCount, packageFullPath); 773 } 774 775 /** 776 * Extracts defined amount of domains from the left side of package/import identifier 777 * @param firstPackageDomainsCount 778 * number of first package domains. 779 * @param packageFullPath 780 * full identifier containing path to package or imported object. 781 * @return String with defined amount of domains or full identifier 782 * (if full identifier had less domain then specified) 783 */ 784 private static String getFirstNDomainsFromIdent( 785 final int firstPackageDomainsCount, final String packageFullPath) { 786 final StringBuilder builder = new StringBuilder(); 787 final StringTokenizer tokens = new StringTokenizer(packageFullPath, "."); 788 int count = firstPackageDomainsCount; 789 790 while (tokens.hasMoreTokens() && count > 0) { 791 builder.append(tokens.nextToken()).append('.'); 792 count--; 793 } 794 return builder.toString(); 795 } 796 797 /** 798 * Contains import attributes as line number, import full path, import 799 * group. 800 * @author max 801 */ 802 private static class ImportDetails { 803 /** Import full path. */ 804 private final String importFullPath; 805 806 /** Import line number. */ 807 private final int lineNumber; 808 809 /** Import group. */ 810 private final String importGroup; 811 812 /** Is static import. */ 813 private final boolean staticImport; 814 815 /** 816 * @param importFullPath 817 * import full path. 818 * @param lineNumber 819 * import line number. 820 * @param importGroup 821 * import group. 822 * @param staticImport 823 * if import is static. 824 */ 825 ImportDetails(String importFullPath, 826 int lineNumber, String importGroup, boolean staticImport) { 827 this.importFullPath = importFullPath; 828 this.lineNumber = lineNumber; 829 this.importGroup = importGroup; 830 this.staticImport = staticImport; 831 } 832 833 /** 834 * Get import full path variable. 835 * @return import full path variable. 836 */ 837 public String getImportFullPath() { 838 return importFullPath; 839 } 840 841 /** 842 * Get import line number. 843 * @return import line. 844 */ 845 public int getLineNumber() { 846 return lineNumber; 847 } 848 849 /** 850 * Get import group. 851 * @return import group. 852 */ 853 public String getImportGroup() { 854 return importGroup; 855 } 856 857 /** 858 * Checks if import is static. 859 * @return true, if import is static. 860 */ 861 public boolean isStaticImport() { 862 return staticImport; 863 } 864 } 865 866 /** 867 * Contains matching attributes assisting in definition of "best matching" 868 * group for import. 869 * @author ivanov-alex 870 */ 871 private static class RuleMatchForImport { 872 /** Import group for current best match. */ 873 private String group; 874 /** Length of matching string for current best match. */ 875 private int matchLength; 876 /** Position of matching string for current best match. */ 877 private final int matchPosition; 878 879 /** Constructor to initialize the fields. 880 * @param group 881 * Matched group. 882 * @param length 883 * Matching length. 884 * @param position 885 * Matching position. 886 */ 887 RuleMatchForImport(String group, int length, int position) { 888 this.group = group; 889 matchLength = length; 890 matchPosition = position; 891 } 892 } 893}