Coverage Report - com.puppycrawl.tools.checkstyle.checks.imports.CustomImportOrderCheck
 
Classes in this File Line Coverage Branch Coverage Complexity
CustomImportOrderCheck
100%
169/169
100%
100/100
2.7
CustomImportOrderCheck$ImportDetails
100%
10/10
N/A
2.7
CustomImportOrderCheck$RuleMatchForImport
100%
6/6
N/A
2.7
 
 1  
 ////////////////////////////////////////////////////////////////////////////////
 2  
 // checkstyle: Checks Java source code for adherence to a set of rules.
 3  
 // Copyright (C) 2001-2017 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  55
 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  55
     private final List<String> customImportOrderRules = new ArrayList<>();
 362  
 
 363  
     /** Contains objects with import attributes. */
 364  55
     private final List<ImportDetails> importToGroupList = new ArrayList<>();
 365  
 
 366  
     /** RegExp for SAME_PACKAGE group imports. */
 367  55
     private String samePackageDomainsRegExp = "";
 368  
 
 369  
     /** RegExp for STANDARD_JAVA_PACKAGE group imports. */
 370  55
     private Pattern standardPackageRegExp = Pattern.compile("^(java|javax)\\.");
 371  
 
 372  
     /** RegExp for THIRD_PARTY_PACKAGE group imports. */
 373  55
     private Pattern thirdPartyPackageRegExp = Pattern.compile(".*");
 374  
 
 375  
     /** RegExp for SPECIAL_IMPORTS group imports. */
 376  55
     private Pattern specialImportsRegExp = Pattern.compile("^$");
 377  
 
 378  
     /** Force empty line separator between import groups. */
 379  55
     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  55
     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  11
         standardPackageRegExp = regexp;
 394  11
     }
 395  
 
 396  
     /**
 397  
      * Sets thirdPartyRegExp specified by user.
 398  
      * @param regexp
 399  
      *        user value.
 400  
      */
 401  
     public final void setThirdPartyPackageRegExp(Pattern regexp) {
 402  13
         thirdPartyPackageRegExp = regexp;
 403  13
     }
 404  
 
 405  
     /**
 406  
      * Sets specialImportsRegExp specified by user.
 407  
      * @param regexp
 408  
      *        user value.
 409  
      */
 410  
     public final void setSpecialImportsRegExp(Pattern regexp) {
 411  15
         specialImportsRegExp = regexp;
 412  15
     }
 413  
 
 414  
     /**
 415  
      * Sets separateLineBetweenGroups specified by user.
 416  
      * @param value
 417  
      *        user value.
 418  
      */
 419  
     public final void setSeparateLineBetweenGroups(boolean value) {
 420  18
         separateLineBetweenGroups = value;
 421  18
     }
 422  
 
 423  
     /**
 424  
      * Sets sortImportsInGroupAlphabetically specified by user.
 425  
      * @param value
 426  
      *        user value.
 427  
      */
 428  
     public final void setSortImportsInGroupAlphabetically(boolean value) {
 429  28
         sortImportsInGroupAlphabetically = value;
 430  28
     }
 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  40
         customImportOrderRules.clear();
 440  135
         for (String currentState : GROUP_SEPARATOR_PATTERN.split(inputCustomImportOrder)) {
 441  99
             addRulesToList(currentState);
 442  
         }
 443  36
         customImportOrderRules.add(NON_GROUP_RULE_GROUP);
 444  36
     }
 445  
 
 446  
     @Override
 447  
     public int[] getDefaultTokens() {
 448  89
         return getRequiredTokens();
 449  
     }
 450  
 
 451  
     @Override
 452  
     public int[] getAcceptableTokens() {
 453  6
         return getRequiredTokens();
 454  
     }
 455  
 
 456  
     @Override
 457  
     public int[] getRequiredTokens() {
 458  185
         return new int[] {
 459  
             TokenTypes.IMPORT,
 460  
             TokenTypes.STATIC_IMPORT,
 461  
             TokenTypes.PACKAGE_DEF,
 462  
         };
 463  
     }
 464  
 
 465  
     @Override
 466  
     public void beginTree(DetailAST rootAST) {
 467  27
         importToGroupList.clear();
 468  27
     }
 469  
 
 470  
     @Override
 471  
     public void visitToken(DetailAST ast) {
 472  242
         if (ast.getType() == TokenTypes.PACKAGE_DEF) {
 473  24
             if (customImportOrderRules.contains(SAME_PACKAGE_RULE_GROUP)) {
 474  12
                 samePackageDomainsRegExp = createSamePackageRegexp(
 475  
                         samePackageMatchingDepth, ast);
 476  
             }
 477  
         }
 478  
         else {
 479  218
             final String importFullPath = getFullImportIdent(ast);
 480  218
             final int lineNo = ast.getLineNo();
 481  218
             final boolean isStatic = ast.getType() == TokenTypes.STATIC_IMPORT;
 482  436
             importToGroupList.add(new ImportDetails(importFullPath,
 483  218
                     lineNo, getImportGroup(isStatic, importFullPath),
 484  
                     isStatic));
 485  
         }
 486  242
     }
 487  
 
 488  
     @Override
 489  
     public void finishTree(DetailAST rootAST) {
 490  
 
 491  27
         if (!importToGroupList.isEmpty()) {
 492  24
             finishImportList();
 493  
         }
 494  27
     }
 495  
 
 496  
     /** Examine the order of all the imports and log any violations. */
 497  
     private void finishImportList() {
 498  24
         final ImportDetails firstImport = importToGroupList.get(0);
 499  48
         String currentGroup = getImportGroup(firstImport.isStaticImport(),
 500  24
                 firstImport.getImportFullPath());
 501  24
         int currentGroupNumber = customImportOrderRules.indexOf(currentGroup);
 502  24
         String previousImportFromCurrentGroup = null;
 503  
 
 504  24
         for (ImportDetails importObject : importToGroupList) {
 505  218
             final String importGroup = importObject.getImportGroup();
 506  218
             final String fullImportIdent = importObject.getImportFullPath();
 507  
 
 508  218
             if (getCountOfEmptyLinesBefore(importObject.getLineNumber()) > 1) {
 509  6
                 log(importObject.getLineNumber(), MSG_LINE_SEPARATOR, fullImportIdent);
 510  
             }
 511  218
             if (importGroup.equals(currentGroup)) {
 512  146
                 if (sortImportsInGroupAlphabetically
 513  
                         && previousImportFromCurrentGroup != null
 514  75
                         && compareImports(fullImportIdent, previousImportFromCurrentGroup) < 0) {
 515  35
                     log(importObject.getLineNumber(), MSG_LEX,
 516  
                             fullImportIdent, previousImportFromCurrentGroup);
 517  
                 }
 518  
                 else {
 519  111
                     previousImportFromCurrentGroup = fullImportIdent;
 520  
                 }
 521  
             }
 522  
             else {
 523  
                 //not the last group, last one is always NON_GROUP
 524  72
                 if (customImportOrderRules.size() > currentGroupNumber + 1) {
 525  47
                     final String nextGroup = getNextImportGroup(currentGroupNumber + 1);
 526  47
                     if (importGroup.equals(nextGroup)) {
 527  20
                         if (separateLineBetweenGroups
 528  18
                                 && getCountOfEmptyLinesBefore(importObject.getLineNumber()) == 0) {
 529  2
                             log(importObject.getLineNumber(), MSG_LINE_SEPARATOR, fullImportIdent);
 530  
                         }
 531  20
                         currentGroup = nextGroup;
 532  20
                         currentGroupNumber = customImportOrderRules.indexOf(nextGroup);
 533  20
                         previousImportFromCurrentGroup = fullImportIdent;
 534  
                     }
 535  
                     else {
 536  27
                         logWrongImportGroupOrder(importObject.getLineNumber(),
 537  
                                 importGroup, nextGroup, fullImportIdent);
 538  
                     }
 539  47
                 }
 540  
                 else {
 541  25
                     logWrongImportGroupOrder(importObject.getLineNumber(),
 542  
                             importGroup, currentGroup, fullImportIdent);
 543  
                 }
 544  
             }
 545  218
         }
 546  24
     }
 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  52
         if (NON_GROUP_RULE_GROUP.equals(importGroup)) {
 562  1
             log(currentImportLine, MSG_NONGROUP_IMPORT, fullImportIdent);
 563  
         }
 564  51
         else if (NON_GROUP_RULE_GROUP.equals(currentGroupNumber)) {
 565  27
             log(currentImportLine, MSG_NONGROUP_EXPECTED, importGroup, fullImportIdent);
 566  
         }
 567  
         else {
 568  24
             log(currentImportLine, MSG_ORDER, importGroup, currentGroupNumber, fullImportIdent);
 569  
         }
 570  52
     }
 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  47
         int nextGroupNumber = currentGroupNumber;
 581  
 
 582  58
         while (customImportOrderRules.size() > nextGroupNumber + 1) {
 583  53
             if (hasAnyImportInCurrentGroup(customImportOrderRules.get(nextGroupNumber))) {
 584  42
                 break;
 585  
             }
 586  11
             nextGroupNumber++;
 587  
         }
 588  47
         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  53
         boolean result = false;
 600  53
         for (ImportDetails currentImport : importToGroupList) {
 601  676
             if (currentGroup.equals(currentImport.getImportGroup())) {
 602  42
                 result = true;
 603  42
                 break;
 604  
             }
 605  634
         }
 606  53
         return result;
 607  
     }
 608  
 
 609  
     /**
 610  
      * Get import valid group.
 611  
      * @param isStatic
 612  
      *        is static import.
 613  
      * @param importPath
 614  
      *        full import path.
 615  
      * @return import valid group.
 616  
      */
 617  
     private String getImportGroup(boolean isStatic, String importPath) {
 618  242
         RuleMatchForImport bestMatch = new RuleMatchForImport(NON_GROUP_RULE_GROUP, 0, 0);
 619  242
         if (isStatic && customImportOrderRules.contains(STATIC_RULE_GROUP)) {
 620  36
             bestMatch.group = STATIC_RULE_GROUP;
 621  36
             bestMatch.matchLength = importPath.length();
 622  
         }
 623  206
         else if (customImportOrderRules.contains(SAME_PACKAGE_RULE_GROUP)) {
 624  127
             final String importPathTrimmedToSamePackageDepth =
 625  127
                     getFirstDomainsFromIdent(samePackageMatchingDepth, importPath);
 626  127
             if (samePackageDomainsRegExp.equals(importPathTrimmedToSamePackageDepth)) {
 627  26
                 bestMatch.group = SAME_PACKAGE_RULE_GROUP;
 628  26
                 bestMatch.matchLength = importPath.length();
 629  
             }
 630  
         }
 631  242
         if (bestMatch.group.equals(NON_GROUP_RULE_GROUP)) {
 632  180
             for (String group : customImportOrderRules) {
 633  588
                 if (STANDARD_JAVA_PACKAGE_RULE_GROUP.equals(group)) {
 634  102
                     bestMatch = findBetterPatternMatch(importPath,
 635  
                             STANDARD_JAVA_PACKAGE_RULE_GROUP, standardPackageRegExp, bestMatch);
 636  
                 }
 637  588
                 if (SPECIAL_IMPORTS_RULE_GROUP.equals(group)) {
 638  29
                     bestMatch = findBetterPatternMatch(importPath,
 639  
                             SPECIAL_IMPORTS_RULE_GROUP, specialImportsRegExp, bestMatch);
 640  
                 }
 641  588
             }
 642  
         }
 643  242
         if (bestMatch.group.equals(NON_GROUP_RULE_GROUP)
 644  97
                 && customImportOrderRules.contains(THIRD_PARTY_PACKAGE_RULE_GROUP)
 645  29
                 && thirdPartyPackageRegExp.matcher(importPath).find()) {
 646  27
             bestMatch.group = THIRD_PARTY_PACKAGE_RULE_GROUP;
 647  
         }
 648  242
         return bestMatch.group;
 649  
     }
 650  
 
 651  
     /** Tries to find better matching regular expression:
 652  
      * longer matching substring wins; in case of the same length,
 653  
      * lower position of matching substring wins.
 654  
      * @param importPath
 655  
      *      Full import identifier
 656  
      * @param group
 657  
      *      Import group we are trying to assign the import
 658  
      * @param regExp
 659  
      *      Regular expression for import group
 660  
      * @param currentBestMatch
 661  
      *      object with currently best match
 662  
      * @return better match (if found) or the same (currentBestMatch)
 663  
      */
 664  
     private static RuleMatchForImport findBetterPatternMatch(String importPath, String group,
 665  
             Pattern regExp, RuleMatchForImport currentBestMatch) {
 666  131
         RuleMatchForImport betterMatchCandidate = currentBestMatch;
 667  131
         final Matcher matcher = regExp.matcher(importPath);
 668  222
         while (matcher.find()) {
 669  91
             final int length = matcher.end() - matcher.start();
 670  91
             if (length > betterMatchCandidate.matchLength
 671  6
                     || length == betterMatchCandidate.matchLength
 672  4
                         && matcher.start() < betterMatchCandidate.matchPosition) {
 673  87
                 betterMatchCandidate = new RuleMatchForImport(group, length, matcher.start());
 674  
             }
 675  91
         }
 676  131
         return betterMatchCandidate;
 677  
     }
 678  
 
 679  
     /**
 680  
      * Checks compare two import paths.
 681  
      * @param import1
 682  
      *        current import.
 683  
      * @param import2
 684  
      *        previous import.
 685  
      * @return a negative integer, zero, or a positive integer as the
 686  
      *        specified String is greater than, equal to, or less
 687  
      *        than this String, ignoring case considerations.
 688  
      */
 689  
     private static int compareImports(String import1, String import2) {
 690  75
         int result = 0;
 691  75
         final String separator = "\\.";
 692  75
         final String[] import1Tokens = import1.split(separator);
 693  75
         final String[] import2Tokens = import2.split(separator);
 694  143
         for (int i = 0; i < import1Tokens.length && i != import2Tokens.length; i++) {
 695  140
             final String import1Token = import1Tokens[i];
 696  140
             final String import2Token = import2Tokens[i];
 697  140
             result = import1Token.compareTo(import2Token);
 698  140
             if (result != 0) {
 699  72
                 break;
 700  
             }
 701  
         }
 702  75
         return result;
 703  
     }
 704  
 
 705  
     /**
 706  
      * Counts empty lines before given.
 707  
      * @param lineNo
 708  
      *        Line number of current import.
 709  
      * @return count of empty lines before given.
 710  
      */
 711  
     private int getCountOfEmptyLinesBefore(int lineNo) {
 712  236
         int result = 0;
 713  236
         final String[] lines = getLines();
 714  
         //  [lineNo - 2] is the number of the previous line
 715  
         //  because the numbering starts from zero.
 716  236
         int lineBeforeIndex = lineNo - 2;
 717  307
         while (lineBeforeIndex >= 0
 718  305
                 && CommonUtils.isBlank(lines[lineBeforeIndex])) {
 719  71
             lineBeforeIndex--;
 720  71
             result++;
 721  
         }
 722  236
         return result;
 723  
     }
 724  
 
 725  
     /**
 726  
      * Forms import full path.
 727  
      * @param token
 728  
      *        current token.
 729  
      * @return full path or null.
 730  
      */
 731  
     private static String getFullImportIdent(DetailAST token) {
 732  231
         String ident = "";
 733  231
         if (token != null) {
 734  230
             ident = FullIdent.createFullIdent(token.findFirstToken(TokenTypes.DOT)).getText();
 735  
         }
 736  231
         return ident;
 737  
     }
 738  
 
 739  
     /**
 740  
      * Parses ordering rule and adds it to the list with rules.
 741  
      * @param ruleStr
 742  
      *        String with rule.
 743  
      */
 744  
     private void addRulesToList(String ruleStr) {
 745  99
         if (STATIC_RULE_GROUP.equals(ruleStr)
 746  78
                 || THIRD_PARTY_PACKAGE_RULE_GROUP.equals(ruleStr)
 747  58
                 || STANDARD_JAVA_PACKAGE_RULE_GROUP.equals(ruleStr)
 748  36
                 || SPECIAL_IMPORTS_RULE_GROUP.equals(ruleStr)) {
 749  78
             customImportOrderRules.add(ruleStr);
 750  
 
 751  
         }
 752  21
         else if (ruleStr.startsWith(SAME_PACKAGE_RULE_GROUP)) {
 753  
 
 754  40
             final String rule = ruleStr.substring(ruleStr.indexOf('(') + 1,
 755  20
                     ruleStr.indexOf(')'));
 756  20
             samePackageMatchingDepth = Integer.parseInt(rule);
 757  19
             if (samePackageMatchingDepth <= 0) {
 758  2
                 throw new IllegalArgumentException(
 759  
                         "SAME_PACKAGE rule parameter should be positive integer: " + ruleStr);
 760  
             }
 761  17
             customImportOrderRules.add(SAME_PACKAGE_RULE_GROUP);
 762  
 
 763  17
         }
 764  
         else {
 765  1
             throw new IllegalStateException("Unexpected rule: " + ruleStr);
 766  
         }
 767  95
     }
 768  
 
 769  
     /**
 770  
      * Creates samePackageDomainsRegExp of the first package domains.
 771  
      * @param firstPackageDomainsCount
 772  
      *        number of first package domains.
 773  
      * @param packageNode
 774  
      *        package node.
 775  
      * @return same package regexp.
 776  
      */
 777  
     private static String createSamePackageRegexp(int firstPackageDomainsCount,
 778  
              DetailAST packageNode) {
 779  12
         final String packageFullPath = getFullImportIdent(packageNode);
 780  12
         return getFirstDomainsFromIdent(firstPackageDomainsCount, packageFullPath);
 781  
     }
 782  
 
 783  
     /**
 784  
      * Extracts defined amount of domains from the left side of package/import identifier.
 785  
      * @param firstPackageDomainsCount
 786  
      *        number of first package domains.
 787  
      * @param packageFullPath
 788  
      *        full identifier containing path to package or imported object.
 789  
      * @return String with defined amount of domains or full identifier
 790  
      *        (if full identifier had less domain then specified)
 791  
      */
 792  
     private static String getFirstDomainsFromIdent(
 793  
             final int firstPackageDomainsCount, final String packageFullPath) {
 794  139
         final StringBuilder builder = new StringBuilder(256);
 795  139
         final StringTokenizer tokens = new StringTokenizer(packageFullPath, ".");
 796  139
         int count = firstPackageDomainsCount;
 797  
 
 798  595
         while (count > 0 && tokens.hasMoreTokens()) {
 799  456
             builder.append(tokens.nextToken()).append('.');
 800  456
             count--;
 801  
         }
 802  139
         return builder.toString();
 803  
     }
 804  
 
 805  
     /**
 806  
      * Contains import attributes as line number, import full path, import
 807  
      * group.
 808  
      * @author max
 809  
      */
 810  
     private static class ImportDetails {
 811  
         /** Import full path. */
 812  
         private final String importFullPath;
 813  
 
 814  
         /** Import line number. */
 815  
         private final int lineNumber;
 816  
 
 817  
         /** Import group. */
 818  
         private final String importGroup;
 819  
 
 820  
         /** Is static import. */
 821  
         private final boolean staticImport;
 822  
 
 823  
         /**
 824  
          * Initialise importFullPath, lineNumber, importGroup, staticImport.
 825  
          * @param importFullPath
 826  
          *        import full path.
 827  
          * @param lineNumber
 828  
          *        import line number.
 829  
          * @param importGroup
 830  
          *        import group.
 831  
          * @param staticImport
 832  
          *        if import is static.
 833  
          */
 834  
         ImportDetails(String importFullPath,
 835  218
                 int lineNumber, String importGroup, boolean staticImport) {
 836  218
             this.importFullPath = importFullPath;
 837  218
             this.lineNumber = lineNumber;
 838  218
             this.importGroup = importGroup;
 839  218
             this.staticImport = staticImport;
 840  218
         }
 841  
 
 842  
         /**
 843  
          * Get import full path variable.
 844  
          * @return import full path variable.
 845  
          */
 846  
         public String getImportFullPath() {
 847  242
             return importFullPath;
 848  
         }
 849  
 
 850  
         /**
 851  
          * Get import line number.
 852  
          * @return import line.
 853  
          */
 854  
         public int getLineNumber() {
 855  331
             return lineNumber;
 856  
         }
 857  
 
 858  
         /**
 859  
          * Get import group.
 860  
          * @return import group.
 861  
          */
 862  
         public String getImportGroup() {
 863  894
             return importGroup;
 864  
         }
 865  
 
 866  
         /**
 867  
          * Checks if import is static.
 868  
          * @return true, if import is static.
 869  
          */
 870  
         public boolean isStaticImport() {
 871  24
             return staticImport;
 872  
         }
 873  
     }
 874  
 
 875  
     /**
 876  
      * Contains matching attributes assisting in definition of "best matching"
 877  
      * group for import.
 878  
      * @author ivanov-alex
 879  
      */
 880  978
     private static class RuleMatchForImport {
 881  
         /** Position of matching string for current best match. */
 882  
         private final int matchPosition;
 883  
         /** Length of matching string for current best match. */
 884  
         private int matchLength;
 885  
         /** Import group for current best match. */
 886  
         private String group;
 887  
 
 888  
         /** Constructor to initialize the fields.
 889  
          * @param group
 890  
          *        Matched group.
 891  
          * @param length
 892  
          *        Matching length.
 893  
          * @param position
 894  
          *        Matching position.
 895  
          */
 896  329
         RuleMatchForImport(String group, int length, int position) {
 897  329
             this.group = group;
 898  329
             matchLength = length;
 899  329
             matchPosition = position;
 900  329
         }
 901  
     }
 902  
 }