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.
140 * Type is {@code java.lang.String}.
141 * Default value is {@code ""}.
142 * </li>
143 * <li>
144 * Property {@code standardPackageRegExp} - Specify RegExp for STANDARD_JAVA_PACKAGE group imports.
145 * Type is {@code java.util.regex.Pattern}.
146 * Default value is {@code "^(java|javax)\."}.
147 * </li>
148 * <li>
149 * Property {@code thirdPartyPackageRegExp} - Specify RegExp for THIRD_PARTY_PACKAGE group imports.
150 * Type is {@code java.util.regex.Pattern}.
151 * Default value is {@code ".*"}.
152 * </li>
153 * <li>
154 * Property {@code specialImportsRegExp} - Specify RegExp for SPECIAL_IMPORTS group imports.
155 * Type is {@code java.util.regex.Pattern}.
156 * Default value is {@code "^$" (empty)}.
157 * </li>
158 * <li>
159 * Property {@code separateLineBetweenGroups} - Force empty line separator between
160 * import groups.
161 * Type is {@code boolean}.
162 * Default value is {@code true}.
163 * </li>
164 * <li>
165 * Property {@code sortImportsInGroupAlphabetically} - Force grouping alphabetically,
166 * in <a href="https://en.wikipedia.org/wiki/ASCII#Order">ASCII sort order</a>.
167 * Type is {@code boolean}.
168 * Default value is {@code false}.
169 * </li>
170 * </ul>
171 * <p>
172 * To configure the check so that it matches default Eclipse formatter configuration
173 * (tested on Kepler and Luna releases):
174 * </p>
175 * <ul>
176 * <li>
177 * group of static imports is on the top
178 * </li>
179 * <li>
180 * groups of non-static imports: "java" and "javax" packages first, then "org" and then all other
181 * imports
182 * </li>
183 * <li>
184 * imports will be sorted in the groups
185 * </li>
186 * <li>
187 * groups are separated by single blank line
188 * </li>
189 * </ul>
190 * <p>
191 * Notes:
192 * </p>
193 * <ul>
194 * <li>
195 * "com" package is not mentioned on configuration, because it is ignored by Eclipse Kepler and Luna
196 * (looks like Eclipse defect)
197 * </li>
198 * <li>
199 * configuration below doesn't work in all 100% cases due to inconsistent behavior prior to Mars
200 * release, but covers most scenarios
201 * </li>
202 * </ul>
203 * <pre>
204 * &lt;module name=&quot;CustomImportOrder&quot;&gt;
205 *   &lt;property name=&quot;customImportOrderRules&quot;
206 *     value=&quot;STATIC###STANDARD_JAVA_PACKAGE###SPECIAL_IMPORTS&quot;/&gt;
207 *   &lt;property name=&quot;specialImportsRegExp&quot; value=&quot;^org\.&quot;/&gt;
208 *   &lt;property name=&quot;sortImportsInGroupAlphabetically&quot; value=&quot;true&quot;/&gt;
209 *   &lt;property name=&quot;separateLineBetweenGroups&quot; value=&quot;true&quot;/&gt;
210 * &lt;/module&gt;
211 * </pre>
212 * <p>
213 * To configure the check so that it matches default Eclipse formatter configuration
214 * (tested on Mars release):
215 * </p>
216 * <ul>
217 * <li>
218 * group of static imports is on the top
219 * </li>
220 * <li>
221 * groups of non-static imports: "java" and "javax" packages first, then "org" and "com",
222 * then all other imports as one group
223 * </li>
224 * <li>
225 * imports will be sorted in the groups
226 * </li>
227 * <li>
228 * groups are separated by one blank line
229 * </li>
230 * </ul>
231 * <pre>
232 * &lt;module name=&quot;CustomImportOrder&quot;&gt;
233 *   &lt;property name=&quot;customImportOrderRules&quot;
234 *     value=&quot;STATIC###STANDARD_JAVA_PACKAGE###SPECIAL_IMPORTS###THIRD_PARTY_PACKAGE&quot;/&gt;
235 *   &lt;property name=&quot;specialImportsRegExp&quot; value=&quot;^org\.&quot;/&gt;
236 *   &lt;property name=&quot;thirdPartyPackageRegExp&quot; value=&quot;^com\.&quot;/&gt;
237 *   &lt;property name=&quot;sortImportsInGroupAlphabetically&quot; value=&quot;true&quot;/&gt;
238 *   &lt;property name=&quot;separateLineBetweenGroups&quot; value=&quot;true&quot;/&gt;
239 * &lt;/module&gt;
240 * </pre>
241 * <p>
242 * To configure the check so that it matches default IntelliJ IDEA formatter configuration
243 * (tested on v14):
244 * </p>
245 * <ul>
246 * <li>
247 * group of static imports is on the bottom
248 * </li>
249 * <li>
250 * groups of non-static imports: all imports except of "javax" and "java", then "javax" and "java"
251 * </li>
252 * <li>
253 * imports will be sorted in the groups
254 * </li>
255 * <li>
256 * groups are separated by one blank line
257 * </li>
258 * </ul>
259 * <p>
260 * Note: "separated" option is disabled because IDEA default has blank line between "java" and
261 * static imports, and no blank line between "javax" and "java"
262 * </p>
263 * <pre>
264 * &lt;module name="CustomImportOrder"&gt;
265 *   &lt;property name="customImportOrderRules"
266 *     value="THIRD_PARTY_PACKAGE###SPECIAL_IMPORTS###STANDARD_JAVA_PACKAGE###STATIC"/&gt;
267 *   &lt;property name="specialImportsRegExp" value="^javax\."/&gt;
268 *   &lt;property name="standardPackageRegExp" value="^java\."/&gt;
269 *   &lt;property name="sortImportsInGroupAlphabetically" value="true"/&gt;
270 *   &lt;property name="separateLineBetweenGroups" value="false"/&gt;
271 * &lt;/module&gt;
272 * </pre>
273 * <p>
274 * To configure the check so that it matches default NetBeans formatter configuration
275 * (tested on v8):
276 * </p>
277 * <ul>
278 * <li>
279 * groups of non-static imports are not defined, all imports will be sorted as a one group
280 * </li>
281 * <li>
282 * static imports are not separated, they will be sorted along with other imports
283 * </li>
284 * </ul>
285 * <pre>
286 * &lt;module name=&quot;CustomImportOrder&quot;/&gt;
287 * </pre>
288 * <p>
289 * To set RegExps for THIRD_PARTY_PACKAGE and STANDARD_JAVA_PACKAGE groups use
290 * thirdPartyPackageRegExp and standardPackageRegExp options.
291 * </p>
292 * <pre>
293 * &lt;module name=&quot;CustomImportOrder&quot;&gt;
294 *   &lt;property name=&quot;customImportOrderRules&quot;
295 *     value=&quot;STATIC###SAME_PACKAGE(3)###THIRD_PARTY_PACKAGE###STANDARD_JAVA_PACKAGE&quot;/&gt;
296 *   &lt;property name=&quot;thirdPartyPackageRegExp&quot; value=&quot;^(com|org)\.&quot;/&gt;
297 *   &lt;property name=&quot;standardPackageRegExp&quot; value=&quot;^(java|javax)\.&quot;/&gt;
298 * &lt;/module&gt;
299 * </pre>
300 * <p>
301 * Also, this check can be configured to force empty line separator between
302 * import groups. For example.
303 * </p>
304 * <pre>
305 * &lt;module name=&quot;CustomImportOrder&quot;&gt;
306 *   &lt;property name=&quot;separateLineBetweenGroups&quot; value=&quot;true&quot;/&gt;
307 * &lt;/module&gt;
308 * </pre>
309 * <p>
310 * It is possible to enforce
311 * <a href="https://en.wikipedia.org/wiki/ASCII#Order">ASCII sort order</a>
312 * of imports in groups using the following configuration:
313 * </p>
314 * <pre>
315 * &lt;module name=&quot;CustomImportOrder&quot;&gt;
316 *   &lt;property name=&quot;sortImportsInGroupAlphabetically&quot; value=&quot;true&quot;/&gt;
317 * &lt;/module&gt;
318 * </pre>
319 * <p>
320 * Example of ASCII order:
321 * </p>
322 * <pre>
323 * import java.awt.Dialog;
324 * import java.awt.Window;
325 * import java.awt.color.ColorSpace;
326 * import java.awt.Frame; // violation here - in ASCII order 'F' should go before 'c',
327 *                        // as all uppercase come before lowercase letters
328 * </pre>
329 * <p>
330 * To force checking imports sequence such as:
331 * </p>
332 * <pre>
333 * package com.puppycrawl.tools.checkstyle.imports;
334 *
335 * import com.google.common.annotations.GwtCompatible;
336 * import com.google.common.annotations.Beta;
337 * import com.google.common.annotations.VisibleForTesting;
338 *
339 * import org.abego.treelayout.Configuration;
340 *
341 * import static sun.tools.util.ModifierFilter.ALL_ACCESS;
342 *
343 * import com.google.common.annotations.GwtCompatible; // violation here - should be in the
344 *                                                     // THIRD_PARTY_PACKAGE group
345 * import android.*;
346 * </pre>
347 * <p>
348 * configure as follows:
349 * </p>
350 * <pre>
351 * &lt;module name=&quot;CustomImportOrder&quot;&gt;
352 *   &lt;property name=&quot;customImportOrderRules&quot;
353 *     value=&quot;SAME_PACKAGE(3)###THIRD_PARTY_PACKAGE###STATIC###SPECIAL_IMPORTS&quot;/&gt;
354 *   &lt;property name=&quot;specialImportsRegExp&quot; value=&quot;^android\.&quot;/&gt;
355 * &lt;/module&gt;
356 * </pre>
357 * <p>
358 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
359 * </p>
360 * <p>
361 * Violation Message Keys:
362 * </p>
363 * <ul>
364 * <li>
365 * {@code custom.import.order}
366 * </li>
367 * <li>
368 * {@code custom.import.order.lex}
369 * </li>
370 * <li>
371 * {@code custom.import.order.line.separator}
372 * </li>
373 * <li>
374 * {@code custom.import.order.nonGroup.expected}
375 * </li>
376 * <li>
377 * {@code custom.import.order.nonGroup.import}
378 * </li>
379 * <li>
380 * {@code custom.import.order.separated.internally}
381 * </li>
382 * </ul>
383 *
384 * @since 5.8
385 */
386@FileStatefulCheck
387public class CustomImportOrderCheck extends AbstractCheck {
388
389    /**
390     * A key is pointing to the warning message text in "messages.properties"
391     * file.
392     */
393    public static final String MSG_LINE_SEPARATOR = "custom.import.order.line.separator";
394
395    /**
396     * A key is pointing to the warning message text in "messages.properties"
397     * file.
398     */
399    public static final String MSG_SEPARATED_IN_GROUP = "custom.import.order.separated.internally";
400
401    /**
402     * A key is pointing to the warning message text in "messages.properties"
403     * file.
404     */
405    public static final String MSG_LEX = "custom.import.order.lex";
406
407    /**
408     * A key is pointing to the warning message text in "messages.properties"
409     * file.
410     */
411    public static final String MSG_NONGROUP_IMPORT = "custom.import.order.nonGroup.import";
412
413    /**
414     * A key is pointing to the warning message text in "messages.properties"
415     * file.
416     */
417    public static final String MSG_NONGROUP_EXPECTED = "custom.import.order.nonGroup.expected";
418
419    /**
420     * A key is pointing to the warning message text in "messages.properties"
421     * file.
422     */
423    public static final String MSG_ORDER = "custom.import.order";
424
425    /** STATIC group name. */
426    public static final String STATIC_RULE_GROUP = "STATIC";
427
428    /** SAME_PACKAGE group name. */
429    public static final String SAME_PACKAGE_RULE_GROUP = "SAME_PACKAGE";
430
431    /** THIRD_PARTY_PACKAGE group name. */
432    public static final String THIRD_PARTY_PACKAGE_RULE_GROUP = "THIRD_PARTY_PACKAGE";
433
434    /** STANDARD_JAVA_PACKAGE group name. */
435    public static final String STANDARD_JAVA_PACKAGE_RULE_GROUP = "STANDARD_JAVA_PACKAGE";
436
437    /** SPECIAL_IMPORTS group name. */
438    public static final String SPECIAL_IMPORTS_RULE_GROUP = "SPECIAL_IMPORTS";
439
440    /** NON_GROUP group name. */
441    private static final String NON_GROUP_RULE_GROUP = "NOT_ASSIGNED_TO_ANY_GROUP";
442
443    /** Pattern used to separate groups of imports. */
444    private static final Pattern GROUP_SEPARATOR_PATTERN = Pattern.compile("\\s*###\\s*");
445
446    /** Processed list of import order rules. */
447    private final List<String> customOrderRules = new ArrayList<>();
448
449    /** Contains objects with import attributes. */
450    private final List<ImportDetails> importToGroupList = new ArrayList<>();
451
452    /** Specify format of order declaration customizing by user. */
453    private String customImportOrderRules = "";
454
455    /** Specify RegExp for SAME_PACKAGE group imports. */
456    private String samePackageDomainsRegExp = "";
457
458    /** Specify RegExp for STANDARD_JAVA_PACKAGE group imports. */
459    private Pattern standardPackageRegExp = Pattern.compile("^(java|javax)\\.");
460
461    /** Specify RegExp for THIRD_PARTY_PACKAGE group imports. */
462    private Pattern thirdPartyPackageRegExp = Pattern.compile(".*");
463
464    /** Specify RegExp for SPECIAL_IMPORTS group imports. */
465    private Pattern specialImportsRegExp = Pattern.compile("^$");
466
467    /** Force empty line separator between import groups. */
468    private boolean separateLineBetweenGroups = true;
469
470    /**
471     * Force grouping alphabetically,
472     * in <a href="https://en.wikipedia.org/wiki/ASCII#Order"> ASCII sort order</a>.
473     */
474    private boolean sortImportsInGroupAlphabetically;
475
476    /** Number of first domains for SAME_PACKAGE group. */
477    private int samePackageMatchingDepth = 2;
478
479    /**
480     * Setter to specify RegExp for STANDARD_JAVA_PACKAGE group imports.
481     *
482     * @param regexp
483     *        user value.
484     */
485    public final void setStandardPackageRegExp(Pattern regexp) {
486        standardPackageRegExp = regexp;
487    }
488
489    /**
490     * Setter to specify RegExp for THIRD_PARTY_PACKAGE group imports.
491     *
492     * @param regexp
493     *        user value.
494     */
495    public final void setThirdPartyPackageRegExp(Pattern regexp) {
496        thirdPartyPackageRegExp = regexp;
497    }
498
499    /**
500     * Setter to specify RegExp for SPECIAL_IMPORTS group imports.
501     *
502     * @param regexp
503     *        user value.
504     */
505    public final void setSpecialImportsRegExp(Pattern regexp) {
506        specialImportsRegExp = regexp;
507    }
508
509    /**
510     * Setter to force empty line separator between import groups.
511     *
512     * @param value
513     *        user value.
514     */
515    public final void setSeparateLineBetweenGroups(boolean value) {
516        separateLineBetweenGroups = value;
517    }
518
519    /**
520     * Setter to force grouping alphabetically, in
521     * <a href="https://en.wikipedia.org/wiki/ASCII#Order">ASCII sort order</a>.
522     *
523     * @param value
524     *        user value.
525     */
526    public final void setSortImportsInGroupAlphabetically(boolean value) {
527        sortImportsInGroupAlphabetically = value;
528    }
529
530    /**
531     * Setter to specify format of order declaration customizing by user.
532     *
533     * @param inputCustomImportOrder
534     *        user value.
535     */
536    public final void setCustomImportOrderRules(final String inputCustomImportOrder) {
537        if (!customImportOrderRules.equals(inputCustomImportOrder)) {
538            for (String currentState : GROUP_SEPARATOR_PATTERN.split(inputCustomImportOrder)) {
539                addRulesToList(currentState);
540            }
541            customOrderRules.add(NON_GROUP_RULE_GROUP);
542        }
543        customImportOrderRules = inputCustomImportOrder;
544    }
545
546    @Override
547    public int[] getDefaultTokens() {
548        return getRequiredTokens();
549    }
550
551    @Override
552    public int[] getAcceptableTokens() {
553        return getRequiredTokens();
554    }
555
556    @Override
557    public int[] getRequiredTokens() {
558        return new int[] {
559            TokenTypes.IMPORT,
560            TokenTypes.STATIC_IMPORT,
561            TokenTypes.PACKAGE_DEF,
562        };
563    }
564
565    @Override
566    public void beginTree(DetailAST rootAST) {
567        importToGroupList.clear();
568    }
569
570    @Override
571    public void visitToken(DetailAST ast) {
572        if (ast.getType() == TokenTypes.PACKAGE_DEF) {
573            samePackageDomainsRegExp = createSamePackageRegexp(
574                    samePackageMatchingDepth, ast);
575        }
576        else {
577            final String importFullPath = getFullImportIdent(ast);
578            final boolean isStatic = ast.getType() == TokenTypes.STATIC_IMPORT;
579            importToGroupList.add(new ImportDetails(importFullPath,
580                    getImportGroup(isStatic, importFullPath), isStatic, ast));
581        }
582    }
583
584    @Override
585    public void finishTree(DetailAST rootAST) {
586        if (!importToGroupList.isEmpty()) {
587            finishImportList();
588        }
589    }
590
591    /** Examine the order of all the imports and log any violations. */
592    private void finishImportList() {
593        String currentGroup = getFirstGroup();
594        int currentGroupNumber = customOrderRules.indexOf(currentGroup);
595        ImportDetails previousImportObjectFromCurrentGroup = null;
596        String previousImportFromCurrentGroup = null;
597
598        for (ImportDetails importObject : importToGroupList) {
599            final String importGroup = importObject.getImportGroup();
600            final String fullImportIdent = importObject.getImportFullPath();
601
602            if (importGroup.equals(currentGroup)) {
603                validateExtraEmptyLine(previousImportObjectFromCurrentGroup,
604                        importObject, fullImportIdent);
605                if (isAlphabeticalOrderBroken(previousImportFromCurrentGroup, fullImportIdent)) {
606                    log(importObject.getImportAST(), MSG_LEX,
607                            fullImportIdent, previousImportFromCurrentGroup);
608                }
609                else {
610                    previousImportFromCurrentGroup = fullImportIdent;
611                }
612                previousImportObjectFromCurrentGroup = importObject;
613            }
614            else {
615                // not the last group, last one is always NON_GROUP
616                if (customOrderRules.size() > currentGroupNumber + 1) {
617                    final String nextGroup = getNextImportGroup(currentGroupNumber + 1);
618                    if (importGroup.equals(nextGroup)) {
619                        validateMissedEmptyLine(previousImportObjectFromCurrentGroup,
620                                importObject, fullImportIdent);
621                        currentGroup = nextGroup;
622                        currentGroupNumber = customOrderRules.indexOf(nextGroup);
623                        previousImportFromCurrentGroup = fullImportIdent;
624                    }
625                    else {
626                        logWrongImportGroupOrder(importObject.getImportAST(),
627                                importGroup, nextGroup, fullImportIdent);
628                    }
629                    previousImportObjectFromCurrentGroup = importObject;
630                }
631                else {
632                    logWrongImportGroupOrder(importObject.getImportAST(),
633                            importGroup, currentGroup, fullImportIdent);
634                }
635            }
636        }
637    }
638
639    /**
640     * Log violation if empty line is missed.
641     *
642     * @param previousImport previous import from current group.
643     * @param importObject current import.
644     * @param fullImportIdent full import identifier.
645     */
646    private void validateMissedEmptyLine(ImportDetails previousImport,
647                                         ImportDetails importObject, String fullImportIdent) {
648        if (isEmptyLineMissed(previousImport, importObject)) {
649            log(importObject.getImportAST(), MSG_LINE_SEPARATOR, fullImportIdent);
650        }
651    }
652
653    /**
654     * Log violation if extra empty line is present.
655     *
656     * @param previousImport previous import from current group.
657     * @param importObject current import.
658     * @param fullImportIdent full import identifier.
659     */
660    private void validateExtraEmptyLine(ImportDetails previousImport,
661                                        ImportDetails importObject, String fullImportIdent) {
662        if (isSeparatedByExtraEmptyLine(previousImport, importObject)) {
663            log(importObject.getImportAST(), MSG_SEPARATED_IN_GROUP, fullImportIdent);
664        }
665    }
666
667    /**
668     * Get first import group.
669     *
670     * @return
671     *        first import group of file.
672     */
673    private String getFirstGroup() {
674        final ImportDetails firstImport = importToGroupList.get(0);
675        return getImportGroup(firstImport.isStaticImport(),
676                firstImport.getImportFullPath());
677    }
678
679    /**
680     * Examine alphabetical order of imports.
681     *
682     * @param previousImport
683     *        previous import of current group.
684     * @param currentImport
685     *        current import.
686     * @return
687     *        true, if previous and current import are not in alphabetical order.
688     */
689    private boolean isAlphabeticalOrderBroken(String previousImport,
690                                              String currentImport) {
691        return sortImportsInGroupAlphabetically
692                && previousImport != null
693                && compareImports(currentImport, previousImport) < 0;
694    }
695
696    /**
697     * Examine empty lines between groups.
698     *
699     * @param previousImportObject
700     *        previous import in current group.
701     * @param currentImportObject
702     *        current import.
703     * @return
704     *        true, if current import NOT separated from previous import by empty line.
705     */
706    private boolean isEmptyLineMissed(ImportDetails previousImportObject,
707                                      ImportDetails currentImportObject) {
708        return separateLineBetweenGroups
709                && getCountOfEmptyLinesBetween(
710                     previousImportObject.getEndLineNumber(),
711                     currentImportObject.getStartLineNumber()) != 1;
712    }
713
714    /**
715     * Examine that imports separated by more than one empty line.
716     *
717     * @param previousImportObject
718     *        previous import in current group.
719     * @param currentImportObject
720     *        current import.
721     * @return
722     *        true, if current import separated from previous by more that one empty line.
723     */
724    private boolean isSeparatedByExtraEmptyLine(ImportDetails previousImportObject,
725                                                ImportDetails currentImportObject) {
726        return previousImportObject != null
727                && getCountOfEmptyLinesBetween(
728                     previousImportObject.getEndLineNumber(),
729                     currentImportObject.getStartLineNumber()) > 0;
730    }
731
732    /**
733     * Log wrong import group order.
734     *
735     * @param importAST
736     *        import ast.
737     * @param importGroup
738     *        import group.
739     * @param currentGroupNumber
740     *        current group number we are checking.
741     * @param fullImportIdent
742     *        full import name.
743     */
744    private void logWrongImportGroupOrder(DetailAST importAST, String importGroup,
745            String currentGroupNumber, String fullImportIdent) {
746        if (NON_GROUP_RULE_GROUP.equals(importGroup)) {
747            log(importAST, MSG_NONGROUP_IMPORT, fullImportIdent);
748        }
749        else if (NON_GROUP_RULE_GROUP.equals(currentGroupNumber)) {
750            log(importAST, MSG_NONGROUP_EXPECTED, importGroup, fullImportIdent);
751        }
752        else {
753            log(importAST, MSG_ORDER, importGroup, currentGroupNumber, fullImportIdent);
754        }
755    }
756
757    /**
758     * Get next import group.
759     *
760     * @param currentGroupNumber
761     *        current group number.
762     * @return
763     *        next import group.
764     */
765    private String getNextImportGroup(int currentGroupNumber) {
766        int nextGroupNumber = currentGroupNumber;
767
768        while (customOrderRules.size() > nextGroupNumber + 1) {
769            if (hasAnyImportInCurrentGroup(customOrderRules.get(nextGroupNumber))) {
770                break;
771            }
772            nextGroupNumber++;
773        }
774        return customOrderRules.get(nextGroupNumber);
775    }
776
777    /**
778     * Checks if current group contains any import.
779     *
780     * @param currentGroup
781     *        current group.
782     * @return
783     *        true, if current group contains at least one import.
784     */
785    private boolean hasAnyImportInCurrentGroup(String currentGroup) {
786        boolean result = false;
787        for (ImportDetails currentImport : importToGroupList) {
788            if (currentGroup.equals(currentImport.getImportGroup())) {
789                result = true;
790                break;
791            }
792        }
793        return result;
794    }
795
796    /**
797     * Get import valid group.
798     *
799     * @param isStatic
800     *        is static import.
801     * @param importPath
802     *        full import path.
803     * @return import valid group.
804     */
805    private String getImportGroup(boolean isStatic, String importPath) {
806        RuleMatchForImport bestMatch = new RuleMatchForImport(NON_GROUP_RULE_GROUP, 0, 0);
807        if (isStatic && customOrderRules.contains(STATIC_RULE_GROUP)) {
808            bestMatch.group = STATIC_RULE_GROUP;
809            bestMatch.matchLength = importPath.length();
810        }
811        else if (customOrderRules.contains(SAME_PACKAGE_RULE_GROUP)) {
812            final String importPathTrimmedToSamePackageDepth =
813                    getFirstDomainsFromIdent(samePackageMatchingDepth, importPath);
814            if (samePackageDomainsRegExp.equals(importPathTrimmedToSamePackageDepth)) {
815                bestMatch.group = SAME_PACKAGE_RULE_GROUP;
816                bestMatch.matchLength = importPath.length();
817            }
818        }
819        for (String group : customOrderRules) {
820            if (STANDARD_JAVA_PACKAGE_RULE_GROUP.equals(group)) {
821                bestMatch = findBetterPatternMatch(importPath,
822                        STANDARD_JAVA_PACKAGE_RULE_GROUP, standardPackageRegExp, bestMatch);
823            }
824            if (SPECIAL_IMPORTS_RULE_GROUP.equals(group)) {
825                bestMatch = findBetterPatternMatch(importPath,
826                        group, specialImportsRegExp, bestMatch);
827            }
828        }
829
830        if (bestMatch.group.equals(NON_GROUP_RULE_GROUP)
831                && customOrderRules.contains(THIRD_PARTY_PACKAGE_RULE_GROUP)
832                && thirdPartyPackageRegExp.matcher(importPath).find()) {
833            bestMatch.group = THIRD_PARTY_PACKAGE_RULE_GROUP;
834        }
835        return bestMatch.group;
836    }
837
838    /**
839     * Tries to find better matching regular expression:
840     * longer matching substring wins; in case of the same length,
841     * lower position of matching substring wins.
842     *
843     * @param importPath
844     *      Full import identifier
845     * @param group
846     *      Import group we are trying to assign the import
847     * @param regExp
848     *      Regular expression for import group
849     * @param currentBestMatch
850     *      object with currently best match
851     * @return better match (if found) or the same (currentBestMatch)
852     */
853    private static RuleMatchForImport findBetterPatternMatch(String importPath, String group,
854            Pattern regExp, RuleMatchForImport currentBestMatch) {
855        RuleMatchForImport betterMatchCandidate = currentBestMatch;
856        final Matcher matcher = regExp.matcher(importPath);
857        while (matcher.find()) {
858            final int length = matcher.end() - matcher.start();
859            if (length > betterMatchCandidate.matchLength
860                    || length == betterMatchCandidate.matchLength
861                        && matcher.start() < betterMatchCandidate.matchPosition) {
862                betterMatchCandidate = new RuleMatchForImport(group, length, matcher.start());
863            }
864        }
865        return betterMatchCandidate;
866    }
867
868    /**
869     * Checks compare two import paths.
870     *
871     * @param import1
872     *        current import.
873     * @param import2
874     *        previous import.
875     * @return a negative integer, zero, or a positive integer as the
876     *        specified String is greater than, equal to, or less
877     *        than this String, ignoring case considerations.
878     */
879    private static int compareImports(String import1, String import2) {
880        int result = 0;
881        final String separator = "\\.";
882        final String[] import1Tokens = import1.split(separator);
883        final String[] import2Tokens = import2.split(separator);
884        for (int i = 0; i != import1Tokens.length && i != import2Tokens.length; i++) {
885            final String import1Token = import1Tokens[i];
886            final String import2Token = import2Tokens[i];
887            result = import1Token.compareTo(import2Token);
888            if (result != 0) {
889                break;
890            }
891        }
892        if (result == 0) {
893            result = Integer.compare(import1Tokens.length, import2Tokens.length);
894        }
895        return result;
896    }
897
898    /**
899     * Counts empty lines between given parameters.
900     *
901     * @param fromLineNo
902     *        One-based line number of previous import.
903     * @param toLineNo
904     *        One-based line number of current import.
905     * @return count of empty lines between given parameters, exclusive,
906     *        eg., (fromLineNo, toLineNo).
907     */
908    private int getCountOfEmptyLinesBetween(int fromLineNo, int toLineNo) {
909        int result = 0;
910        final String[] lines = getLines();
911
912        for (int i = fromLineNo + 1; i <= toLineNo - 1; i++) {
913            // "- 1" because the numbering is one-based
914            if (CommonUtil.isBlank(lines[i - 1])) {
915                result++;
916            }
917        }
918        return result;
919    }
920
921    /**
922     * Forms import full path.
923     *
924     * @param token
925     *        current token.
926     * @return full path or null.
927     */
928    private static String getFullImportIdent(DetailAST token) {
929        String ident = "";
930        if (token != null) {
931            ident = FullIdent.createFullIdent(token.findFirstToken(TokenTypes.DOT)).getText();
932        }
933        return ident;
934    }
935
936    /**
937     * Parses ordering rule and adds it to the list with rules.
938     *
939     * @param ruleStr
940     *        String with rule.
941     * @throws IllegalArgumentException when SAME_PACKAGE rule parameter is not positive integer
942     * @throws IllegalStateException when ruleStr is unexpected value
943     */
944    private void addRulesToList(String ruleStr) {
945        if (STATIC_RULE_GROUP.equals(ruleStr)
946                || THIRD_PARTY_PACKAGE_RULE_GROUP.equals(ruleStr)
947                || STANDARD_JAVA_PACKAGE_RULE_GROUP.equals(ruleStr)
948                || SPECIAL_IMPORTS_RULE_GROUP.equals(ruleStr)) {
949            customOrderRules.add(ruleStr);
950        }
951        else if (ruleStr.startsWith(SAME_PACKAGE_RULE_GROUP)) {
952            final String rule = ruleStr.substring(ruleStr.indexOf('(') + 1,
953                    ruleStr.indexOf(')'));
954            samePackageMatchingDepth = Integer.parseInt(rule);
955            if (samePackageMatchingDepth <= 0) {
956                throw new IllegalArgumentException(
957                        "SAME_PACKAGE rule parameter should be positive integer: " + ruleStr);
958            }
959            customOrderRules.add(SAME_PACKAGE_RULE_GROUP);
960        }
961        else {
962            throw new IllegalStateException("Unexpected rule: " + ruleStr);
963        }
964    }
965
966    /**
967     * Creates samePackageDomainsRegExp of the first package domains.
968     *
969     * @param firstPackageDomainsCount
970     *        number of first package domains.
971     * @param packageNode
972     *        package node.
973     * @return same package regexp.
974     */
975    private static String createSamePackageRegexp(int firstPackageDomainsCount,
976             DetailAST packageNode) {
977        final String packageFullPath = getFullImportIdent(packageNode);
978        return getFirstDomainsFromIdent(firstPackageDomainsCount, packageFullPath);
979    }
980
981    /**
982     * Extracts defined amount of domains from the left side of package/import identifier.
983     *
984     * @param firstPackageDomainsCount
985     *        number of first package domains.
986     * @param packageFullPath
987     *        full identifier containing path to package or imported object.
988     * @return String with defined amount of domains or full identifier
989     *        (if full identifier had less domain than specified)
990     */
991    private static String getFirstDomainsFromIdent(
992            final int firstPackageDomainsCount, final String packageFullPath) {
993        final StringBuilder builder = new StringBuilder(256);
994        final StringTokenizer tokens = new StringTokenizer(packageFullPath, ".");
995        int count = firstPackageDomainsCount;
996
997        while (count > 0 && tokens.hasMoreTokens()) {
998            builder.append(tokens.nextToken()).append('.');
999            count--;
1000        }
1001        return builder.toString();
1002    }
1003
1004    /**
1005     * Contains import attributes as line number, import full path, import
1006     * group.
1007     */
1008    private static class ImportDetails {
1009
1010        /** Import full path. */
1011        private final String importFullPath;
1012
1013        /** Import group. */
1014        private final String importGroup;
1015
1016        /** Is static import. */
1017        private final boolean staticImport;
1018
1019        /** Import AST. */
1020        private final DetailAST importAST;
1021
1022        /**
1023         * Initialise importFullPath, importGroup, staticImport, importAST.
1024         *
1025         * @param importFullPath
1026         *        import full path.
1027         * @param importGroup
1028         *        import group.
1029         * @param staticImport
1030         *        if import is static.
1031         * @param importAST
1032         *        import ast
1033         */
1034        /* package */ ImportDetails(String importFullPath, String importGroup, boolean staticImport,
1035                                    DetailAST importAST) {
1036            this.importFullPath = importFullPath;
1037            this.importGroup = importGroup;
1038            this.staticImport = staticImport;
1039            this.importAST = importAST;
1040        }
1041
1042        /**
1043         * Get import full path variable.
1044         *
1045         * @return import full path variable.
1046         */
1047        public String getImportFullPath() {
1048            return importFullPath;
1049        }
1050
1051        /**
1052         * Get import start line number from ast.
1053         *
1054         * @return import start line from ast.
1055         */
1056        public int getStartLineNumber() {
1057            return importAST.getLineNo();
1058        }
1059
1060        /**
1061         * Get import end line number from ast.
1062         * <p>
1063         * <b>Note:</b> It can be different from <b>startLineNumber</b> when import statement span
1064         * multiple lines.
1065         * </p>
1066         *
1067         * @return import end line from ast.
1068         */
1069        public int getEndLineNumber() {
1070            return importAST.getLastChild().getLineNo();
1071        }
1072
1073        /**
1074         * Get import group.
1075         *
1076         * @return import group.
1077         */
1078        public String getImportGroup() {
1079            return importGroup;
1080        }
1081
1082        /**
1083         * Checks if import is static.
1084         *
1085         * @return true, if import is static.
1086         */
1087        public boolean isStaticImport() {
1088            return staticImport;
1089        }
1090
1091        /**
1092         * Get import ast.
1093         *
1094         * @return import ast.
1095         */
1096        public DetailAST getImportAST() {
1097            return importAST;
1098        }
1099
1100    }
1101
1102    /**
1103     * Contains matching attributes assisting in definition of "best matching"
1104     * group for import.
1105     */
1106    private static class RuleMatchForImport {
1107
1108        /** Position of matching string for current best match. */
1109        private final int matchPosition;
1110        /** Length of matching string for current best match. */
1111        private int matchLength;
1112        /** Import group for current best match. */
1113        private String group;
1114
1115        /**
1116         * Constructor to initialize the fields.
1117         *
1118         * @param group
1119         *        Matched group.
1120         * @param length
1121         *        Matching length.
1122         * @param position
1123         *        Matching position.
1124         */
1125        /* package */ RuleMatchForImport(String group, int length, int position) {
1126            this.group = group;
1127            matchLength = length;
1128            matchPosition = position;
1129        }
1130
1131    }
1132
1133}