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