001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2020 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 format of order declaration
139 * customizing by user. 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_SEPARATED_IN_GROUP = "custom.import.order.separated.internally";
367
368    /**
369     * A key is pointing to the warning message text in "messages.properties"
370     * file.
371     */
372    public static final String MSG_LEX = "custom.import.order.lex";
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_IMPORT = "custom.import.order.nonGroup.import";
379
380    /**
381     * A key is pointing to the warning message text in "messages.properties"
382     * file.
383     */
384    public static final String MSG_NONGROUP_EXPECTED = "custom.import.order.nonGroup.expected";
385
386    /**
387     * A key is pointing to the warning message text in "messages.properties"
388     * file.
389     */
390    public static final String MSG_ORDER = "custom.import.order";
391
392    /** STATIC group name. */
393    public static final String STATIC_RULE_GROUP = "STATIC";
394
395    /** SAME_PACKAGE group name. */
396    public static final String SAME_PACKAGE_RULE_GROUP = "SAME_PACKAGE";
397
398    /** THIRD_PARTY_PACKAGE group name. */
399    public static final String THIRD_PARTY_PACKAGE_RULE_GROUP = "THIRD_PARTY_PACKAGE";
400
401    /** STANDARD_JAVA_PACKAGE group name. */
402    public static final String STANDARD_JAVA_PACKAGE_RULE_GROUP = "STANDARD_JAVA_PACKAGE";
403
404    /** SPECIAL_IMPORTS group name. */
405    public static final String SPECIAL_IMPORTS_RULE_GROUP = "SPECIAL_IMPORTS";
406
407    /** NON_GROUP group name. */
408    private static final String NON_GROUP_RULE_GROUP = "NOT_ASSIGNED_TO_ANY_GROUP";
409
410    /** Pattern used to separate groups of imports. */
411    private static final Pattern GROUP_SEPARATOR_PATTERN = Pattern.compile("\\s*###\\s*");
412
413    /** Processed list of import order rules. */
414    private final List<String> customOrderRules = new ArrayList<>();
415
416    /** Contains objects with import attributes. */
417    private final List<ImportDetails> importToGroupList = new ArrayList<>();
418
419    /** Specify format of order declaration customizing by user. */
420    private String customImportOrderRules = "";
421
422    /** Specify RegExp for SAME_PACKAGE group imports. */
423    private String samePackageDomainsRegExp = "";
424
425    /** Specify RegExp for STANDARD_JAVA_PACKAGE group imports. */
426    private Pattern standardPackageRegExp = Pattern.compile("^(java|javax)\\.");
427
428    /** Specify RegExp for THIRD_PARTY_PACKAGE group imports. */
429    private Pattern thirdPartyPackageRegExp = Pattern.compile(".*");
430
431    /** Specify RegExp for SPECIAL_IMPORTS group imports. */
432    private Pattern specialImportsRegExp = Pattern.compile("^$");
433
434    /** Force empty line separator between import groups. */
435    private boolean separateLineBetweenGroups = true;
436
437    /**
438     * Force grouping alphabetically,
439     * in <a href="https://en.wikipedia.org/wiki/ASCII#Order"> ASCII sort order</a>.
440     */
441    private boolean sortImportsInGroupAlphabetically;
442
443    /** Number of first domains for SAME_PACKAGE group. */
444    private int samePackageMatchingDepth = 2;
445
446    /**
447     * Setter to specify RegExp for STANDARD_JAVA_PACKAGE group imports.
448     *
449     * @param regexp
450     *        user value.
451     */
452    public final void setStandardPackageRegExp(Pattern regexp) {
453        standardPackageRegExp = regexp;
454    }
455
456    /**
457     * Setter to specify RegExp for THIRD_PARTY_PACKAGE group imports.
458     *
459     * @param regexp
460     *        user value.
461     */
462    public final void setThirdPartyPackageRegExp(Pattern regexp) {
463        thirdPartyPackageRegExp = regexp;
464    }
465
466    /**
467     * Setter to specify RegExp for SPECIAL_IMPORTS group imports.
468     *
469     * @param regexp
470     *        user value.
471     */
472    public final void setSpecialImportsRegExp(Pattern regexp) {
473        specialImportsRegExp = regexp;
474    }
475
476    /**
477     * Setter to force empty line separator between import groups.
478     *
479     * @param value
480     *        user value.
481     */
482    public final void setSeparateLineBetweenGroups(boolean value) {
483        separateLineBetweenGroups = value;
484    }
485
486    /**
487     * Setter to force grouping alphabetically, in
488     * <a href="https://en.wikipedia.org/wiki/ASCII#Order">ASCII sort order</a>.
489     *
490     * @param value
491     *        user value.
492     */
493    public final void setSortImportsInGroupAlphabetically(boolean value) {
494        sortImportsInGroupAlphabetically = value;
495    }
496
497    /**
498     * Setter to specify format of order declaration customizing by user.
499     *
500     * @param inputCustomImportOrder
501     *        user value.
502     */
503    public final void setCustomImportOrderRules(final String inputCustomImportOrder) {
504        if (!customImportOrderRules.equals(inputCustomImportOrder)) {
505            for (String currentState : GROUP_SEPARATOR_PATTERN.split(inputCustomImportOrder)) {
506                addRulesToList(currentState);
507            }
508            customOrderRules.add(NON_GROUP_RULE_GROUP);
509        }
510        customImportOrderRules = inputCustomImportOrder;
511    }
512
513    @Override
514    public int[] getDefaultTokens() {
515        return getRequiredTokens();
516    }
517
518    @Override
519    public int[] getAcceptableTokens() {
520        return getRequiredTokens();
521    }
522
523    @Override
524    public int[] getRequiredTokens() {
525        return new int[] {
526            TokenTypes.IMPORT,
527            TokenTypes.STATIC_IMPORT,
528            TokenTypes.PACKAGE_DEF,
529        };
530    }
531
532    @Override
533    public void beginTree(DetailAST rootAST) {
534        importToGroupList.clear();
535    }
536
537    @Override
538    public void visitToken(DetailAST ast) {
539        if (ast.getType() == TokenTypes.PACKAGE_DEF) {
540            samePackageDomainsRegExp = createSamePackageRegexp(
541                    samePackageMatchingDepth, ast);
542        }
543        else {
544            final String importFullPath = getFullImportIdent(ast);
545            final boolean isStatic = ast.getType() == TokenTypes.STATIC_IMPORT;
546            importToGroupList.add(new ImportDetails(importFullPath,
547                    getImportGroup(isStatic, importFullPath), isStatic, ast));
548        }
549    }
550
551    @Override
552    public void finishTree(DetailAST rootAST) {
553        if (!importToGroupList.isEmpty()) {
554            finishImportList();
555        }
556    }
557
558    /** Examine the order of all the imports and log any violations. */
559    private void finishImportList() {
560        String currentGroup = getFirstGroup();
561        int currentGroupNumber = customOrderRules.indexOf(currentGroup);
562        ImportDetails previousImportObjectFromCurrentGroup = null;
563        String previousImportFromCurrentGroup = null;
564
565        for (ImportDetails importObject : importToGroupList) {
566            final String importGroup = importObject.getImportGroup();
567            final String fullImportIdent = importObject.getImportFullPath();
568
569            if (importGroup.equals(currentGroup)) {
570                validateExtraEmptyLine(previousImportObjectFromCurrentGroup,
571                        importObject, fullImportIdent);
572                if (isAlphabeticalOrderBroken(previousImportFromCurrentGroup, fullImportIdent)) {
573                    log(importObject.getImportAST(), MSG_LEX,
574                            fullImportIdent, previousImportFromCurrentGroup);
575                }
576                else {
577                    previousImportFromCurrentGroup = fullImportIdent;
578                }
579                previousImportObjectFromCurrentGroup = importObject;
580            }
581            else {
582                // not the last group, last one is always NON_GROUP
583                if (customOrderRules.size() > currentGroupNumber + 1) {
584                    final String nextGroup = getNextImportGroup(currentGroupNumber + 1);
585                    if (importGroup.equals(nextGroup)) {
586                        validateMissedEmptyLine(previousImportObjectFromCurrentGroup,
587                                importObject, fullImportIdent);
588                        currentGroup = nextGroup;
589                        currentGroupNumber = customOrderRules.indexOf(nextGroup);
590                        previousImportFromCurrentGroup = fullImportIdent;
591                    }
592                    else {
593                        logWrongImportGroupOrder(importObject.getImportAST(),
594                                importGroup, nextGroup, fullImportIdent);
595                    }
596                    previousImportObjectFromCurrentGroup = importObject;
597                }
598                else {
599                    logWrongImportGroupOrder(importObject.getImportAST(),
600                            importGroup, currentGroup, fullImportIdent);
601                }
602            }
603        }
604    }
605
606    /**
607     * Log violation if empty line is missed.
608     *
609     * @param previousImport previous import from current group.
610     * @param importObject current import.
611     * @param fullImportIdent full import identifier.
612     */
613    private void validateMissedEmptyLine(ImportDetails previousImport,
614                                         ImportDetails importObject, String fullImportIdent) {
615        if (isEmptyLineMissed(previousImport, importObject)) {
616            log(importObject.getImportAST(), MSG_LINE_SEPARATOR, fullImportIdent);
617        }
618    }
619
620    /**
621     * Log violation if extra empty line is present.
622     *
623     * @param previousImport previous import from current group.
624     * @param importObject current import.
625     * @param fullImportIdent full import identifier.
626     */
627    private void validateExtraEmptyLine(ImportDetails previousImport,
628                                        ImportDetails importObject, String fullImportIdent) {
629        if (isSeparatedByExtraEmptyLine(previousImport, importObject)) {
630            log(importObject.getImportAST(), MSG_SEPARATED_IN_GROUP, fullImportIdent);
631        }
632    }
633
634    /**
635     * Get first import group.
636     *
637     * @return
638     *        first import group of file.
639     */
640    private String getFirstGroup() {
641        final ImportDetails firstImport = importToGroupList.get(0);
642        return getImportGroup(firstImport.isStaticImport(),
643                firstImport.getImportFullPath());
644    }
645
646    /**
647     * Examine alphabetical order of imports.
648     *
649     * @param previousImport
650     *        previous import of current group.
651     * @param currentImport
652     *        current import.
653     * @return
654     *        true, if previous and current import are not in alphabetical order.
655     */
656    private boolean isAlphabeticalOrderBroken(String previousImport,
657                                              String currentImport) {
658        return sortImportsInGroupAlphabetically
659                && previousImport != null
660                && compareImports(currentImport, previousImport) < 0;
661    }
662
663    /**
664     * Examine empty lines between groups.
665     *
666     * @param previousImportObject
667     *        previous import in current group.
668     * @param currentImportObject
669     *        current import.
670     * @return
671     *        true, if current import NOT separated from previous import by empty line.
672     */
673    private boolean isEmptyLineMissed(ImportDetails previousImportObject,
674                                      ImportDetails currentImportObject) {
675        return separateLineBetweenGroups
676                && getCountOfEmptyLinesBetween(
677                     previousImportObject.getEndLineNumber(),
678                     currentImportObject.getStartLineNumber()) != 1;
679    }
680
681    /**
682     * Examine that imports separated by more than one empty line.
683     *
684     * @param previousImportObject
685     *        previous import in current group.
686     * @param currentImportObject
687     *        current import.
688     * @return
689     *        true, if current import separated from previous by more that one empty line.
690     */
691    private boolean isSeparatedByExtraEmptyLine(ImportDetails previousImportObject,
692                                                ImportDetails currentImportObject) {
693        return previousImportObject != null
694                && getCountOfEmptyLinesBetween(
695                     previousImportObject.getEndLineNumber(),
696                     currentImportObject.getStartLineNumber()) > 0;
697    }
698
699    /**
700     * Log wrong import group order.
701     *
702     * @param importAST
703     *        import ast.
704     * @param importGroup
705     *        import group.
706     * @param currentGroupNumber
707     *        current group number we are checking.
708     * @param fullImportIdent
709     *        full import name.
710     */
711    private void logWrongImportGroupOrder(DetailAST importAST, String importGroup,
712            String currentGroupNumber, String fullImportIdent) {
713        if (NON_GROUP_RULE_GROUP.equals(importGroup)) {
714            log(importAST, MSG_NONGROUP_IMPORT, fullImportIdent);
715        }
716        else if (NON_GROUP_RULE_GROUP.equals(currentGroupNumber)) {
717            log(importAST, MSG_NONGROUP_EXPECTED, importGroup, fullImportIdent);
718        }
719        else {
720            log(importAST, MSG_ORDER, importGroup, currentGroupNumber, fullImportIdent);
721        }
722    }
723
724    /**
725     * Get next import group.
726     *
727     * @param currentGroupNumber
728     *        current group number.
729     * @return
730     *        next import group.
731     */
732    private String getNextImportGroup(int currentGroupNumber) {
733        int nextGroupNumber = currentGroupNumber;
734
735        while (customOrderRules.size() > nextGroupNumber + 1) {
736            if (hasAnyImportInCurrentGroup(customOrderRules.get(nextGroupNumber))) {
737                break;
738            }
739            nextGroupNumber++;
740        }
741        return customOrderRules.get(nextGroupNumber);
742    }
743
744    /**
745     * Checks if current group contains any import.
746     *
747     * @param currentGroup
748     *        current group.
749     * @return
750     *        true, if current group contains at least one import.
751     */
752    private boolean hasAnyImportInCurrentGroup(String currentGroup) {
753        boolean result = false;
754        for (ImportDetails currentImport : importToGroupList) {
755            if (currentGroup.equals(currentImport.getImportGroup())) {
756                result = true;
757                break;
758            }
759        }
760        return result;
761    }
762
763    /**
764     * Get import valid group.
765     *
766     * @param isStatic
767     *        is static import.
768     * @param importPath
769     *        full import path.
770     * @return import valid group.
771     */
772    private String getImportGroup(boolean isStatic, String importPath) {
773        RuleMatchForImport bestMatch = new RuleMatchForImport(NON_GROUP_RULE_GROUP, 0, 0);
774        if (isStatic && customOrderRules.contains(STATIC_RULE_GROUP)) {
775            bestMatch.group = STATIC_RULE_GROUP;
776            bestMatch.matchLength = importPath.length();
777        }
778        else if (customOrderRules.contains(SAME_PACKAGE_RULE_GROUP)) {
779            final String importPathTrimmedToSamePackageDepth =
780                    getFirstDomainsFromIdent(samePackageMatchingDepth, importPath);
781            if (samePackageDomainsRegExp.equals(importPathTrimmedToSamePackageDepth)) {
782                bestMatch.group = SAME_PACKAGE_RULE_GROUP;
783                bestMatch.matchLength = importPath.length();
784            }
785        }
786        for (String group : customOrderRules) {
787            if (STANDARD_JAVA_PACKAGE_RULE_GROUP.equals(group)) {
788                bestMatch = findBetterPatternMatch(importPath,
789                        STANDARD_JAVA_PACKAGE_RULE_GROUP, standardPackageRegExp, bestMatch);
790            }
791            if (SPECIAL_IMPORTS_RULE_GROUP.equals(group)) {
792                bestMatch = findBetterPatternMatch(importPath,
793                        group, specialImportsRegExp, bestMatch);
794            }
795        }
796
797        if (bestMatch.group.equals(NON_GROUP_RULE_GROUP)
798                && customOrderRules.contains(THIRD_PARTY_PACKAGE_RULE_GROUP)
799                && thirdPartyPackageRegExp.matcher(importPath).find()) {
800            bestMatch.group = THIRD_PARTY_PACKAGE_RULE_GROUP;
801        }
802        return bestMatch.group;
803    }
804
805    /**
806     * Tries to find better matching regular expression:
807     * longer matching substring wins; in case of the same length,
808     * lower position of matching substring wins.
809     *
810     * @param importPath
811     *      Full import identifier
812     * @param group
813     *      Import group we are trying to assign the import
814     * @param regExp
815     *      Regular expression for import group
816     * @param currentBestMatch
817     *      object with currently best match
818     * @return better match (if found) or the same (currentBestMatch)
819     */
820    private static RuleMatchForImport findBetterPatternMatch(String importPath, String group,
821            Pattern regExp, RuleMatchForImport currentBestMatch) {
822        RuleMatchForImport betterMatchCandidate = currentBestMatch;
823        final Matcher matcher = regExp.matcher(importPath);
824        while (matcher.find()) {
825            final int length = matcher.end() - matcher.start();
826            if (length > betterMatchCandidate.matchLength
827                    || length == betterMatchCandidate.matchLength
828                        && matcher.start() < betterMatchCandidate.matchPosition) {
829                betterMatchCandidate = new RuleMatchForImport(group, length, matcher.start());
830            }
831        }
832        return betterMatchCandidate;
833    }
834
835    /**
836     * Checks compare two import paths.
837     *
838     * @param import1
839     *        current import.
840     * @param import2
841     *        previous import.
842     * @return a negative integer, zero, or a positive integer as the
843     *        specified String is greater than, equal to, or less
844     *        than this String, ignoring case considerations.
845     */
846    private static int compareImports(String import1, String import2) {
847        int result = 0;
848        final String separator = "\\.";
849        final String[] import1Tokens = import1.split(separator);
850        final String[] import2Tokens = import2.split(separator);
851        for (int i = 0; i != import1Tokens.length && i != import2Tokens.length; i++) {
852            final String import1Token = import1Tokens[i];
853            final String import2Token = import2Tokens[i];
854            result = import1Token.compareTo(import2Token);
855            if (result != 0) {
856                break;
857            }
858        }
859        if (result == 0) {
860            result = Integer.compare(import1Tokens.length, import2Tokens.length);
861        }
862        return result;
863    }
864
865    /**
866     * Counts empty lines between given parameters.
867     *
868     * @param fromLineNo
869     *        One-based line number of previous import.
870     * @param toLineNo
871     *        One-based line number of current import.
872     * @return count of empty lines between given parameters, exclusive,
873     *        eg., (fromLineNo, toLineNo).
874     */
875    private int getCountOfEmptyLinesBetween(int fromLineNo, int toLineNo) {
876        int result = 0;
877        final String[] lines = getLines();
878
879        for (int i = fromLineNo + 1; i <= toLineNo - 1; i++) {
880            // "- 1" because the numbering is one-based
881            if (CommonUtil.isBlank(lines[i - 1])) {
882                result++;
883            }
884        }
885        return result;
886    }
887
888    /**
889     * Forms import full path.
890     *
891     * @param token
892     *        current token.
893     * @return full path or null.
894     */
895    private static String getFullImportIdent(DetailAST token) {
896        String ident = "";
897        if (token != null) {
898            ident = FullIdent.createFullIdent(token.findFirstToken(TokenTypes.DOT)).getText();
899        }
900        return ident;
901    }
902
903    /**
904     * Parses ordering rule and adds it to the list with rules.
905     *
906     * @param ruleStr
907     *        String with rule.
908     * @throws IllegalArgumentException when SAME_PACKAGE rule parameter is not positive integer
909     * @throws IllegalStateException when ruleStr is unexpected value
910     */
911    private void addRulesToList(String ruleStr) {
912        if (STATIC_RULE_GROUP.equals(ruleStr)
913                || THIRD_PARTY_PACKAGE_RULE_GROUP.equals(ruleStr)
914                || STANDARD_JAVA_PACKAGE_RULE_GROUP.equals(ruleStr)
915                || SPECIAL_IMPORTS_RULE_GROUP.equals(ruleStr)) {
916            customOrderRules.add(ruleStr);
917        }
918        else if (ruleStr.startsWith(SAME_PACKAGE_RULE_GROUP)) {
919            final String rule = ruleStr.substring(ruleStr.indexOf('(') + 1,
920                    ruleStr.indexOf(')'));
921            samePackageMatchingDepth = Integer.parseInt(rule);
922            if (samePackageMatchingDepth <= 0) {
923                throw new IllegalArgumentException(
924                        "SAME_PACKAGE rule parameter should be positive integer: " + ruleStr);
925            }
926            customOrderRules.add(SAME_PACKAGE_RULE_GROUP);
927        }
928        else {
929            throw new IllegalStateException("Unexpected rule: " + ruleStr);
930        }
931    }
932
933    /**
934     * Creates samePackageDomainsRegExp of the first package domains.
935     *
936     * @param firstPackageDomainsCount
937     *        number of first package domains.
938     * @param packageNode
939     *        package node.
940     * @return same package regexp.
941     */
942    private static String createSamePackageRegexp(int firstPackageDomainsCount,
943             DetailAST packageNode) {
944        final String packageFullPath = getFullImportIdent(packageNode);
945        return getFirstDomainsFromIdent(firstPackageDomainsCount, packageFullPath);
946    }
947
948    /**
949     * Extracts defined amount of domains from the left side of package/import identifier.
950     *
951     * @param firstPackageDomainsCount
952     *        number of first package domains.
953     * @param packageFullPath
954     *        full identifier containing path to package or imported object.
955     * @return String with defined amount of domains or full identifier
956     *        (if full identifier had less domain than specified)
957     */
958    private static String getFirstDomainsFromIdent(
959            final int firstPackageDomainsCount, final String packageFullPath) {
960        final StringBuilder builder = new StringBuilder(256);
961        final StringTokenizer tokens = new StringTokenizer(packageFullPath, ".");
962        int count = firstPackageDomainsCount;
963
964        while (count > 0 && tokens.hasMoreTokens()) {
965            builder.append(tokens.nextToken()).append('.');
966            count--;
967        }
968        return builder.toString();
969    }
970
971    /**
972     * Contains import attributes as line number, import full path, import
973     * group.
974     */
975    private static class ImportDetails {
976
977        /** Import full path. */
978        private final String importFullPath;
979
980        /** Import group. */
981        private final String importGroup;
982
983        /** Is static import. */
984        private final boolean staticImport;
985
986        /** Import AST. */
987        private final DetailAST importAST;
988
989        /**
990         * Initialise importFullPath, importGroup, staticImport, importAST.
991         *
992         * @param importFullPath
993         *        import full path.
994         * @param importGroup
995         *        import group.
996         * @param staticImport
997         *        if import is static.
998         * @param importAST
999         *        import ast
1000         */
1001        /* package */ ImportDetails(String importFullPath, String importGroup, boolean staticImport,
1002                                    DetailAST importAST) {
1003            this.importFullPath = importFullPath;
1004            this.importGroup = importGroup;
1005            this.staticImport = staticImport;
1006            this.importAST = importAST;
1007        }
1008
1009        /**
1010         * Get import full path variable.
1011         *
1012         * @return import full path variable.
1013         */
1014        public String getImportFullPath() {
1015            return importFullPath;
1016        }
1017
1018        /**
1019         * Get import start line number from ast.
1020         *
1021         * @return import start line from ast.
1022         */
1023        public int getStartLineNumber() {
1024            return importAST.getLineNo();
1025        }
1026
1027        /**
1028         * Get import end line number from ast.
1029         * <p>
1030         * <b>Note:</b> It can be different from <b>startLineNumber</b> when import statement span
1031         * multiple lines.
1032         * </p>
1033         * @return import end line from ast.
1034         */
1035        public int getEndLineNumber() {
1036            return importAST.getLastChild().getLineNo();
1037        }
1038
1039        /**
1040         * Get import group.
1041         *
1042         * @return import group.
1043         */
1044        public String getImportGroup() {
1045            return importGroup;
1046        }
1047
1048        /**
1049         * Checks if import is static.
1050         *
1051         * @return true, if import is static.
1052         */
1053        public boolean isStaticImport() {
1054            return staticImport;
1055        }
1056
1057        /**
1058         * Get import ast.
1059         *
1060         * @return import ast.
1061         */
1062        public DetailAST getImportAST() {
1063            return importAST;
1064        }
1065
1066    }
1067
1068    /**
1069     * Contains matching attributes assisting in definition of "best matching"
1070     * group for import.
1071     */
1072    private static class RuleMatchForImport {
1073
1074        /** Position of matching string for current best match. */
1075        private final int matchPosition;
1076        /** Length of matching string for current best match. */
1077        private int matchLength;
1078        /** Import group for current best match. */
1079        private String group;
1080
1081        /**
1082         * Constructor to initialize the fields.
1083         *
1084         * @param group
1085         *        Matched group.
1086         * @param length
1087         *        Matching length.
1088         * @param position
1089         *        Matching position.
1090         */
1091        /* package */ RuleMatchForImport(String group, int length, int position) {
1092            this.group = group;
1093            matchLength = length;
1094            matchPosition = position;
1095        }
1096
1097    }
1098
1099}