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