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