View Javadoc
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 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         if (!importToGroupList.isEmpty()) {
490             finishImportList();
491         }
492     }
493 
494     /** Examine the order of all the imports and log any violations. */
495     private void finishImportList() {
496         final ImportDetails firstImport = importToGroupList.get(0);
497         String currentGroup = getImportGroup(firstImport.isStaticImport(),
498                 firstImport.getImportFullPath());
499         int currentGroupNumber = customImportOrderRules.indexOf(currentGroup);
500         String previousImportFromCurrentGroup = null;
501 
502         for (ImportDetails importObject : importToGroupList) {
503             final String importGroup = importObject.getImportGroup();
504             final String fullImportIdent = importObject.getImportFullPath();
505 
506             if (getCountOfEmptyLinesBefore(importObject.getLineNumber()) > 1) {
507                 log(importObject.getLineNumber(), MSG_LINE_SEPARATOR, fullImportIdent);
508             }
509             if (importGroup.equals(currentGroup)) {
510                 if (sortImportsInGroupAlphabetically
511                         && previousImportFromCurrentGroup != null
512                         && compareImports(fullImportIdent, previousImportFromCurrentGroup) < 0) {
513                     log(importObject.getLineNumber(), MSG_LEX,
514                             fullImportIdent, previousImportFromCurrentGroup);
515                 }
516                 else {
517                     previousImportFromCurrentGroup = fullImportIdent;
518                 }
519             }
520             else {
521                 //not the last group, last one is always NON_GROUP
522                 if (customImportOrderRules.size() > currentGroupNumber + 1) {
523                     final String nextGroup = getNextImportGroup(currentGroupNumber + 1);
524                     if (importGroup.equals(nextGroup)) {
525                         if (separateLineBetweenGroups
526                                 && getCountOfEmptyLinesBefore(importObject.getLineNumber()) == 0) {
527                             log(importObject.getLineNumber(), MSG_LINE_SEPARATOR, fullImportIdent);
528                         }
529                         currentGroup = nextGroup;
530                         currentGroupNumber = customImportOrderRules.indexOf(nextGroup);
531                         previousImportFromCurrentGroup = fullImportIdent;
532                     }
533                     else {
534                         logWrongImportGroupOrder(importObject.getLineNumber(),
535                                 importGroup, nextGroup, fullImportIdent);
536                     }
537                 }
538                 else {
539                     logWrongImportGroupOrder(importObject.getLineNumber(),
540                             importGroup, currentGroup, fullImportIdent);
541                 }
542             }
543         }
544     }
545 
546     /**
547      * Log wrong import group order.
548      * @param currentImportLine
549      *        line number of current import current import.
550      * @param importGroup
551      *        import group.
552      * @param currentGroupNumber
553      *        current group number we are checking.
554      * @param fullImportIdent
555      *        full import name.
556      */
557     private void logWrongImportGroupOrder(int currentImportLine, String importGroup,
558             String currentGroupNumber, String fullImportIdent) {
559         if (NON_GROUP_RULE_GROUP.equals(importGroup)) {
560             log(currentImportLine, MSG_NONGROUP_IMPORT, fullImportIdent);
561         }
562         else if (NON_GROUP_RULE_GROUP.equals(currentGroupNumber)) {
563             log(currentImportLine, MSG_NONGROUP_EXPECTED, importGroup, fullImportIdent);
564         }
565         else {
566             log(currentImportLine, MSG_ORDER, importGroup, currentGroupNumber, fullImportIdent);
567         }
568     }
569 
570     /**
571      * Get next import group.
572      * @param currentGroupNumber
573      *        current group number.
574      * @return
575      *        next import group.
576      */
577     private String getNextImportGroup(int currentGroupNumber) {
578         int nextGroupNumber = currentGroupNumber;
579 
580         while (customImportOrderRules.size() > nextGroupNumber + 1) {
581             if (hasAnyImportInCurrentGroup(customImportOrderRules.get(nextGroupNumber))) {
582                 break;
583             }
584             nextGroupNumber++;
585         }
586         return customImportOrderRules.get(nextGroupNumber);
587     }
588 
589     /**
590      * Checks if current group contains any import.
591      * @param currentGroup
592      *        current group.
593      * @return
594      *        true, if current group contains at least one import.
595      */
596     private boolean hasAnyImportInCurrentGroup(String currentGroup) {
597         boolean result = false;
598         for (ImportDetails currentImport : importToGroupList) {
599             if (currentGroup.equals(currentImport.getImportGroup())) {
600                 result = true;
601                 break;
602             }
603         }
604         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         RuleMatchForImport bestMatch = new RuleMatchForImport(NON_GROUP_RULE_GROUP, 0, 0);
617         if (isStatic && customImportOrderRules.contains(STATIC_RULE_GROUP)) {
618             bestMatch.group = STATIC_RULE_GROUP;
619             bestMatch.matchLength = importPath.length();
620         }
621         else if (customImportOrderRules.contains(SAME_PACKAGE_RULE_GROUP)) {
622             final String importPathTrimmedToSamePackageDepth =
623                     getFirstDomainsFromIdent(samePackageMatchingDepth, importPath);
624             if (samePackageDomainsRegExp.equals(importPathTrimmedToSamePackageDepth)) {
625                 bestMatch.group = SAME_PACKAGE_RULE_GROUP;
626                 bestMatch.matchLength = importPath.length();
627             }
628         }
629         if (bestMatch.group.equals(NON_GROUP_RULE_GROUP)) {
630             for (String group : customImportOrderRules) {
631                 if (STANDARD_JAVA_PACKAGE_RULE_GROUP.equals(group)) {
632                     bestMatch = findBetterPatternMatch(importPath,
633                             STANDARD_JAVA_PACKAGE_RULE_GROUP, standardPackageRegExp, bestMatch);
634                 }
635                 if (SPECIAL_IMPORTS_RULE_GROUP.equals(group)) {
636                     bestMatch = findBetterPatternMatch(importPath,
637                             group, specialImportsRegExp, bestMatch);
638                 }
639             }
640         }
641         if (bestMatch.group.equals(NON_GROUP_RULE_GROUP)
642                 && customImportOrderRules.contains(THIRD_PARTY_PACKAGE_RULE_GROUP)
643                 && thirdPartyPackageRegExp.matcher(importPath).find()) {
644             bestMatch.group = THIRD_PARTY_PACKAGE_RULE_GROUP;
645         }
646         return bestMatch.group;
647     }
648 
649     /** Tries to find better matching regular expression:
650      * longer matching substring wins; in case of the same length,
651      * lower position of matching substring wins.
652      * @param importPath
653      *      Full import identifier
654      * @param group
655      *      Import group we are trying to assign the import
656      * @param regExp
657      *      Regular expression for import group
658      * @param currentBestMatch
659      *      object with currently best match
660      * @return better match (if found) or the same (currentBestMatch)
661      */
662     private static RuleMatchForImport findBetterPatternMatch(String importPath, String group,
663             Pattern regExp, RuleMatchForImport currentBestMatch) {
664         RuleMatchForImport betterMatchCandidate = currentBestMatch;
665         final Matcher matcher = regExp.matcher(importPath);
666         while (matcher.find()) {
667             final int length = matcher.end() - matcher.start();
668             if (length > betterMatchCandidate.matchLength
669                     || length == betterMatchCandidate.matchLength
670                         && matcher.start() < betterMatchCandidate.matchPosition) {
671                 betterMatchCandidate = new RuleMatchForImport(group, length, matcher.start());
672             }
673         }
674         return betterMatchCandidate;
675     }
676 
677     /**
678      * Checks compare two import paths.
679      * @param import1
680      *        current import.
681      * @param import2
682      *        previous import.
683      * @return a negative integer, zero, or a positive integer as the
684      *        specified String is greater than, equal to, or less
685      *        than this String, ignoring case considerations.
686      */
687     private static int compareImports(String import1, String import2) {
688         int result = 0;
689         final String separator = "\\.";
690         final String[] import1Tokens = import1.split(separator);
691         final String[] import2Tokens = import2.split(separator);
692         for (int i = 0; i != import1Tokens.length && i != import2Tokens.length; i++) {
693             final String import1Token = import1Tokens[i];
694             final String import2Token = import2Tokens[i];
695             result = import1Token.compareTo(import2Token);
696             if (result != 0) {
697                 break;
698             }
699         }
700         if (result == 0) {
701             result = Integer.compare(import1Tokens.length, import2Tokens.length);
702         }
703         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         int result = 0;
714         final String[] lines = getLines();
715         //  [lineNo - 2] is the number of the previous line
716         //  because the numbering starts from zero.
717         int lineBeforeIndex = lineNo - 2;
718         while (lineBeforeIndex >= 0
719                 && CommonUtils.isBlank(lines[lineBeforeIndex])) {
720             lineBeforeIndex--;
721             result++;
722         }
723         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         String ident = "";
734         if (token != null) {
735             ident = FullIdent.createFullIdent(token.findFirstToken(TokenTypes.DOT)).getText();
736         }
737         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         if (STATIC_RULE_GROUP.equals(ruleStr)
747                 || THIRD_PARTY_PACKAGE_RULE_GROUP.equals(ruleStr)
748                 || STANDARD_JAVA_PACKAGE_RULE_GROUP.equals(ruleStr)
749                 || SPECIAL_IMPORTS_RULE_GROUP.equals(ruleStr)) {
750             customImportOrderRules.add(ruleStr);
751         }
752         else if (ruleStr.startsWith(SAME_PACKAGE_RULE_GROUP)) {
753             final String rule = ruleStr.substring(ruleStr.indexOf('(') + 1,
754                     ruleStr.indexOf(')'));
755             samePackageMatchingDepth = Integer.parseInt(rule);
756             if (samePackageMatchingDepth <= 0) {
757                 throw new IllegalArgumentException(
758                         "SAME_PACKAGE rule parameter should be positive integer: " + ruleStr);
759             }
760             customImportOrderRules.add(SAME_PACKAGE_RULE_GROUP);
761         }
762         else {
763             throw new IllegalStateException("Unexpected rule: " + ruleStr);
764         }
765     }
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         final String packageFullPath = getFullImportIdent(packageNode);
778         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         final StringBuilder builder = new StringBuilder(256);
793         final StringTokenizer tokens = new StringTokenizer(packageFullPath, ".");
794         int count = firstPackageDomainsCount;
795 
796         while (count > 0 && tokens.hasMoreTokens()) {
797             builder.append(tokens.nextToken()).append('.');
798             count--;
799         }
800         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                 int lineNumber, String importGroup, boolean staticImport) {
835             this.importFullPath = importFullPath;
836             this.lineNumber = lineNumber;
837             this.importGroup = importGroup;
838             this.staticImport = staticImport;
839         }
840 
841         /**
842          * Get import full path variable.
843          * @return import full path variable.
844          */
845         public String getImportFullPath() {
846             return importFullPath;
847         }
848 
849         /**
850          * Get import line number.
851          * @return import line.
852          */
853         public int getLineNumber() {
854             return lineNumber;
855         }
856 
857         /**
858          * Get import group.
859          * @return import group.
860          */
861         public String getImportGroup() {
862             return importGroup;
863         }
864 
865         /**
866          * Checks if import is static.
867          * @return true, if import is static.
868          */
869         public boolean isStaticImport() {
870             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     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         RuleMatchForImport(String group, int length, int position) {
898             this.group = group;
899             matchLength = length;
900             matchPosition = position;
901         }
902 
903     }
904 
905 }