001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2015 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.Check;
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>samePackageMatchingDepth</td><td>Number of first domains for SAME_PACKAGE group.
166 *          </td><td>Integer</td><td>2</td></tr>
167 *      <tr><td>separateLineBetweenGroups</td><td>Force empty line separator between import groups.
168 *          </td><td>boolean</td><td>true</td></tr>
169 *      <tr><td>sortImportsInGroupAlphabetically</td><td>Force grouping alphabetically,
170 *          in ASCII sort order.</td><td>boolean</td><td>false</td></tr>
171 * </table>
172 *
173 * <p>
174 * For example:
175 * </p>
176 *        <p>To configure the check so that it matches default Eclipse formatter configuration
177 *        (tested on Kepler, Luna and Mars):</p>
178 *        <ul>
179 *          <li>group of static imports is on the top</li>
180 *          <li>groups of non-static imports: &quot;java&quot; and &quot;javax&quot; packages
181 *          first, then &quot;org&quot; and then all other imports</li>
182 *          <li>imports will be sorted in the groups</li>
183 *          <li>groups are separated by, at least, one blank line</li>
184 *        </ul>
185 * <pre>
186 * &lt;module name=&quot;CustomImportOrder&quot;&gt;
187 *    &lt;property name=&quot;customImportOrderRules&quot;
188 *        value=&quot;STATIC###STANDARD_JAVA_PACKAGE###SPECIAL_IMPORTS&quot;/&gt;
189 *    &lt;property name=&quot;specialImportsRegExp&quot; value=&quot;org&quot;/&gt;
190 *    &lt;property name=&quot;sortImportsInGroupAlphabetically&quot; value=&quot;true&quot;/&gt;
191 *    &lt;property name=&quot;separateLineBetweenGroups&quot; value=&quot;true&quot;/&gt;
192 * &lt;/module&gt;
193 * </pre>
194 *
195 *        <p>To configure the check so that it matches default IntelliJ IDEA formatter
196 *        configuration (tested on v14):</p>
197 *        <ul>
198 *          <li>group of static imports is on the bottom</li>
199 *          <li>groups of non-static imports: all imports except of &quot;javax&quot;
200 *          and &quot;java&quot;, then &quot;javax&quot; and &quot;java&quot;</li>
201 *          <li>imports will be sorted in the groups</li>
202 *          <li>groups are separated by, at least, one blank line</li>
203 *        </ul>
204 *
205 *        <p>
206 *        Note: &quot;separated&quot; option is disabled because IDEA default has blank line
207 *        between &quot;java&quot; and static imports, and no blank line between
208 *        &quot;javax&quot; and &quot;java&quot;
209 *        </p>
210 *
211 * <pre>
212 * &lt;module name=&quot;CustomImportOrder&quot;&gt;
213 *    &lt;property name=&quot;customImportOrderRules&quot;
214 *        value=&quot;THIRD_PARTY_PACKAGE###SPECIAL_IMPORTS###STANDARD_JAVA_PACKAGE
215 *        ###STATIC&quot;/&gt;
216 *    &lt;property name=&quot;specialImportsRegExp&quot; value=&quot;^javax\.&quot;/&gt;
217 *    &lt;property name=&quot;standardPackageRegExp&quot; value=&quot;^java\.&quot;/&gt;
218 *    &lt;property name=&quot;sortImportsInGroupAlphabetically&quot; value=&quot;true&quot;/&gt;
219 *    &lt;property name=&quot;separateLineBetweenGroups&quot; value=&quot;false&quot;/&gt;
220 *&lt;/module&gt;
221 * </pre>
222 *
223 * <p>To configure the check so that it matches default NetBeans formatter
224 *    configuration (tested on v8):</p>
225 * <ul>
226 *     <li>groups of non-static imports are not defined, all imports will be sorted as a one
227 *         group</li>
228 *     <li>static imports are not separated, they will be sorted along with other imports</li>
229 * </ul>
230 *
231 * <pre>
232 *&lt;module name=&quot;CustomImportOrder&quot;/&gt;
233 * </pre>
234 * <p>To set RegExps for THIRD_PARTY_PACKAGE and STANDARD_JAVA_PACKAGE groups use
235 *         thirdPartyPackageRegExp and standardPackageRegExp options.</p>
236 * <pre>
237 * &lt;module name=&quot;CustomImportOrder&quot;&gt;
238 *    &lt;property name=&quot;customImportOrderRules&quot;
239 *    value=&quot;STATIC###SAME_PACKAGE(3)###THIRD_PARTY_PACKAGE###STANDARD_JAVA_PACKAGE&quot;/&gt;
240 *    &lt;property name=&quot;thirdPartyPackageRegExp&quot; value=&quot;com|org&quot;/&gt;
241 *    &lt;property name=&quot;standardPackageRegExp&quot; value=&quot;^(java|javax)\.&quot;/&gt;
242 * &lt;/module&gt;
243 * </pre>
244 * <p>
245 * Also, this check can be configured to force empty line separator between
246 * import groups. For example
247 * </p>
248 *
249 * <pre>
250 * &lt;module name=&quot;CustomImportOrder&quot;&gt;
251 *    &lt;property name=&quot;separateLineBetweenGroups&quot; value=&quot;true&quot;/&gt;
252 * &lt;/module&gt;
253 * </pre>
254 * <p>
255 * It is possible to enforce
256 * <a href="https://en.wikipedia.org/wiki/ASCII#Order">ASCII sort order</a>
257 * of imports in groups using the following configuration:
258 * </p>
259 * <pre>
260 * &lt;module name=&quot;CustomImportOrder&quot;&gt;
261 *    &lt;property name=&quot;sortImportsInGroupAlphabetically&quot; value=&quot;true&quot;/&gt;
262 * &lt;/module&gt;
263 * </pre>
264 * <p>
265 * Example of ASCII order:
266 * </p>
267 * <pre>
268 * {@code
269 *import java.awt.Dialog;
270 *import java.awt.Window;
271 *import java.awt.color.ColorSpace;
272 *import java.awt.Frame; // violation here - in ASCII order 'F' should go before 'c',
273 *                       // as all uppercase come before lowercase letters}
274 * </pre>
275 * <p>
276 * To force checking imports sequence such as:
277 * </p>
278 *
279 * <pre>
280 * {@code
281 * package com.puppycrawl.tools.checkstyle.imports;
282 *
283 * import com.google.common.annotations.GwtCompatible;
284 * import com.google.common.annotations.Beta;
285 * import com.google.common.annotations.VisibleForTesting;
286 *
287 * import org.abego.treelayout.Configuration;
288 *
289 * import static sun.tools.util.ModifierFilter.ALL_ACCESS;
290 *
291 * import com.google.common.annotations.GwtCompatible; // violation here - should be in the
292 *                                                     // THIRD_PARTY_PACKAGE group
293 * import android.*;}
294 * </pre>
295 * configure as follows:
296 * <pre>
297 * &lt;module name=&quot;CustomImportOrder&quot;&gt;
298 *    &lt;property name=&quot;customImportOrderRules&quot;
299 *    value=&quot;SAME_PACKAGE(3)###THIRD_PARTY_PACKAGE###STATIC###SPECIAL_IMPORTS&quot;/&gt;
300 *    &lt;property name=&quot;specialImportsRegExp&quot; value=&quot;android.*&quot;/&gt;
301 * &lt;/module&gt;
302 * </pre>
303 *
304 * @author maxvetrenko
305 * @author <a href="mailto:nesterenko-aleksey@list.ru">Aleksey Nesterenko</a>
306 */
307public class CustomImportOrderCheck extends Check {
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    /** RegExp for SAME_PACKAGE group imports. */
361    private String samePackageDomainsRegExp = "";
362
363    /** RegExp for STANDARD_JAVA_PACKAGE group imports. */
364    private Pattern standardPackageRegExp = Pattern.compile("^(java|javax)\\.");
365
366    /** RegExp for THIRDPARTY_PACKAGE group imports. */
367    private Pattern thirdPartyPackageRegExp = Pattern.compile(".*");
368
369    /** RegExp for SPECIAL_IMPORTS group imports. */
370    private Pattern specialImportsRegExp = Pattern.compile("^$");
371
372    /** Force empty line separator between import groups. */
373    private boolean separateLineBetweenGroups = true;
374
375    /** Force grouping alphabetically, in ASCII order. */
376    private boolean sortImportsInGroupAlphabetically;
377
378    /** List of order declaration customizing by user. */
379    private final List<String> customImportOrderRules = new ArrayList<>();
380
381    /** Number of first domains for SAME_PACKAGE group. */
382    private int samePackageMatchingDepth = 2;
383
384    /** Contains objects with import attributes. */
385    private final List<ImportDetails> importToGroupList = new ArrayList<>();
386
387    /**
388     * Sets standardRegExp specified by user.
389     * @param regexp
390     *        user value.
391     * @throws org.apache.commons.beanutils.ConversionException
392     *         if unable to create Pattern object.
393     */
394    public final void setStandardPackageRegExp(String regexp) {
395        standardPackageRegExp = CommonUtils.createPattern(regexp);
396    }
397
398    /**
399     * Sets thirdPartyRegExp specified by user.
400     * @param regexp
401     *        user value.
402     * @throws org.apache.commons.beanutils.ConversionException
403     *         if unable to create Pattern object.
404     */
405    public final void setThirdPartyPackageRegExp(String regexp) {
406        thirdPartyPackageRegExp = CommonUtils.createPattern(regexp);
407    }
408
409    /**
410     * Sets specialImportsRegExp specified by user.
411     * @param regexp
412     *        user value.
413     * @throws org.apache.commons.beanutils.ConversionException
414     *         if unable to create Pattern object.
415     */
416    public final void setSpecialImportsRegExp(String regexp) {
417        specialImportsRegExp = CommonUtils.createPattern(regexp);
418    }
419
420    /**
421     * Sets separateLineBetweenGroups specified by user.
422     * @param value
423     *        user value.
424     */
425    public final void setSeparateLineBetweenGroups(boolean value) {
426        separateLineBetweenGroups = value;
427    }
428
429    /**
430     * Sets sortImportsInGroupAlphabetically specified by user.
431     * @param value
432     *        user value.
433     */
434    public final void setSortImportsInGroupAlphabetically(boolean value) {
435        sortImportsInGroupAlphabetically = value;
436    }
437
438    /**
439     * Sets a custom import order from the rules in the string format specified
440     * by user.
441     * @param inputCustomImportOrder
442     *        user value.
443     */
444    public final void setCustomImportOrderRules(final String inputCustomImportOrder) {
445        customImportOrderRules.clear();
446        for (String currentState : GROUP_SEPARATOR_PATTERN.split(inputCustomImportOrder)) {
447            addRulesToList(currentState);
448        }
449        customImportOrderRules.add(NON_GROUP_RULE_GROUP);
450    }
451
452    @Override
453    public int[] getDefaultTokens() {
454        return getAcceptableTokens();
455    }
456
457    @Override
458    public int[] getAcceptableTokens() {
459        return new int[] {
460            TokenTypes.IMPORT,
461            TokenTypes.STATIC_IMPORT,
462            TokenTypes.PACKAGE_DEF,
463        };
464    }
465
466    @Override
467    public int[] getRequiredTokens() {
468        return getAcceptableTokens();
469    }
470
471    @Override
472    public void beginTree(DetailAST rootAST) {
473        importToGroupList.clear();
474    }
475
476    @Override
477    public void visitToken(DetailAST ast) {
478        if (ast.getType() == TokenTypes.PACKAGE_DEF) {
479            if (customImportOrderRules.contains(SAME_PACKAGE_RULE_GROUP)) {
480                samePackageDomainsRegExp = createSamePackageRegexp(
481                        samePackageMatchingDepth, ast);
482            }
483        }
484        else {
485            final String importFullPath = getFullImportIdent(ast);
486            final int lineNo = ast.getLineNo();
487            final boolean isStatic = ast.getType() == TokenTypes.STATIC_IMPORT;
488            importToGroupList.add(new ImportDetails(importFullPath,
489                    lineNo, getImportGroup(isStatic, importFullPath),
490                    isStatic));
491        }
492    }
493
494    @Override
495    public void finishTree(DetailAST rootAST) {
496
497        if (importToGroupList.isEmpty()) {
498            return;
499        }
500
501        final ImportDetails firstImport = importToGroupList.get(0);
502        String currentGroup = getImportGroup(firstImport.isStaticImport(),
503                firstImport.getImportFullPath());
504        int currentGroupNumber = customImportOrderRules.indexOf(currentGroup);
505        String previousImportFromCurrentGroup = null;
506
507        for (ImportDetails importObject : importToGroupList) {
508            final String importGroup = importObject.getImportGroup();
509            final String fullImportIdent = importObject.getImportFullPath();
510
511            if (importGroup.equals(currentGroup)) {
512                if (sortImportsInGroupAlphabetically
513                        && previousImportFromCurrentGroup != null
514                        && compareImports(fullImportIdent, previousImportFromCurrentGroup) < 0) {
515                    log(importObject.getLineNumber(), MSG_LEX,
516                            fullImportIdent, previousImportFromCurrentGroup);
517                }
518                else {
519                    previousImportFromCurrentGroup = fullImportIdent;
520                }
521            }
522            else {
523                //not the last group, last one is always NON_GROUP
524                if (customImportOrderRules.size() > currentGroupNumber + 1) {
525                    final String nextGroup = getNextImportGroup(currentGroupNumber + 1);
526                    if (importGroup.equals(nextGroup)) {
527                        if (separateLineBetweenGroups
528                                && !hasEmptyLineBefore(importObject.getLineNumber())) {
529                            log(importObject.getLineNumber(), MSG_LINE_SEPARATOR, fullImportIdent);
530                        }
531                        currentGroup = nextGroup;
532                        currentGroupNumber = customImportOrderRules.indexOf(nextGroup);
533                        previousImportFromCurrentGroup = fullImportIdent;
534                    }
535                    else {
536                        logWrongImportGroupOrder(importObject.getLineNumber(),
537                                importGroup, nextGroup, fullImportIdent);
538                    }
539                }
540                else {
541                    logWrongImportGroupOrder(importObject.getLineNumber(),
542                            importGroup, currentGroup, fullImportIdent);
543                }
544            }
545        }
546    }
547
548    /**
549     * Log wrong import group order.
550     * @param currentImportLine
551     *        line number of current import current import.
552     * @param importGroup
553     *        import group.
554     * @param currentGroupNumber
555     *        current group number we are checking.
556     * @param fullImportIdent
557     *        full import name.
558     */
559    private void logWrongImportGroupOrder(int currentImportLine, String importGroup,
560            String currentGroupNumber, String fullImportIdent) {
561        if (NON_GROUP_RULE_GROUP.equals(importGroup)) {
562            log(currentImportLine, MSG_NONGROUP_IMPORT, fullImportIdent);
563        }
564        else if (NON_GROUP_RULE_GROUP.equals(currentGroupNumber)) {
565            log(currentImportLine, MSG_NONGROUP_EXPECTED, importGroup, fullImportIdent);
566        }
567        else {
568            log(currentImportLine, MSG_ORDER, importGroup, currentGroupNumber, fullImportIdent);
569        }
570    }
571
572    /**
573     * Get next import group.
574     * @param currentGroupNumber
575     *        current group number.
576     * @return
577     *        next import group.
578     */
579    private String getNextImportGroup(int currentGroupNumber) {
580        int nextGroupNumber = currentGroupNumber;
581
582        while (customImportOrderRules.size() > nextGroupNumber + 1) {
583            if (hasAnyImportInCurrentGroup(customImportOrderRules.get(nextGroupNumber))) {
584                break;
585            }
586            nextGroupNumber++;
587        }
588        return customImportOrderRules.get(nextGroupNumber);
589    }
590
591    /**
592     * Checks if current group contains any import.
593     * @param currentGroup
594     *        current group.
595     * @return
596     *        true, if current group contains at least one import.
597     */
598    private boolean hasAnyImportInCurrentGroup(String currentGroup) {
599        for (ImportDetails currentImport : importToGroupList) {
600            if (currentGroup.equals(currentImport.getImportGroup())) {
601                return true;
602            }
603        }
604        return false;
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                    getFirstNDomainsFromIdent(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                            SPECIAL_IMPORTS_RULE_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        return result;
701    }
702
703    /**
704     * Checks if a token has a empty line before.
705     * @param lineNo
706     *        Line number of current import.
707     * @return true, if token have empty line before.
708     */
709    private boolean hasEmptyLineBefore(int lineNo) {
710        //  [lineNo - 2] is the number of the previous line
711        //  because the numbering starts from zero.
712        final String lineBefore = getLine(lineNo - 2);
713        return lineBefore.trim().isEmpty();
714    }
715
716    /**
717     * Forms import full path.
718     * @param token
719     *        current token.
720     * @return full path or null.
721     */
722    private static String getFullImportIdent(DetailAST token) {
723        if (token == null) {
724            return "";
725        }
726        else {
727            return FullIdent.createFullIdent(token.findFirstToken(TokenTypes.DOT)).getText();
728        }
729    }
730
731    /**
732     * Parses ordering rule and adds it to the list with rules.
733     * @param ruleStr
734     *        String with rule.
735     */
736    private void addRulesToList(String ruleStr) {
737        if (STATIC_RULE_GROUP.equals(ruleStr)
738                || THIRD_PARTY_PACKAGE_RULE_GROUP.equals(ruleStr)
739                || STANDARD_JAVA_PACKAGE_RULE_GROUP.equals(ruleStr)
740                || SPECIAL_IMPORTS_RULE_GROUP.equals(ruleStr)) {
741            customImportOrderRules.add(ruleStr);
742
743        }
744        else if (ruleStr.startsWith(SAME_PACKAGE_RULE_GROUP)) {
745
746            final String rule = ruleStr.substring(ruleStr.indexOf('(') + 1,
747                    ruleStr.indexOf(')'));
748            samePackageMatchingDepth = Integer.parseInt(rule);
749            if (samePackageMatchingDepth <= 0) {
750                throw new IllegalArgumentException(
751                        "SAME_PACKAGE rule parameter should be positive integer: " + ruleStr);
752            }
753            customImportOrderRules.add(SAME_PACKAGE_RULE_GROUP);
754
755        }
756        else {
757            throw new IllegalStateException("Unexpected rule: " + ruleStr);
758        }
759    }
760
761    /**
762     * Creates samePackageDomainsRegExp of the first package domains.
763     * @param firstPackageDomainsCount
764     *        number of first package domains.
765     * @param packageNode
766     *        package node.
767     * @return same package regexp.
768     */
769    private static String createSamePackageRegexp(int firstPackageDomainsCount,
770             DetailAST packageNode) {
771        final String packageFullPath = getFullImportIdent(packageNode);
772        return getFirstNDomainsFromIdent(firstPackageDomainsCount, packageFullPath);
773    }
774
775    /**
776     * Extracts defined amount of domains from the left side of package/import identifier
777     * @param firstPackageDomainsCount
778     *        number of first package domains.
779     * @param packageFullPath
780     *        full identifier containing path to package or imported object.
781     * @return String with defined amount of domains or full identifier
782     *        (if full identifier had less domain then specified)
783     */
784    private static String getFirstNDomainsFromIdent(
785            final int firstPackageDomainsCount, final String packageFullPath) {
786        final StringBuilder builder = new StringBuilder();
787        final StringTokenizer tokens = new StringTokenizer(packageFullPath, ".");
788        int count = firstPackageDomainsCount;
789
790        while (tokens.hasMoreTokens() && count > 0) {
791            builder.append(tokens.nextToken()).append('.');
792            count--;
793        }
794        return builder.toString();
795    }
796
797    /**
798     * Contains import attributes as line number, import full path, import
799     * group.
800     * @author max
801     */
802    private static class ImportDetails {
803        /** Import full path. */
804        private final String importFullPath;
805
806        /** Import line number. */
807        private final int lineNumber;
808
809        /** Import group. */
810        private final String importGroup;
811
812        /** Is static import. */
813        private final boolean staticImport;
814
815        /**
816         * @param importFullPath
817         *        import full path.
818         * @param lineNumber
819         *        import line number.
820         * @param importGroup
821         *        import group.
822         * @param staticImport
823         *        if import is static.
824         */
825        ImportDetails(String importFullPath,
826                int lineNumber, String importGroup, boolean staticImport) {
827            this.importFullPath = importFullPath;
828            this.lineNumber = lineNumber;
829            this.importGroup = importGroup;
830            this.staticImport = staticImport;
831        }
832
833        /**
834         * Get import full path variable.
835         * @return import full path variable.
836         */
837        public String getImportFullPath() {
838            return importFullPath;
839        }
840
841        /**
842         * Get import line number.
843         * @return import line.
844         */
845        public int getLineNumber() {
846            return lineNumber;
847        }
848
849        /**
850         * Get import group.
851         * @return import group.
852         */
853        public String getImportGroup() {
854            return importGroup;
855        }
856
857        /**
858         * Checks if import is static.
859         * @return true, if import is static.
860         */
861        public boolean isStaticImport() {
862            return staticImport;
863        }
864    }
865
866    /**
867     * Contains matching attributes assisting in definition of "best matching"
868     * group for import.
869     * @author ivanov-alex
870     */
871    private static class RuleMatchForImport {
872        /** Import group for current best match. */
873        private String group;
874        /** Length of matching string for current best match. */
875        private int matchLength;
876        /** Position of matching string for current best match. */
877        private final int matchPosition;
878
879        /** Constructor to initialize the fields.
880         * @param group
881         *        Matched group.
882         * @param length
883         *        Matching length.
884         * @param position
885         *        Matching position.
886         */
887        RuleMatchForImport(String group, int length, int position) {
888            this.group = group;
889            matchLength = length;
890            matchPosition = position;
891        }
892    }
893}