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