001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2017 the original author or authors.
004//
005// This library is free software; you can redistribute it and/or
006// modify it under the terms of the GNU Lesser General Public
007// License as published by the Free Software Foundation; either
008// version 2.1 of the License, or (at your option) any later version.
009//
010// This library is distributed in the hope that it will be useful,
011// but WITHOUT ANY WARRANTY; without even the implied warranty of
012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
013// Lesser General Public License for more details.
014//
015// You should have received a copy of the GNU Lesser General Public
016// License along with this library; if not, write to the Free Software
017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
018////////////////////////////////////////////////////////////////////////////////
019
020package com.puppycrawl.tools.checkstyle.checks.imports;
021
022import java.util.ArrayList;
023import java.util.List;
024import java.util.StringTokenizer;
025import java.util.regex.Matcher;
026import java.util.regex.Pattern;
027
028import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
029import com.puppycrawl.tools.checkstyle.api.DetailAST;
030import com.puppycrawl.tools.checkstyle.api.FullIdent;
031import com.puppycrawl.tools.checkstyle.api.TokenTypes;
032
033/**
034 * <p>
035 * Checks that the groups of import declarations appear in the order specified
036 * by the user. If there is an import but its group is not specified in the
037 * configuration such an import should be placed at the end of the import list.
038 * </p>
039 * The rule consists of:
040 *
041 * <p>
042 * 1. STATIC group. This group sets the ordering of static imports.
043 * </p>
044 *
045 * <p>
046 * 2. SAME_PACKAGE(n) group. This group sets the ordering of the same package imports.
047 * Imports are considered on SAME_PACKAGE group if <b>n</b> first domains in package name
048 * and import name are identical.
049 * </p>
050 *
051 * <pre>
052 *{@code
053 *package java.util.concurrent.locks;
054 *
055 *import java.io.File;
056 *import java.util.*; //#1
057 *import java.util.List; //#2
058 *import java.util.StringTokenizer; //#3
059 *import java.util.concurrent.*; //#4
060 *import java.util.concurrent.AbstractExecutorService; //#5
061 *import java.util.concurrent.locks.LockSupport; //#6
062 *import java.util.regex.Pattern; //#7
063 *import java.util.regex.Matcher; //#8
064 *}
065 * </pre>
066 *
067 * <p>
068 * If we have SAME_PACKAGE(3) on configuration file,
069 * imports #4-6 will be considered as a SAME_PACKAGE group (java.util.concurrent.*,
070 * java.util.concurrent.AbstractExecutorService, java.util.concurrent.locks.LockSupport).
071 * SAME_PACKAGE(2) will include #1-8. SAME_PACKAGE(4) will include only #6.
072 * SAME_PACKAGE(5) will result in no imports assigned to SAME_PACKAGE group because
073 * actual package java.util.concurrent.locks has only 4 domains.
074 * </p>
075 *
076 * <p>
077 * 3. THIRD_PARTY_PACKAGE group. This group sets ordering of third party imports.
078 * Third party imports are all imports except STATIC,
079 * SAME_PACKAGE(n), STANDARD_JAVA_PACKAGE and SPECIAL_IMPORTS.
080 * </p>
081 *
082 * <p>
083 * 4. STANDARD_JAVA_PACKAGE group. By default this group sets ordering of standard java/javax
084 * imports.
085 * </p>
086 *
087 * <p>
088 * 5. SPECIAL_IMPORTS group. This group may contains some imports
089 * that have particular meaning for the user.
090 * </p>
091 *
092 * <p>
093 * NOTE!
094 * </p>
095 * <p>
096 * Use the separator '###' between rules.
097 * </p>
098 * <p>
099 * To set RegExps for THIRD_PARTY_PACKAGE and STANDARD_JAVA_PACKAGE groups use
100 * thirdPartyPackageRegExp and standardPackageRegExp options.
101 * </p>
102 * <p>
103 * Pretty often one import can match more than one group. For example, static import from standard
104 * package or regular expressions are configured to allow one import match multiple groups.
105 * In this case, group will be assigned according to priorities:
106 * </p>
107 * <ol>
108 * <li>
109 *    STATIC has top priority
110 * </li>
111 * <li>
112 *    SAME_PACKAGE has second priority
113 * </li>
114 * <li>
115 *    STANDARD_JAVA_PACKAGE and SPECIAL_IMPORTS will compete using "best match" rule: longer
116 *    matching substring wins; in case of the same length, lower position of matching substring
117 *    wins; if position is the same, order of rules in configuration solves the puzzle.
118 * </li>
119 * <li>
120 *    THIRD_PARTY has the least priority
121 * </li>
122 * </ol>
123 * <p>
124 *    Few examples to illustrate "best match":
125 * </p>
126 * <p>
127 *    1. patterns STANDARD_JAVA_PACKAGE = "Check", SPECIAL_IMPORTS="ImportOrderCheck" and input
128 *    file:
129 * </p>
130 * <pre>
131 *{@code
132 *import com.puppycrawl.tools.checkstyle.checks.imports.CustomImportOrderCheck;
133 *import com.puppycrawl.tools.checkstyle.checks.imports.ImportOrderCheck;}
134 * </pre>
135 * <p>
136 *    Result: imports will be assigned to SPECIAL_IMPORTS, because matching substring length is 16.
137 *    Matching substring for STANDARD_JAVA_PACKAGE is 5.
138 * </p>
139 * <p>
140 *    2. patterns STANDARD_JAVA_PACKAGE = "Check", SPECIAL_IMPORTS="Avoid" and file:
141 * </p>
142 * <pre>
143 *{@code
144 *import com.puppycrawl.tools.checkstyle.checks.imports.AvoidStarImportCheck;}
145 * </pre>
146 * <p>
147 *   Result: import will be assigned to SPECIAL_IMPORTS. Matching substring length is 5 for both
148 *   patterns. However, "Avoid" position is lower then "Check" position.
149 * </p>
150 *
151 * <pre>
152 *    Properties:
153 * </pre>
154 * <table summary="Properties" border="1">
155 *     <tr><th>name</th><th>Description</th><th>type</th><th>default value</th></tr>
156 *      <tr><td>customImportOrderRules</td><td>List of order declaration customizing by user.</td>
157 *          <td>string</td><td>null</td></tr>
158 *      <tr><td>standardPackageRegExp</td><td>RegExp for STANDARD_JAVA_PACKAGE group imports.</td>
159 *          <td>regular expression</td><td>^(java|javax)\.</td></tr>
160 *      <tr><td>thirdPartyPackageRegExp</td><td>RegExp for THIRD_PARTY_PACKAGE group imports.</td>
161 *          <td>regular expression</td><td>.*</td></tr>
162 *      <tr><td>specialImportsRegExp</td><td>RegExp for SPECIAL_IMPORTS group imports.</td>
163 *          <td>regular expression</td><td>^$</td></tr>
164 *      <tr><td>separateLineBetweenGroups</td><td>Force empty line separator between import groups.
165 *          </td><td>boolean</td><td>true</td></tr>
166 *      <tr><td>sortImportsInGroupAlphabetically</td><td>Force grouping alphabetically,
167 *          in ASCII sort order.</td><td>boolean</td><td>false</td></tr>
168 * </table>
169 *
170 * <p>
171 * For example:
172 * </p>
173 *        <p>To configure the check so that it matches default Eclipse formatter configuration
174 *        (tested on Kepler, Luna and Mars):</p>
175 *        <ul>
176 *          <li>group of static imports is on the top</li>
177 *          <li>groups of non-static imports: &quot;java&quot; and &quot;javax&quot; packages
178 *          first, then &quot;org&quot; and then all other imports</li>
179 *          <li>imports will be sorted in the groups</li>
180 *          <li>groups are separated by, at least, one blank line</li>
181 *        </ul>
182 * <pre>
183 * &lt;module name=&quot;CustomImportOrder&quot;&gt;
184 *    &lt;property name=&quot;customImportOrderRules&quot;
185 *        value=&quot;STATIC###STANDARD_JAVA_PACKAGE###SPECIAL_IMPORTS&quot;/&gt;
186 *    &lt;property name=&quot;specialImportsRegExp&quot; value=&quot;org&quot;/&gt;
187 *    &lt;property name=&quot;sortImportsInGroupAlphabetically&quot; value=&quot;true&quot;/&gt;
188 *    &lt;property name=&quot;separateLineBetweenGroups&quot; value=&quot;true&quot;/&gt;
189 * &lt;/module&gt;
190 * </pre>
191 *
192 *        <p>To configure the check so that it matches default IntelliJ IDEA formatter
193 *        configuration (tested on v14):</p>
194 *        <ul>
195 *          <li>group of static imports is on the bottom</li>
196 *          <li>groups of non-static imports: all imports except of &quot;javax&quot;
197 *          and &quot;java&quot;, then &quot;javax&quot; and &quot;java&quot;</li>
198 *          <li>imports will be sorted in the groups</li>
199 *          <li>groups are separated by, at least, one blank line</li>
200 *        </ul>
201 *
202 *        <p>
203 *        Note: &quot;separated&quot; option is disabled because IDEA default has blank line
204 *        between &quot;java&quot; and static imports, and no blank line between
205 *        &quot;javax&quot; and &quot;java&quot;
206 *        </p>
207 *
208 * <pre>
209 * &lt;module name=&quot;CustomImportOrder&quot;&gt;
210 *    &lt;property name=&quot;customImportOrderRules&quot;
211 *        value=&quot;THIRD_PARTY_PACKAGE###SPECIAL_IMPORTS###STANDARD_JAVA_PACKAGE
212 *        ###STATIC&quot;/&gt;
213 *    &lt;property name=&quot;specialImportsRegExp&quot; value=&quot;^javax\.&quot;/&gt;
214 *    &lt;property name=&quot;standardPackageRegExp&quot; value=&quot;^java\.&quot;/&gt;
215 *    &lt;property name=&quot;sortImportsInGroupAlphabetically&quot; value=&quot;true&quot;/&gt;
216 *    &lt;property name=&quot;separateLineBetweenGroups&quot; value=&quot;false&quot;/&gt;
217 *&lt;/module&gt;
218 * </pre>
219 *
220 * <p>To configure the check so that it matches default NetBeans formatter
221 *    configuration (tested on v8):</p>
222 * <ul>
223 *     <li>groups of non-static imports are not defined, all imports will be sorted as a one
224 *         group</li>
225 *     <li>static imports are not separated, they will be sorted along with other imports</li>
226 * </ul>
227 *
228 * <pre>
229 *&lt;module name=&quot;CustomImportOrder&quot;/&gt;
230 * </pre>
231 * <p>To set RegExps for THIRD_PARTY_PACKAGE and STANDARD_JAVA_PACKAGE groups use
232 *         thirdPartyPackageRegExp and standardPackageRegExp options.</p>
233 * <pre>
234 * &lt;module name=&quot;CustomImportOrder&quot;&gt;
235 *    &lt;property name=&quot;customImportOrderRules&quot;
236 *    value=&quot;STATIC###SAME_PACKAGE(3)###THIRD_PARTY_PACKAGE###STANDARD_JAVA_PACKAGE&quot;/&gt;
237 *    &lt;property name=&quot;thirdPartyPackageRegExp&quot; value=&quot;com|org&quot;/&gt;
238 *    &lt;property name=&quot;standardPackageRegExp&quot; value=&quot;^(java|javax)\.&quot;/&gt;
239 * &lt;/module&gt;
240 * </pre>
241 * <p>
242 * Also, this check can be configured to force empty line separator between
243 * import groups. For example
244 * </p>
245 *
246 * <pre>
247 * &lt;module name=&quot;CustomImportOrder&quot;&gt;
248 *    &lt;property name=&quot;separateLineBetweenGroups&quot; value=&quot;true&quot;/&gt;
249 * &lt;/module&gt;
250 * </pre>
251 * <p>
252 * It is possible to enforce
253 * <a href="https://en.wikipedia.org/wiki/ASCII#Order">ASCII sort order</a>
254 * of imports in groups using the following configuration:
255 * </p>
256 * <pre>
257 * &lt;module name=&quot;CustomImportOrder&quot;&gt;
258 *    &lt;property name=&quot;sortImportsInGroupAlphabetically&quot; value=&quot;true&quot;/&gt;
259 * &lt;/module&gt;
260 * </pre>
261 * <p>
262 * Example of ASCII order:
263 * </p>
264 * <pre>
265 * {@code
266 *import java.awt.Dialog;
267 *import java.awt.Window;
268 *import java.awt.color.ColorSpace;
269 *import java.awt.Frame; // violation here - in ASCII order 'F' should go before 'c',
270 *                       // as all uppercase come before lowercase letters}
271 * </pre>
272 * <p>
273 * To force checking imports sequence such as:
274 * </p>
275 *
276 * <pre>
277 * {@code
278 * package com.puppycrawl.tools.checkstyle.imports;
279 *
280 * import com.google.common.annotations.GwtCompatible;
281 * import com.google.common.annotations.Beta;
282 * import com.google.common.annotations.VisibleForTesting;
283 *
284 * import org.abego.treelayout.Configuration;
285 *
286 * import static sun.tools.util.ModifierFilter.ALL_ACCESS;
287 *
288 * import com.google.common.annotations.GwtCompatible; // violation here - should be in the
289 *                                                     // THIRD_PARTY_PACKAGE group
290 * import android.*;}
291 * </pre>
292 * configure as follows:
293 * <pre>
294 * &lt;module name=&quot;CustomImportOrder&quot;&gt;
295 *    &lt;property name=&quot;customImportOrderRules&quot;
296 *    value=&quot;SAME_PACKAGE(3)###THIRD_PARTY_PACKAGE###STATIC###SPECIAL_IMPORTS&quot;/&gt;
297 *    &lt;property name=&quot;specialImportsRegExp&quot; value=&quot;android.*&quot;/&gt;
298 * &lt;/module&gt;
299 * </pre>
300 *
301 * @author maxvetrenko
302 * @author <a href="mailto:nesterenko-aleksey@list.ru">Aleksey Nesterenko</a>
303 */
304public class CustomImportOrderCheck extends AbstractCheck {
305
306    /**
307     * A key is pointing to the warning message text in "messages.properties"
308     * file.
309     */
310    public static final String MSG_LINE_SEPARATOR = "custom.import.order.line.separator";
311
312    /**
313     * A key is pointing to the warning message text in "messages.properties"
314     * file.
315     */
316    public static final String MSG_LEX = "custom.import.order.lex";
317
318    /**
319     * A key is pointing to the warning message text in "messages.properties"
320     * file.
321     */
322    public static final String MSG_NONGROUP_IMPORT = "custom.import.order.nonGroup.import";
323
324    /**
325     * A key is pointing to the warning message text in "messages.properties"
326     * file.
327     */
328    public static final String MSG_NONGROUP_EXPECTED = "custom.import.order.nonGroup.expected";
329
330    /**
331     * A key is pointing to the warning message text in "messages.properties"
332     * file.
333     */
334    public static final String MSG_ORDER = "custom.import.order";
335
336    /** STATIC group name. */
337    public static final String STATIC_RULE_GROUP = "STATIC";
338
339    /** SAME_PACKAGE group name. */
340    public static final String SAME_PACKAGE_RULE_GROUP = "SAME_PACKAGE";
341
342    /** THIRD_PARTY_PACKAGE group name. */
343    public static final String THIRD_PARTY_PACKAGE_RULE_GROUP = "THIRD_PARTY_PACKAGE";
344
345    /** STANDARD_JAVA_PACKAGE group name. */
346    public static final String STANDARD_JAVA_PACKAGE_RULE_GROUP = "STANDARD_JAVA_PACKAGE";
347
348    /** SPECIAL_IMPORTS group name. */
349    public static final String SPECIAL_IMPORTS_RULE_GROUP = "SPECIAL_IMPORTS";
350
351    /** NON_GROUP group name. */
352    private static final String NON_GROUP_RULE_GROUP = "NOT_ASSIGNED_TO_ANY_GROUP";
353
354    /** Pattern used to separate groups of imports. */
355    private static final Pattern GROUP_SEPARATOR_PATTERN = Pattern.compile("\\s*###\\s*");
356
357    /** List of order declaration customizing by user. */
358    private final List<String> customImportOrderRules = new ArrayList<>();
359
360    /** Contains objects with import attributes. */
361    private final List<ImportDetails> importToGroupList = new ArrayList<>();
362
363    /** RegExp for SAME_PACKAGE group imports. */
364    private String samePackageDomainsRegExp = "";
365
366    /** RegExp for STANDARD_JAVA_PACKAGE group imports. */
367    private Pattern standardPackageRegExp = Pattern.compile("^(java|javax)\\.");
368
369    /** RegExp for THIRD_PARTY_PACKAGE group imports. */
370    private Pattern thirdPartyPackageRegExp = Pattern.compile(".*");
371
372    /** RegExp for SPECIAL_IMPORTS group imports. */
373    private Pattern specialImportsRegExp = Pattern.compile("^$");
374
375    /** Force empty line separator between import groups. */
376    private boolean separateLineBetweenGroups = true;
377
378    /** Force grouping alphabetically, in ASCII order. */
379    private boolean sortImportsInGroupAlphabetically;
380
381    /** Number of first domains for SAME_PACKAGE group. */
382    private int samePackageMatchingDepth = 2;
383
384    /**
385     * Sets standardRegExp specified by user.
386     * @param regexp
387     *        user value.
388     */
389    public final void setStandardPackageRegExp(Pattern regexp) {
390        standardPackageRegExp = regexp;
391    }
392
393    /**
394     * Sets thirdPartyRegExp specified by user.
395     * @param regexp
396     *        user value.
397     */
398    public final void setThirdPartyPackageRegExp(Pattern regexp) {
399        thirdPartyPackageRegExp = regexp;
400    }
401
402    /**
403     * Sets specialImportsRegExp specified by user.
404     * @param regexp
405     *        user value.
406     */
407    public final void setSpecialImportsRegExp(Pattern regexp) {
408        specialImportsRegExp = regexp;
409    }
410
411    /**
412     * Sets separateLineBetweenGroups specified by user.
413     * @param value
414     *        user value.
415     */
416    public final void setSeparateLineBetweenGroups(boolean value) {
417        separateLineBetweenGroups = value;
418    }
419
420    /**
421     * Sets sortImportsInGroupAlphabetically specified by user.
422     * @param value
423     *        user value.
424     */
425    public final void setSortImportsInGroupAlphabetically(boolean value) {
426        sortImportsInGroupAlphabetically = value;
427    }
428
429    /**
430     * Sets a custom import order from the rules in the string format specified
431     * by user.
432     * @param inputCustomImportOrder
433     *        user value.
434     */
435    public final void setCustomImportOrderRules(final String inputCustomImportOrder) {
436        customImportOrderRules.clear();
437        for (String currentState : GROUP_SEPARATOR_PATTERN.split(inputCustomImportOrder)) {
438            addRulesToList(currentState);
439        }
440        customImportOrderRules.add(NON_GROUP_RULE_GROUP);
441    }
442
443    @Override
444    public int[] getDefaultTokens() {
445        return getAcceptableTokens();
446    }
447
448    @Override
449    public int[] getAcceptableTokens() {
450        return new int[] {
451            TokenTypes.IMPORT,
452            TokenTypes.STATIC_IMPORT,
453            TokenTypes.PACKAGE_DEF,
454        };
455    }
456
457    @Override
458    public int[] getRequiredTokens() {
459        return getAcceptableTokens();
460    }
461
462    @Override
463    public void beginTree(DetailAST rootAST) {
464        importToGroupList.clear();
465    }
466
467    @Override
468    public void visitToken(DetailAST ast) {
469        if (ast.getType() == TokenTypes.PACKAGE_DEF) {
470            if (customImportOrderRules.contains(SAME_PACKAGE_RULE_GROUP)) {
471                samePackageDomainsRegExp = createSamePackageRegexp(
472                        samePackageMatchingDepth, ast);
473            }
474        }
475        else {
476            final String importFullPath = getFullImportIdent(ast);
477            final int lineNo = ast.getLineNo();
478            final boolean isStatic = ast.getType() == TokenTypes.STATIC_IMPORT;
479            importToGroupList.add(new ImportDetails(importFullPath,
480                    lineNo, getImportGroup(isStatic, importFullPath),
481                    isStatic));
482        }
483    }
484
485    @Override
486    public void finishTree(DetailAST rootAST) {
487
488        if (!importToGroupList.isEmpty()) {
489            finishImportList();
490        }
491    }
492
493    /** Examine the order of all the imports and log any violations. */
494    private void finishImportList() {
495        final ImportDetails firstImport = importToGroupList.get(0);
496        String currentGroup = getImportGroup(firstImport.isStaticImport(),
497                firstImport.getImportFullPath());
498        int currentGroupNumber = customImportOrderRules.indexOf(currentGroup);
499        String previousImportFromCurrentGroup = null;
500
501        for (ImportDetails importObject : importToGroupList) {
502            final String importGroup = importObject.getImportGroup();
503            final String fullImportIdent = importObject.getImportFullPath();
504
505            if (getCountOfEmptyLinesBefore(importObject.getLineNumber()) > 1) {
506                log(importObject.getLineNumber(), MSG_LINE_SEPARATOR, fullImportIdent);
507            }
508            if (importGroup.equals(currentGroup)) {
509                if (sortImportsInGroupAlphabetically
510                        && previousImportFromCurrentGroup != null
511                        && compareImports(fullImportIdent, previousImportFromCurrentGroup) < 0) {
512                    log(importObject.getLineNumber(), MSG_LEX,
513                            fullImportIdent, previousImportFromCurrentGroup);
514                }
515                else {
516                    previousImportFromCurrentGroup = fullImportIdent;
517                }
518            }
519            else {
520                //not the last group, last one is always NON_GROUP
521                if (customImportOrderRules.size() > currentGroupNumber + 1) {
522                    final String nextGroup = getNextImportGroup(currentGroupNumber + 1);
523                    if (importGroup.equals(nextGroup)) {
524                        if (separateLineBetweenGroups
525                                && getCountOfEmptyLinesBefore(importObject.getLineNumber()) == 0) {
526                            log(importObject.getLineNumber(), MSG_LINE_SEPARATOR, fullImportIdent);
527                        }
528                        currentGroup = nextGroup;
529                        currentGroupNumber = customImportOrderRules.indexOf(nextGroup);
530                        previousImportFromCurrentGroup = fullImportIdent;
531                    }
532                    else {
533                        logWrongImportGroupOrder(importObject.getLineNumber(),
534                                importGroup, nextGroup, fullImportIdent);
535                    }
536                }
537                else {
538                    logWrongImportGroupOrder(importObject.getLineNumber(),
539                            importGroup, currentGroup, fullImportIdent);
540                }
541            }
542        }
543    }
544
545    /**
546     * Log wrong import group order.
547     * @param currentImportLine
548     *        line number of current import current import.
549     * @param importGroup
550     *        import group.
551     * @param currentGroupNumber
552     *        current group number we are checking.
553     * @param fullImportIdent
554     *        full import name.
555     */
556    private void logWrongImportGroupOrder(int currentImportLine, String importGroup,
557            String currentGroupNumber, String fullImportIdent) {
558        if (NON_GROUP_RULE_GROUP.equals(importGroup)) {
559            log(currentImportLine, MSG_NONGROUP_IMPORT, fullImportIdent);
560        }
561        else if (NON_GROUP_RULE_GROUP.equals(currentGroupNumber)) {
562            log(currentImportLine, MSG_NONGROUP_EXPECTED, importGroup, fullImportIdent);
563        }
564        else {
565            log(currentImportLine, MSG_ORDER, importGroup, currentGroupNumber, fullImportIdent);
566        }
567    }
568
569    /**
570     * Get next import group.
571     * @param currentGroupNumber
572     *        current group number.
573     * @return
574     *        next import group.
575     */
576    private String getNextImportGroup(int currentGroupNumber) {
577        int nextGroupNumber = currentGroupNumber;
578
579        while (customImportOrderRules.size() > nextGroupNumber + 1) {
580            if (hasAnyImportInCurrentGroup(customImportOrderRules.get(nextGroupNumber))) {
581                break;
582            }
583            nextGroupNumber++;
584        }
585        return customImportOrderRules.get(nextGroupNumber);
586    }
587
588    /**
589     * Checks if current group contains any import.
590     * @param currentGroup
591     *        current group.
592     * @return
593     *        true, if current group contains at least one import.
594     */
595    private boolean hasAnyImportInCurrentGroup(String currentGroup) {
596        boolean result = false;
597        for (ImportDetails currentImport : importToGroupList) {
598            if (currentGroup.equals(currentImport.getImportGroup())) {
599                result = true;
600                break;
601            }
602        }
603        return result;
604    }
605
606    /**
607     * Get import valid group.
608     * @param isStatic
609     *        is static import.
610     * @param importPath
611     *        full import path.
612     * @return import valid group.
613     */
614    private String getImportGroup(boolean isStatic, String importPath) {
615        RuleMatchForImport bestMatch = new RuleMatchForImport(NON_GROUP_RULE_GROUP, 0, 0);
616        if (isStatic && customImportOrderRules.contains(STATIC_RULE_GROUP)) {
617            bestMatch.group = STATIC_RULE_GROUP;
618            bestMatch.matchLength = importPath.length();
619        }
620        else if (customImportOrderRules.contains(SAME_PACKAGE_RULE_GROUP)) {
621            final String importPathTrimmedToSamePackageDepth =
622                    getFirstDomainsFromIdent(samePackageMatchingDepth, importPath);
623            if (samePackageDomainsRegExp.equals(importPathTrimmedToSamePackageDepth)) {
624                bestMatch.group = SAME_PACKAGE_RULE_GROUP;
625                bestMatch.matchLength = importPath.length();
626            }
627        }
628        if (bestMatch.group.equals(NON_GROUP_RULE_GROUP)) {
629            for (String group : customImportOrderRules) {
630                if (STANDARD_JAVA_PACKAGE_RULE_GROUP.equals(group)) {
631                    bestMatch = findBetterPatternMatch(importPath,
632                            STANDARD_JAVA_PACKAGE_RULE_GROUP, standardPackageRegExp, bestMatch);
633                }
634                if (SPECIAL_IMPORTS_RULE_GROUP.equals(group)) {
635                    bestMatch = findBetterPatternMatch(importPath,
636                            SPECIAL_IMPORTS_RULE_GROUP, specialImportsRegExp, bestMatch);
637                }
638            }
639        }
640        if (bestMatch.group.equals(NON_GROUP_RULE_GROUP)
641                && customImportOrderRules.contains(THIRD_PARTY_PACKAGE_RULE_GROUP)
642                && thirdPartyPackageRegExp.matcher(importPath).find()) {
643            bestMatch.group = THIRD_PARTY_PACKAGE_RULE_GROUP;
644        }
645        return bestMatch.group;
646    }
647
648    /** Tries to find better matching regular expression:
649     * longer matching substring wins; in case of the same length,
650     * lower position of matching substring wins.
651     * @param importPath
652     *      Full import identifier
653     * @param group
654     *      Import group we are trying to assign the import
655     * @param regExp
656     *      Regular expression for import group
657     * @param currentBestMatch
658     *      object with currently best match
659     * @return better match (if found) or the same (currentBestMatch)
660     */
661    private static RuleMatchForImport findBetterPatternMatch(String importPath, String group,
662            Pattern regExp, RuleMatchForImport currentBestMatch) {
663        RuleMatchForImport betterMatchCandidate = currentBestMatch;
664        final Matcher matcher = regExp.matcher(importPath);
665        while (matcher.find()) {
666            final int length = matcher.end() - matcher.start();
667            if (length > betterMatchCandidate.matchLength
668                    || length == betterMatchCandidate.matchLength
669                        && matcher.start() < betterMatchCandidate.matchPosition) {
670                betterMatchCandidate = new RuleMatchForImport(group, length, matcher.start());
671            }
672        }
673        return betterMatchCandidate;
674    }
675
676    /**
677     * Checks compare two import paths.
678     * @param import1
679     *        current import.
680     * @param import2
681     *        previous import.
682     * @return a negative integer, zero, or a positive integer as the
683     *        specified String is greater than, equal to, or less
684     *        than this String, ignoring case considerations.
685     */
686    private static int compareImports(String import1, String import2) {
687        int result = 0;
688        final String separator = "\\.";
689        final String[] import1Tokens = import1.split(separator);
690        final String[] import2Tokens = import2.split(separator);
691        for (int i = 0; i < import1Tokens.length && i != import2Tokens.length; i++) {
692            final String import1Token = import1Tokens[i];
693            final String import2Token = import2Tokens[i];
694            result = import1Token.compareTo(import2Token);
695            if (result != 0) {
696                break;
697            }
698        }
699        return result;
700    }
701
702    /**
703     * Counts empty lines before given.
704     * @param lineNo
705     *        Line number of current import.
706     * @return count of empty lines before given.
707     */
708    private int getCountOfEmptyLinesBefore(int lineNo) {
709        int result = 0;
710        final String[] lines = getLines();
711        //  [lineNo - 2] is the number of the previous line
712        //  because the numbering starts from zero.
713        int lineBeforeIndex = lineNo - 2;
714        while (lineBeforeIndex >= 0 && lines[lineBeforeIndex].trim().isEmpty()) {
715            lineBeforeIndex--;
716            result++;
717        }
718        return result;
719    }
720
721    /**
722     * Forms import full path.
723     * @param token
724     *        current token.
725     * @return full path or null.
726     */
727    private static String getFullImportIdent(DetailAST token) {
728        String ident = "";
729        if (token != null) {
730            ident = FullIdent.createFullIdent(token.findFirstToken(TokenTypes.DOT)).getText();
731        }
732        return ident;
733    }
734
735    /**
736     * Parses ordering rule and adds it to the list with rules.
737     * @param ruleStr
738     *        String with rule.
739     */
740    private void addRulesToList(String ruleStr) {
741        if (STATIC_RULE_GROUP.equals(ruleStr)
742                || THIRD_PARTY_PACKAGE_RULE_GROUP.equals(ruleStr)
743                || STANDARD_JAVA_PACKAGE_RULE_GROUP.equals(ruleStr)
744                || SPECIAL_IMPORTS_RULE_GROUP.equals(ruleStr)) {
745            customImportOrderRules.add(ruleStr);
746
747        }
748        else if (ruleStr.startsWith(SAME_PACKAGE_RULE_GROUP)) {
749
750            final String rule = ruleStr.substring(ruleStr.indexOf('(') + 1,
751                    ruleStr.indexOf(')'));
752            samePackageMatchingDepth = Integer.parseInt(rule);
753            if (samePackageMatchingDepth <= 0) {
754                throw new IllegalArgumentException(
755                        "SAME_PACKAGE rule parameter should be positive integer: " + ruleStr);
756            }
757            customImportOrderRules.add(SAME_PACKAGE_RULE_GROUP);
758
759        }
760        else {
761            throw new IllegalStateException("Unexpected rule: " + ruleStr);
762        }
763    }
764
765    /**
766     * Creates samePackageDomainsRegExp of the first package domains.
767     * @param firstPackageDomainsCount
768     *        number of first package domains.
769     * @param packageNode
770     *        package node.
771     * @return same package regexp.
772     */
773    private static String createSamePackageRegexp(int firstPackageDomainsCount,
774             DetailAST packageNode) {
775        final String packageFullPath = getFullImportIdent(packageNode);
776        return getFirstDomainsFromIdent(firstPackageDomainsCount, packageFullPath);
777    }
778
779    /**
780     * Extracts defined amount of domains from the left side of package/import identifier
781     * @param firstPackageDomainsCount
782     *        number of first package domains.
783     * @param packageFullPath
784     *        full identifier containing path to package or imported object.
785     * @return String with defined amount of domains or full identifier
786     *        (if full identifier had less domain then specified)
787     */
788    private static String getFirstDomainsFromIdent(
789            final int firstPackageDomainsCount, final String packageFullPath) {
790        final StringBuilder builder = new StringBuilder();
791        final StringTokenizer tokens = new StringTokenizer(packageFullPath, ".");
792        int count = firstPackageDomainsCount;
793
794        while (count > 0 && tokens.hasMoreTokens()) {
795            builder.append(tokens.nextToken()).append('.');
796            count--;
797        }
798        return builder.toString();
799    }
800
801    /**
802     * Contains import attributes as line number, import full path, import
803     * group.
804     * @author max
805     */
806    private static class ImportDetails {
807        /** Import full path. */
808        private final String importFullPath;
809
810        /** Import line number. */
811        private final int lineNumber;
812
813        /** Import group. */
814        private final String importGroup;
815
816        /** Is static import. */
817        private final boolean staticImport;
818
819        /**
820         * @param importFullPath
821         *        import full path.
822         * @param lineNumber
823         *        import line number.
824         * @param importGroup
825         *        import group.
826         * @param staticImport
827         *        if import is static.
828         */
829        ImportDetails(String importFullPath,
830                int lineNumber, String importGroup, boolean staticImport) {
831            this.importFullPath = importFullPath;
832            this.lineNumber = lineNumber;
833            this.importGroup = importGroup;
834            this.staticImport = staticImport;
835        }
836
837        /**
838         * Get import full path variable.
839         * @return import full path variable.
840         */
841        public String getImportFullPath() {
842            return importFullPath;
843        }
844
845        /**
846         * Get import line number.
847         * @return import line.
848         */
849        public int getLineNumber() {
850            return lineNumber;
851        }
852
853        /**
854         * Get import group.
855         * @return import group.
856         */
857        public String getImportGroup() {
858            return importGroup;
859        }
860
861        /**
862         * Checks if import is static.
863         * @return true, if import is static.
864         */
865        public boolean isStaticImport() {
866            return staticImport;
867        }
868    }
869
870    /**
871     * Contains matching attributes assisting in definition of "best matching"
872     * group for import.
873     * @author ivanov-alex
874     */
875    private static class RuleMatchForImport {
876        /** Position of matching string for current best match. */
877        private final int matchPosition;
878        /** Length of matching string for current best match. */
879        private int matchLength;
880        /** Import group for current best match. */
881        private String group;
882
883        /** Constructor to initialize the fields.
884         * @param group
885         *        Matched group.
886         * @param length
887         *        Matching length.
888         * @param position
889         *        Matching position.
890         */
891        RuleMatchForImport(String group, int length, int position) {
892            this.group = group;
893            matchLength = length;
894            matchPosition = position;
895        }
896    }
897}