Coverage Report - com.puppycrawl.tools.checkstyle.checks.imports.CustomImportOrderCheck
 
Classes in this File Line Coverage Branch Coverage Complexity
CustomImportOrderCheck
100%
170/170
100%
102/102
2.733
CustomImportOrderCheck$ImportDetails
100%
10/10
N/A
2.733
CustomImportOrderCheck$RuleMatchForImport
100%
6/6
N/A
2.733
 
 1  
 ////////////////////////////////////////////////////////////////////////////////
 2  
 // checkstyle: Checks Java source code for adherence to a set of rules.
 3  
 // Copyright (C) 2001-2018 the original author or authors.
 4  
 //
 5  
 // This library is free software; you can redistribute it and/or
 6  
 // modify it under the terms of the GNU Lesser General Public
 7  
 // License as published by the Free Software Foundation; either
 8  
 // version 2.1 of the License, or (at your option) any later version.
 9  
 //
 10  
 // This library is distributed in the hope that it will be useful,
 11  
 // but WITHOUT ANY WARRANTY; without even the implied warranty of
 12  
 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 13  
 // Lesser General Public License for more details.
 14  
 //
 15  
 // You should have received a copy of the GNU Lesser General Public
 16  
 // License along with this library; if not, write to the Free Software
 17  
 // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 18  
 ////////////////////////////////////////////////////////////////////////////////
 19  
 
 20  
 package com.puppycrawl.tools.checkstyle.checks.imports;
 21  
 
 22  
 import java.util.ArrayList;
 23  
 import java.util.List;
 24  
 import java.util.StringTokenizer;
 25  
 import java.util.regex.Matcher;
 26  
 import java.util.regex.Pattern;
 27  
 
 28  
 import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
 29  
 import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
 30  
 import com.puppycrawl.tools.checkstyle.api.DetailAST;
 31  
 import com.puppycrawl.tools.checkstyle.api.FullIdent;
 32  
 import com.puppycrawl.tools.checkstyle.api.TokenTypes;
 33  
 import com.puppycrawl.tools.checkstyle.utils.CommonUtils;
 34  
 
 35  
 /**
 36  
  * <p>
 37  
  * Checks that the groups of import declarations appear in the order specified
 38  
  * by the user. If there is an import but its group is not specified in the
 39  
  * configuration such an import should be placed at the end of the import list.
 40  
  * </p>
 41  
  * The rule consists of:
 42  
  *
 43  
  * <p>
 44  
  * 1. STATIC group. This group sets the ordering of static imports.
 45  
  * </p>
 46  
  *
 47  
  * <p>
 48  
  * 2. SAME_PACKAGE(n) group. This group sets the ordering of the same package imports.
 49  
  * Imports are considered on SAME_PACKAGE group if <b>n</b> first domains in package name
 50  
  * and import name are identical.
 51  
  * </p>
 52  
  *
 53  
  * <pre>
 54  
  *{@code
 55  
  *package java.util.concurrent.locks;
 56  
  *
 57  
  *import java.io.File;
 58  
  *import java.util.*; //#1
 59  
  *import java.util.List; //#2
 60  
  *import java.util.StringTokenizer; //#3
 61  
  *import java.util.concurrent.*; //#4
 62  
  *import java.util.concurrent.AbstractExecutorService; //#5
 63  
  *import java.util.concurrent.locks.LockSupport; //#6
 64  
  *import java.util.regex.Pattern; //#7
 65  
  *import java.util.regex.Matcher; //#8
 66  
  *}
 67  
  * </pre>
 68  
  *
 69  
  * <p>
 70  
  * If we have SAME_PACKAGE(3) on configuration file,
 71  
  * imports #4-6 will be considered as a SAME_PACKAGE group (java.util.concurrent.*,
 72  
  * java.util.concurrent.AbstractExecutorService, java.util.concurrent.locks.LockSupport).
 73  
  * SAME_PACKAGE(2) will include #1-8. SAME_PACKAGE(4) will include only #6.
 74  
  * SAME_PACKAGE(5) will result in no imports assigned to SAME_PACKAGE group because
 75  
  * actual package java.util.concurrent.locks has only 4 domains.
 76  
  * </p>
 77  
  *
 78  
  * <p>
 79  
  * 3. THIRD_PARTY_PACKAGE group. This group sets ordering of third party imports.
 80  
  * Third party imports are all imports except STATIC,
 81  
  * SAME_PACKAGE(n), STANDARD_JAVA_PACKAGE and SPECIAL_IMPORTS.
 82  
  * </p>
 83  
  *
 84  
  * <p>
 85  
  * 4. STANDARD_JAVA_PACKAGE group. By default this group sets ordering of standard java/javax
 86  
  * imports.
 87  
  * </p>
 88  
  *
 89  
  * <p>
 90  
  * 5. SPECIAL_IMPORTS group. This group may contains some imports
 91  
  * that have particular meaning for the user.
 92  
  * </p>
 93  
  *
 94  
  * <p>
 95  
  * NOTE!
 96  
  * </p>
 97  
  * <p>
 98  
  * Use the separator '###' between rules.
 99  
  * </p>
 100  
  * <p>
 101  
  * To set RegExps for THIRD_PARTY_PACKAGE and STANDARD_JAVA_PACKAGE groups use
 102  
  * thirdPartyPackageRegExp and standardPackageRegExp options.
 103  
  * </p>
 104  
  * <p>
 105  
  * Pretty often one import can match more than one group. For example, static import from standard
 106  
  * package or regular expressions are configured to allow one import match multiple groups.
 107  
  * In this case, group will be assigned according to priorities:
 108  
  * </p>
 109  
  * <ol>
 110  
  * <li>
 111  
  *    STATIC has top priority
 112  
  * </li>
 113  
  * <li>
 114  
  *    SAME_PACKAGE has second priority
 115  
  * </li>
 116  
  * <li>
 117  
  *    STANDARD_JAVA_PACKAGE and SPECIAL_IMPORTS will compete using "best match" rule: longer
 118  
  *    matching substring wins; in case of the same length, lower position of matching substring
 119  
  *    wins; if position is the same, order of rules in configuration solves the puzzle.
 120  
  * </li>
 121  
  * <li>
 122  
  *    THIRD_PARTY has the least priority
 123  
  * </li>
 124  
  * </ol>
 125  
  * <p>
 126  
  *    Few examples to illustrate "best match":
 127  
  * </p>
 128  
  * <p>
 129  
  *    1. patterns STANDARD_JAVA_PACKAGE = "Check", SPECIAL_IMPORTS="ImportOrderCheck" and input
 130  
  *    file:
 131  
  * </p>
 132  
  * <pre>
 133  
  *{@code
 134  
  *import com.puppycrawl.tools.checkstyle.checks.imports.CustomImportOrderCheck;
 135  
  *import com.puppycrawl.tools.checkstyle.checks.imports.ImportOrderCheck;}
 136  
  * </pre>
 137  
  * <p>
 138  
  *    Result: imports will be assigned to SPECIAL_IMPORTS, because matching substring length is 16.
 139  
  *    Matching substring for STANDARD_JAVA_PACKAGE is 5.
 140  
  * </p>
 141  
  * <p>
 142  
  *    2. patterns STANDARD_JAVA_PACKAGE = "Check", SPECIAL_IMPORTS="Avoid" and file:
 143  
  * </p>
 144  
  * <pre>
 145  
  *{@code
 146  
  *import com.puppycrawl.tools.checkstyle.checks.imports.AvoidStarImportCheck;}
 147  
  * </pre>
 148  
  * <p>
 149  
  *   Result: import will be assigned to SPECIAL_IMPORTS. Matching substring length is 5 for both
 150  
  *   patterns. However, "Avoid" position is lower then "Check" position.
 151  
  * </p>
 152  
  *
 153  
  * <pre>
 154  
  *    Properties:
 155  
  * </pre>
 156  
  * <table summary="Properties" border="1">
 157  
  *     <tr><th>name</th><th>Description</th><th>type</th><th>default value</th></tr>
 158  
  *      <tr><td>customImportOrderRules</td><td>List of order declaration customizing by user.</td>
 159  
  *          <td>string</td><td>null</td></tr>
 160  
  *      <tr><td>standardPackageRegExp</td><td>RegExp for STANDARD_JAVA_PACKAGE group imports.</td>
 161  
  *          <td>regular expression</td><td>^(java|javax)\.</td></tr>
 162  
  *      <tr><td>thirdPartyPackageRegExp</td><td>RegExp for THIRD_PARTY_PACKAGE group imports.</td>
 163  
  *          <td>regular expression</td><td>.*</td></tr>
 164  
  *      <tr><td>specialImportsRegExp</td><td>RegExp for SPECIAL_IMPORTS group imports.</td>
 165  
  *          <td>regular expression</td><td>^$</td></tr>
 166  
  *      <tr><td>separateLineBetweenGroups</td><td>Force empty line separator between import groups.
 167  
  *          </td><td>boolean</td><td>true</td></tr>
 168  
  *      <tr><td>sortImportsInGroupAlphabetically</td><td>Force grouping alphabetically,
 169  
  *          in ASCII sort order.</td><td>boolean</td><td>false</td></tr>
 170  
  * </table>
 171  
  *
 172  
  * <p>
 173  
  * For example:
 174  
  * </p>
 175  
  *        <p>To configure the check so that it matches default Eclipse formatter configuration
 176  
  *        (tested on Kepler, Luna and Mars):</p>
 177  
  *        <ul>
 178  
  *          <li>group of static imports is on the top</li>
 179  
  *          <li>groups of non-static imports: &quot;java&quot; and &quot;javax&quot; packages
 180  
  *          first, then &quot;org&quot; and then all other imports</li>
 181  
  *          <li>imports will be sorted in the groups</li>
 182  
  *          <li>groups are separated by, at least, one blank line</li>
 183  
  *        </ul>
 184  
  * <pre>
 185  
  * &lt;module name=&quot;CustomImportOrder&quot;&gt;
 186  
  *    &lt;property name=&quot;customImportOrderRules&quot;
 187  
  *        value=&quot;STATIC###STANDARD_JAVA_PACKAGE###SPECIAL_IMPORTS&quot;/&gt;
 188  
  *    &lt;property name=&quot;specialImportsRegExp&quot; value=&quot;org&quot;/&gt;
 189  
  *    &lt;property name=&quot;sortImportsInGroupAlphabetically&quot; value=&quot;true&quot;/&gt;
 190  
  *    &lt;property name=&quot;separateLineBetweenGroups&quot; value=&quot;true&quot;/&gt;
 191  
  * &lt;/module&gt;
 192  
  * </pre>
 193  
  *
 194  
  *        <p>To configure the check so that it matches default IntelliJ IDEA formatter
 195  
  *        configuration (tested on v14):</p>
 196  
  *        <ul>
 197  
  *          <li>group of static imports is on the bottom</li>
 198  
  *          <li>groups of non-static imports: all imports except of &quot;javax&quot;
 199  
  *          and &quot;java&quot;, then &quot;javax&quot; and &quot;java&quot;</li>
 200  
  *          <li>imports will be sorted in the groups</li>
 201  
  *          <li>groups are separated by, at least, one blank line</li>
 202  
  *        </ul>
 203  
  *
 204  
  *        <p>
 205  
  *        Note: &quot;separated&quot; option is disabled because IDEA default has blank line
 206  
  *        between &quot;java&quot; and static imports, and no blank line between
 207  
  *        &quot;javax&quot; and &quot;java&quot;
 208  
  *        </p>
 209  
  *
 210  
  * <pre>
 211  
  * &lt;module name=&quot;CustomImportOrder&quot;&gt;
 212  
  *    &lt;property name=&quot;customImportOrderRules&quot;
 213  
  *        value=&quot;THIRD_PARTY_PACKAGE###SPECIAL_IMPORTS###STANDARD_JAVA_PACKAGE
 214  
  *        ###STATIC&quot;/&gt;
 215  
  *    &lt;property name=&quot;specialImportsRegExp&quot; value=&quot;^javax\.&quot;/&gt;
 216  
  *    &lt;property name=&quot;standardPackageRegExp&quot; value=&quot;^java\.&quot;/&gt;
 217  
  *    &lt;property name=&quot;sortImportsInGroupAlphabetically&quot; value=&quot;true&quot;/&gt;
 218  
  *    &lt;property name=&quot;separateLineBetweenGroups&quot; value=&quot;false&quot;/&gt;
 219  
  *&lt;/module&gt;
 220  
  * </pre>
 221  
  *
 222  
  * <p>To configure the check so that it matches default NetBeans formatter
 223  
  *    configuration (tested on v8):</p>
 224  
  * <ul>
 225  
  *     <li>groups of non-static imports are not defined, all imports will be sorted as a one
 226  
  *         group</li>
 227  
  *     <li>static imports are not separated, they will be sorted along with other imports</li>
 228  
  * </ul>
 229  
  *
 230  
  * <pre>
 231  
  *&lt;module name=&quot;CustomImportOrder&quot;/&gt;
 232  
  * </pre>
 233  
  * <p>To set RegExps for THIRD_PARTY_PACKAGE and STANDARD_JAVA_PACKAGE groups use
 234  
  *         thirdPartyPackageRegExp and standardPackageRegExp options.</p>
 235  
  * <pre>
 236  
  * &lt;module name=&quot;CustomImportOrder&quot;&gt;
 237  
  *    &lt;property name=&quot;customImportOrderRules&quot;
 238  
  *    value=&quot;STATIC###SAME_PACKAGE(3)###THIRD_PARTY_PACKAGE###STANDARD_JAVA_PACKAGE&quot;/&gt;
 239  
  *    &lt;property name=&quot;thirdPartyPackageRegExp&quot; value=&quot;com|org&quot;/&gt;
 240  
  *    &lt;property name=&quot;standardPackageRegExp&quot; value=&quot;^(java|javax)\.&quot;/&gt;
 241  
  * &lt;/module&gt;
 242  
  * </pre>
 243  
  * <p>
 244  
  * Also, this check can be configured to force empty line separator between
 245  
  * import groups. For example
 246  
  * </p>
 247  
  *
 248  
  * <pre>
 249  
  * &lt;module name=&quot;CustomImportOrder&quot;&gt;
 250  
  *    &lt;property name=&quot;separateLineBetweenGroups&quot; value=&quot;true&quot;/&gt;
 251  
  * &lt;/module&gt;
 252  
  * </pre>
 253  
  * <p>
 254  
  * It is possible to enforce
 255  
  * <a href="https://en.wikipedia.org/wiki/ASCII#Order">ASCII sort order</a>
 256  
  * of imports in groups using the following configuration:
 257  
  * </p>
 258  
  * <pre>
 259  
  * &lt;module name=&quot;CustomImportOrder&quot;&gt;
 260  
  *    &lt;property name=&quot;sortImportsInGroupAlphabetically&quot; value=&quot;true&quot;/&gt;
 261  
  * &lt;/module&gt;
 262  
  * </pre>
 263  
  * <p>
 264  
  * Example of ASCII order:
 265  
  * </p>
 266  
  * <pre>
 267  
  * {@code
 268  
  *import java.awt.Dialog;
 269  
  *import java.awt.Window;
 270  
  *import java.awt.color.ColorSpace;
 271  
  *import java.awt.Frame; // violation here - in ASCII order 'F' should go before 'c',
 272  
  *                       // as all uppercase come before lowercase letters}
 273  
  * </pre>
 274  
  * <p>
 275  
  * To force checking imports sequence such as:
 276  
  * </p>
 277  
  *
 278  
  * <pre>
 279  
  * {@code
 280  
  * package com.puppycrawl.tools.checkstyle.imports;
 281  
  *
 282  
  * import com.google.common.annotations.GwtCompatible;
 283  
  * import com.google.common.annotations.Beta;
 284  
  * import com.google.common.annotations.VisibleForTesting;
 285  
  *
 286  
  * import org.abego.treelayout.Configuration;
 287  
  *
 288  
  * import static sun.tools.util.ModifierFilter.ALL_ACCESS;
 289  
  *
 290  
  * import com.google.common.annotations.GwtCompatible; // violation here - should be in the
 291  
  *                                                     // THIRD_PARTY_PACKAGE group
 292  
  * import android.*;}
 293  
  * </pre>
 294  
  * configure as follows:
 295  
  * <pre>
 296  
  * &lt;module name=&quot;CustomImportOrder&quot;&gt;
 297  
  *    &lt;property name=&quot;customImportOrderRules&quot;
 298  
  *    value=&quot;SAME_PACKAGE(3)###THIRD_PARTY_PACKAGE###STATIC###SPECIAL_IMPORTS&quot;/&gt;
 299  
  *    &lt;property name=&quot;specialImportsRegExp&quot; value=&quot;android.*&quot;/&gt;
 300  
  * &lt;/module&gt;
 301  
  * </pre>
 302  
  *
 303  
  * @author maxvetrenko
 304  
  * @author <a href="mailto:nesterenko-aleksey@list.ru">Aleksey Nesterenko</a>
 305  
  */
 306  
 @FileStatefulCheck
 307  58
 public class CustomImportOrderCheck extends AbstractCheck {
 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  2
     private static final Pattern GROUP_SEPARATOR_PATTERN = Pattern.compile("\\s*###\\s*");
 359  
 
 360  
     /** List of order declaration customizing by user. */
 361  58
     private final List<String> customImportOrderRules = new ArrayList<>();
 362  
 
 363  
     /** Contains objects with import attributes. */
 364  58
     private final List<ImportDetails> importToGroupList = new ArrayList<>();
 365  
 
 366  
     /** RegExp for SAME_PACKAGE group imports. */
 367  58
     private String samePackageDomainsRegExp = "";
 368  
 
 369  
     /** RegExp for STANDARD_JAVA_PACKAGE group imports. */
 370  58
     private Pattern standardPackageRegExp = Pattern.compile("^(java|javax)\\.");
 371  
 
 372  
     /** RegExp for THIRD_PARTY_PACKAGE group imports. */
 373  58
     private Pattern thirdPartyPackageRegExp = Pattern.compile(".*");
 374  
 
 375  
     /** RegExp for SPECIAL_IMPORTS group imports. */
 376  58
     private Pattern specialImportsRegExp = Pattern.compile("^$");
 377  
 
 378  
     /** Force empty line separator between import groups. */
 379  58
     private boolean separateLineBetweenGroups = true;
 380  
 
 381  
     /** Force grouping alphabetically, in ASCII order. */
 382  
     private boolean sortImportsInGroupAlphabetically;
 383  
 
 384  
     /** Number of first domains for SAME_PACKAGE group. */
 385  58
     private int samePackageMatchingDepth = 2;
 386  
 
 387  
     /**
 388  
      * Sets standardRegExp specified by user.
 389  
      * @param regexp
 390  
      *        user value.
 391  
      */
 392  
     public final void setStandardPackageRegExp(Pattern regexp) {
 393  12
         standardPackageRegExp = regexp;
 394  12
     }
 395  
 
 396  
     /**
 397  
      * Sets thirdPartyRegExp specified by user.
 398  
      * @param regexp
 399  
      *        user value.
 400  
      */
 401  
     public final void setThirdPartyPackageRegExp(Pattern regexp) {
 402  14
         thirdPartyPackageRegExp = regexp;
 403  14
     }
 404  
 
 405  
     /**
 406  
      * Sets specialImportsRegExp specified by user.
 407  
      * @param regexp
 408  
      *        user value.
 409  
      */
 410  
     public final void setSpecialImportsRegExp(Pattern regexp) {
 411  18
         specialImportsRegExp = regexp;
 412  18
     }
 413  
 
 414  
     /**
 415  
      * Sets separateLineBetweenGroups specified by user.
 416  
      * @param value
 417  
      *        user value.
 418  
      */
 419  
     public final void setSeparateLineBetweenGroups(boolean value) {
 420  20
         separateLineBetweenGroups = value;
 421  20
     }
 422  
 
 423  
     /**
 424  
      * Sets sortImportsInGroupAlphabetically specified by user.
 425  
      * @param value
 426  
      *        user value.
 427  
      */
 428  
     public final void setSortImportsInGroupAlphabetically(boolean value) {
 429  29
         sortImportsInGroupAlphabetically = value;
 430  29
     }
 431  
 
 432  
     /**
 433  
      * Sets a custom import order from the rules in the string format specified
 434  
      * by user.
 435  
      * @param inputCustomImportOrder
 436  
      *        user value.
 437  
      */
 438  
     public final void setCustomImportOrderRules(final String inputCustomImportOrder) {
 439  145
         for (String currentState : GROUP_SEPARATOR_PATTERN.split(inputCustomImportOrder)) {
 440  106
             addRulesToList(currentState);
 441  
         }
 442  39
         customImportOrderRules.add(NON_GROUP_RULE_GROUP);
 443  39
     }
 444  
 
 445  
     @Override
 446  
     public int[] getDefaultTokens() {
 447  95
         return getRequiredTokens();
 448  
     }
 449  
 
 450  
     @Override
 451  
     public int[] getAcceptableTokens() {
 452  6
         return getRequiredTokens();
 453  
     }
 454  
 
 455  
     @Override
 456  
     public int[] getRequiredTokens() {
 457  197
         return new int[] {
 458  
             TokenTypes.IMPORT,
 459  
             TokenTypes.STATIC_IMPORT,
 460  
             TokenTypes.PACKAGE_DEF,
 461  
         };
 462  
     }
 463  
 
 464  
     @Override
 465  
     public void beginTree(DetailAST rootAST) {
 466  31
         importToGroupList.clear();
 467  31
     }
 468  
 
 469  
     @Override
 470  
     public void visitToken(DetailAST ast) {
 471  262
         if (ast.getType() == TokenTypes.PACKAGE_DEF) {
 472  28
             if (customImportOrderRules.contains(SAME_PACKAGE_RULE_GROUP)) {
 473  12
                 samePackageDomainsRegExp = createSamePackageRegexp(
 474  
                         samePackageMatchingDepth, ast);
 475  
             }
 476  
         }
 477  
         else {
 478  234
             final String importFullPath = getFullImportIdent(ast);
 479  234
             final int lineNo = ast.getLineNo();
 480  234
             final boolean isStatic = ast.getType() == TokenTypes.STATIC_IMPORT;
 481  468
             importToGroupList.add(new ImportDetails(importFullPath,
 482  234
                     lineNo, getImportGroup(isStatic, importFullPath),
 483  
                     isStatic));
 484  
         }
 485  262
     }
 486  
 
 487  
     @Override
 488  
     public void finishTree(DetailAST rootAST) {
 489  31
         if (!importToGroupList.isEmpty()) {
 490  28
             finishImportList();
 491  
         }
 492  31
     }
 493  
 
 494  
     /** Examine the order of all the imports and log any violations. */
 495  
     private void finishImportList() {
 496  28
         final ImportDetails firstImport = importToGroupList.get(0);
 497  56
         String currentGroup = getImportGroup(firstImport.isStaticImport(),
 498  28
                 firstImport.getImportFullPath());
 499  28
         int currentGroupNumber = customImportOrderRules.indexOf(currentGroup);
 500  28
         String previousImportFromCurrentGroup = null;
 501  
 
 502  28
         for (ImportDetails importObject : importToGroupList) {
 503  234
             final String importGroup = importObject.getImportGroup();
 504  234
             final String fullImportIdent = importObject.getImportFullPath();
 505  
 
 506  234
             if (getCountOfEmptyLinesBefore(importObject.getLineNumber()) > 1) {
 507  6
                 log(importObject.getLineNumber(), MSG_LINE_SEPARATOR, fullImportIdent);
 508  
             }
 509  234
             if (importGroup.equals(currentGroup)) {
 510  158
                 if (sortImportsInGroupAlphabetically
 511  
                         && previousImportFromCurrentGroup != null
 512  77
                         && compareImports(fullImportIdent, previousImportFromCurrentGroup) < 0) {
 513  36
                     log(importObject.getLineNumber(), MSG_LEX,
 514  
                             fullImportIdent, previousImportFromCurrentGroup);
 515  
                 }
 516  
                 else {
 517  122
                     previousImportFromCurrentGroup = fullImportIdent;
 518  
                 }
 519  
             }
 520  
             else {
 521  
                 //not the last group, last one is always NON_GROUP
 522  76
                 if (customImportOrderRules.size() > currentGroupNumber + 1) {
 523  51
                     final String nextGroup = getNextImportGroup(currentGroupNumber + 1);
 524  51
                     if (importGroup.equals(nextGroup)) {
 525  23
                         if (separateLineBetweenGroups
 526  20
                                 && getCountOfEmptyLinesBefore(importObject.getLineNumber()) == 0) {
 527  2
                             log(importObject.getLineNumber(), MSG_LINE_SEPARATOR, fullImportIdent);
 528  
                         }
 529  23
                         currentGroup = nextGroup;
 530  23
                         currentGroupNumber = customImportOrderRules.indexOf(nextGroup);
 531  23
                         previousImportFromCurrentGroup = fullImportIdent;
 532  
                     }
 533  
                     else {
 534  28
                         logWrongImportGroupOrder(importObject.getLineNumber(),
 535  
                                 importGroup, nextGroup, fullImportIdent);
 536  
                     }
 537  51
                 }
 538  
                 else {
 539  25
                     logWrongImportGroupOrder(importObject.getLineNumber(),
 540  
                             importGroup, currentGroup, fullImportIdent);
 541  
                 }
 542  
             }
 543  234
         }
 544  28
     }
 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  53
         if (NON_GROUP_RULE_GROUP.equals(importGroup)) {
 560  1
             log(currentImportLine, MSG_NONGROUP_IMPORT, fullImportIdent);
 561  
         }
 562  52
         else if (NON_GROUP_RULE_GROUP.equals(currentGroupNumber)) {
 563  27
             log(currentImportLine, MSG_NONGROUP_EXPECTED, importGroup, fullImportIdent);
 564  
         }
 565  
         else {
 566  25
             log(currentImportLine, MSG_ORDER, importGroup, currentGroupNumber, fullImportIdent);
 567  
         }
 568  53
     }
 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  51
         int nextGroupNumber = currentGroupNumber;
 579  
 
 580  62
         while (customImportOrderRules.size() > nextGroupNumber + 1) {
 581  57
             if (hasAnyImportInCurrentGroup(customImportOrderRules.get(nextGroupNumber))) {
 582  46
                 break;
 583  
             }
 584  11
             nextGroupNumber++;
 585  
         }
 586  51
         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  57
         boolean result = false;
 598  57
         for (ImportDetails currentImport : importToGroupList) {
 599  694
             if (currentGroup.equals(currentImport.getImportGroup())) {
 600  46
                 result = true;
 601  46
                 break;
 602  
             }
 603  648
         }
 604  57
         return result;
 605  
     }
 606  
 
 607  
     /**
 608  
      * Get import valid group.
 609  
      * @param isStatic
 610  
      *        is static import.
 611  
      * @param importPath
 612  
      *        full import path.
 613  
      * @return import valid group.
 614  
      */
 615  
     private String getImportGroup(boolean isStatic, String importPath) {
 616  262
         RuleMatchForImport bestMatch = new RuleMatchForImport(NON_GROUP_RULE_GROUP, 0, 0);
 617  262
         if (isStatic && customImportOrderRules.contains(STATIC_RULE_GROUP)) {
 618  36
             bestMatch.group = STATIC_RULE_GROUP;
 619  36
             bestMatch.matchLength = importPath.length();
 620  
         }
 621  226
         else if (customImportOrderRules.contains(SAME_PACKAGE_RULE_GROUP)) {
 622  127
             final String importPathTrimmedToSamePackageDepth =
 623  127
                     getFirstDomainsFromIdent(samePackageMatchingDepth, importPath);
 624  127
             if (samePackageDomainsRegExp.equals(importPathTrimmedToSamePackageDepth)) {
 625  26
                 bestMatch.group = SAME_PACKAGE_RULE_GROUP;
 626  26
                 bestMatch.matchLength = importPath.length();
 627  
             }
 628  
         }
 629  262
         if (bestMatch.group.equals(NON_GROUP_RULE_GROUP)) {
 630  200
             for (String group : customImportOrderRules) {
 631  655
                 if (STANDARD_JAVA_PACKAGE_RULE_GROUP.equals(group)) {
 632  122
                     bestMatch = findBetterPatternMatch(importPath,
 633  
                             STANDARD_JAVA_PACKAGE_RULE_GROUP, standardPackageRegExp, bestMatch);
 634  
                 }
 635  655
                 if (SPECIAL_IMPORTS_RULE_GROUP.equals(group)) {
 636  49
                     bestMatch = findBetterPatternMatch(importPath,
 637  
                             group, specialImportsRegExp, bestMatch);
 638  
                 }
 639  655
             }
 640  
         }
 641  262
         if (bestMatch.group.equals(NON_GROUP_RULE_GROUP)
 642  98
                 && customImportOrderRules.contains(THIRD_PARTY_PACKAGE_RULE_GROUP)
 643  30
                 && thirdPartyPackageRegExp.matcher(importPath).find()) {
 644  28
             bestMatch.group = THIRD_PARTY_PACKAGE_RULE_GROUP;
 645  
         }
 646  262
         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  171
         RuleMatchForImport betterMatchCandidate = currentBestMatch;
 665  171
         final Matcher matcher = regExp.matcher(importPath);
 666  295
         while (matcher.find()) {
 667  124
             final int length = matcher.end() - matcher.start();
 668  124
             if (length > betterMatchCandidate.matchLength
 669  19
                     || length == betterMatchCandidate.matchLength
 670  13
                         && matcher.start() < betterMatchCandidate.matchPosition) {
 671  107
                 betterMatchCandidate = new RuleMatchForImport(group, length, matcher.start());
 672  
             }
 673  124
         }
 674  171
         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  77
         int result = 0;
 689  77
         final String separator = "\\.";
 690  77
         final String[] import1Tokens = import1.split(separator);
 691  77
         final String[] import2Tokens = import2.split(separator);
 692  151
         for (int i = 0; i != import1Tokens.length && i != import2Tokens.length; i++) {
 693  147
             final String import1Token = import1Tokens[i];
 694  147
             final String import2Token = import2Tokens[i];
 695  147
             result = import1Token.compareTo(import2Token);
 696  147
             if (result != 0) {
 697  73
                 break;
 698  
             }
 699  
         }
 700  77
         if (result == 0) {
 701  4
             result = Integer.compare(import1Tokens.length, import2Tokens.length);
 702  
         }
 703  77
         return result;
 704  
     }
 705  
 
 706  
     /**
 707  
      * Counts empty lines before given.
 708  
      * @param lineNo
 709  
      *        Line number of current import.
 710  
      * @return count of empty lines before given.
 711  
      */
 712  
     private int getCountOfEmptyLinesBefore(int lineNo) {
 713  254
         int result = 0;
 714  254
         final String[] lines = getLines();
 715  
         //  [lineNo - 2] is the number of the previous line
 716  
         //  because the numbering starts from zero.
 717  254
         int lineBeforeIndex = lineNo - 2;
 718  334
         while (lineBeforeIndex >= 0
 719  332
                 && CommonUtils.isBlank(lines[lineBeforeIndex])) {
 720  80
             lineBeforeIndex--;
 721  80
             result++;
 722  
         }
 723  254
         return result;
 724  
     }
 725  
 
 726  
     /**
 727  
      * Forms import full path.
 728  
      * @param token
 729  
      *        current token.
 730  
      * @return full path or null.
 731  
      */
 732  
     private static String getFullImportIdent(DetailAST token) {
 733  247
         String ident = "";
 734  247
         if (token != null) {
 735  246
             ident = FullIdent.createFullIdent(token.findFirstToken(TokenTypes.DOT)).getText();
 736  
         }
 737  247
         return ident;
 738  
     }
 739  
 
 740  
     /**
 741  
      * Parses ordering rule and adds it to the list with rules.
 742  
      * @param ruleStr
 743  
      *        String with rule.
 744  
      */
 745  
     private void addRulesToList(String ruleStr) {
 746  106
         if (STATIC_RULE_GROUP.equals(ruleStr)
 747  85
                 || THIRD_PARTY_PACKAGE_RULE_GROUP.equals(ruleStr)
 748  64
                 || STANDARD_JAVA_PACKAGE_RULE_GROUP.equals(ruleStr)
 749  39
                 || SPECIAL_IMPORTS_RULE_GROUP.equals(ruleStr)) {
 750  85
             customImportOrderRules.add(ruleStr);
 751  
         }
 752  21
         else if (ruleStr.startsWith(SAME_PACKAGE_RULE_GROUP)) {
 753  40
             final String rule = ruleStr.substring(ruleStr.indexOf('(') + 1,
 754  20
                     ruleStr.indexOf(')'));
 755  20
             samePackageMatchingDepth = Integer.parseInt(rule);
 756  19
             if (samePackageMatchingDepth <= 0) {
 757  2
                 throw new IllegalArgumentException(
 758  
                         "SAME_PACKAGE rule parameter should be positive integer: " + ruleStr);
 759  
             }
 760  17
             customImportOrderRules.add(SAME_PACKAGE_RULE_GROUP);
 761  17
         }
 762  
         else {
 763  1
             throw new IllegalStateException("Unexpected rule: " + ruleStr);
 764  
         }
 765  102
     }
 766  
 
 767  
     /**
 768  
      * Creates samePackageDomainsRegExp of the first package domains.
 769  
      * @param firstPackageDomainsCount
 770  
      *        number of first package domains.
 771  
      * @param packageNode
 772  
      *        package node.
 773  
      * @return same package regexp.
 774  
      */
 775  
     private static String createSamePackageRegexp(int firstPackageDomainsCount,
 776  
              DetailAST packageNode) {
 777  12
         final String packageFullPath = getFullImportIdent(packageNode);
 778  12
         return getFirstDomainsFromIdent(firstPackageDomainsCount, packageFullPath);
 779  
     }
 780  
 
 781  
     /**
 782  
      * Extracts defined amount of domains from the left side of package/import identifier.
 783  
      * @param firstPackageDomainsCount
 784  
      *        number of first package domains.
 785  
      * @param packageFullPath
 786  
      *        full identifier containing path to package or imported object.
 787  
      * @return String with defined amount of domains or full identifier
 788  
      *        (if full identifier had less domain then specified)
 789  
      */
 790  
     private static String getFirstDomainsFromIdent(
 791  
             final int firstPackageDomainsCount, final String packageFullPath) {
 792  139
         final StringBuilder builder = new StringBuilder(256);
 793  139
         final StringTokenizer tokens = new StringTokenizer(packageFullPath, ".");
 794  139
         int count = firstPackageDomainsCount;
 795  
 
 796  595
         while (count > 0 && tokens.hasMoreTokens()) {
 797  456
             builder.append(tokens.nextToken()).append('.');
 798  456
             count--;
 799  
         }
 800  139
         return builder.toString();
 801  
     }
 802  
 
 803  
     /**
 804  
      * Contains import attributes as line number, import full path, import
 805  
      * group.
 806  
      * @author max
 807  
      */
 808  
     private static class ImportDetails {
 809  
 
 810  
         /** Import full path. */
 811  
         private final String importFullPath;
 812  
 
 813  
         /** Import line number. */
 814  
         private final int lineNumber;
 815  
 
 816  
         /** Import group. */
 817  
         private final String importGroup;
 818  
 
 819  
         /** Is static import. */
 820  
         private final boolean staticImport;
 821  
 
 822  
         /**
 823  
          * Initialise importFullPath, lineNumber, importGroup, staticImport.
 824  
          * @param importFullPath
 825  
          *        import full path.
 826  
          * @param lineNumber
 827  
          *        import line number.
 828  
          * @param importGroup
 829  
          *        import group.
 830  
          * @param staticImport
 831  
          *        if import is static.
 832  
          */
 833  
         ImportDetails(String importFullPath,
 834  234
                 int lineNumber, String importGroup, boolean staticImport) {
 835  234
             this.importFullPath = importFullPath;
 836  234
             this.lineNumber = lineNumber;
 837  234
             this.importGroup = importGroup;
 838  234
             this.staticImport = staticImport;
 839  234
         }
 840  
 
 841  
         /**
 842  
          * Get import full path variable.
 843  
          * @return import full path variable.
 844  
          */
 845  
         public String getImportFullPath() {
 846  262
             return importFullPath;
 847  
         }
 848  
 
 849  
         /**
 850  
          * Get import line number.
 851  
          * @return import line.
 852  
          */
 853  
         public int getLineNumber() {
 854  351
             return lineNumber;
 855  
         }
 856  
 
 857  
         /**
 858  
          * Get import group.
 859  
          * @return import group.
 860  
          */
 861  
         public String getImportGroup() {
 862  928
             return importGroup;
 863  
         }
 864  
 
 865  
         /**
 866  
          * Checks if import is static.
 867  
          * @return true, if import is static.
 868  
          */
 869  
         public boolean isStaticImport() {
 870  28
             return staticImport;
 871  
         }
 872  
 
 873  
     }
 874  
 
 875  
     /**
 876  
      * Contains matching attributes assisting in definition of "best matching"
 877  
      * group for import.
 878  
      * @author ivanov-alex
 879  
      */
 880  1094
     private static class RuleMatchForImport {
 881  
 
 882  
         /** Position of matching string for current best match. */
 883  
         private final int matchPosition;
 884  
         /** Length of matching string for current best match. */
 885  
         private int matchLength;
 886  
         /** Import group for current best match. */
 887  
         private String group;
 888  
 
 889  
         /** Constructor to initialize the fields.
 890  
          * @param group
 891  
          *        Matched group.
 892  
          * @param length
 893  
          *        Matching length.
 894  
          * @param position
 895  
          *        Matching position.
 896  
          */
 897  369
         RuleMatchForImport(String group, int length, int position) {
 898  369
             this.group = group;
 899  369
             matchLength = length;
 900  369
             matchPosition = position;
 901  369
         }
 902  
 
 903  
     }
 904  
 
 905  
 }