1 ///////////////////////////////////////////////////////////////////////////////////////////////
2 // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3 // Copyright (C) 2001-2025 the original author or authors.
4 //
5 // This library is free software; you can redistribute it and/or
6 // modify it under the terms of the GNU Lesser General Public
7 // License as published by the Free Software Foundation; either
8 // version 2.1 of the License, or (at your option) any later version.
9 //
10 // This library is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 // Lesser General Public License for more details.
14 //
15 // You should have received a copy of the GNU Lesser General Public
16 // License along with this library; if not, write to the Free Software
17 // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18 ///////////////////////////////////////////////////////////////////////////////////////////////
19
20 package com.puppycrawl.tools.checkstyle.checks.imports;
21
22 import java.util.ArrayList;
23 import java.util.Arrays;
24 import java.util.List;
25 import java.util.StringTokenizer;
26 import java.util.regex.Matcher;
27 import java.util.regex.Pattern;
28
29 import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
30 import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
31 import com.puppycrawl.tools.checkstyle.api.DetailAST;
32 import com.puppycrawl.tools.checkstyle.api.FullIdent;
33 import com.puppycrawl.tools.checkstyle.api.TokenTypes;
34 import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
35
36 /**
37 * <div>
38 * Checks that the groups of import declarations appear in the order specified
39 * by the user. If there is an import but its group is not specified in the
40 * configuration such an import should be placed at the end of the import list.
41 * </div>
42 *
43 * <p>
44 * The rule consists of:
45 * </p>
46 * <ol>
47 * <li>
48 * STATIC group. This group sets the ordering of static imports.
49 * </li>
50 * <li>
51 * SAME_PACKAGE(n) group. This group sets the ordering of the same package imports.
52 * Imports are considered on SAME_PACKAGE group if <b>n</b> first domains in package
53 * name and import name are identical:
54 * <div class="wrapper"><pre class="prettyprint"><code class="language-java">
55 * package java.util.concurrent.locks;
56 *
57 * import java.io.File;
58 * import java.util.*; //#1
59 * import java.util.List; //#2
60 * import java.util.StringTokenizer; //#3
61 * import java.util.concurrent.*; //#4
62 * import java.util.concurrent.AbstractExecutorService; //#5
63 * import java.util.concurrent.locks.LockSupport; //#6
64 * import java.util.regex.Pattern; //#7
65 * import java.util.regex.Matcher; //#8
66 * </code></pre></div>
67 * If we have SAME_PACKAGE(3) on configuration file, imports #4-6 will be considered as
68 * a SAME_PACKAGE group (java.util.concurrent.*, java.util.concurrent.AbstractExecutorService,
69 * java.util.concurrent.locks.LockSupport). SAME_PACKAGE(2) will include #1-8.
70 * SAME_PACKAGE(4) will include only #6. SAME_PACKAGE(5) will result in no imports assigned
71 * to SAME_PACKAGE group because actual package java.util.concurrent.locks has only 4 domains.
72 * </li>
73 * <li>
74 * THIRD_PARTY_PACKAGE group. This group sets ordering of third party imports.
75 * Third party imports are all imports except STATIC, SAME_PACKAGE(n), STANDARD_JAVA_PACKAGE and
76 * SPECIAL_IMPORTS.
77 * </li>
78 * <li>
79 * STANDARD_JAVA_PACKAGE group. By default, this group sets ordering of standard java/javax imports.
80 * </li>
81 * <li>
82 * SPECIAL_IMPORTS group. This group may contain some imports that have particular meaning for the
83 * user.
84 * </li>
85 * </ol>
86 *
87 * <p>
88 * Notes:
89 * Rules are configured as a comma-separated ordered list.
90 * </p>
91 *
92 * <p>
93 * Note: '###' group separator is deprecated (in favor of a comma-separated list),
94 * but is currently supported for backward compatibility.
95 * </p>
96 *
97 * <p>
98 * To set RegExps for THIRD_PARTY_PACKAGE and STANDARD_JAVA_PACKAGE groups use
99 * thirdPartyPackageRegExp and standardPackageRegExp options.
100 * </p>
101 *
102 * <p>
103 * Pretty often one import can match more than one group. For example, static import from standard
104 * package or regular expressions are configured to allow one import match multiple groups.
105 * In this case, group will be assigned according to priorities:
106 * </p>
107 * <ol>
108 * <li>
109 * STATIC has top priority
110 * </li>
111 * <li>
112 * SAME_PACKAGE has second priority
113 * </li>
114 * <li>
115 * STANDARD_JAVA_PACKAGE and SPECIAL_IMPORTS will compete using "best match" rule: longer
116 * matching substring wins; in case of the same length, lower position of matching substring
117 * wins; if position is the same, order of rules in configuration solves the puzzle.
118 * </li>
119 * <li>
120 * THIRD_PARTY has the least priority
121 * </li>
122 * </ol>
123 *
124 * <p>
125 * Few examples to illustrate "best match":
126 * </p>
127 *
128 * <p>
129 * 1. patterns STANDARD_JAVA_PACKAGE = "Check", SPECIAL_IMPORTS="ImportOrderCheck" and input file:
130 * </p>
131 * <div class="wrapper"><pre class="prettyprint"><code class="language-java">
132 * import com.puppycrawl.tools.checkstyle.checks.imports.CustomImportOrderCheck;
133 * import com.puppycrawl.tools.checkstyle.checks.imports.ImportOrderCheck;
134 * </code></pre></div>
135 *
136 * <p>
137 * Result: imports will be assigned to SPECIAL_IMPORTS, because matching substring length is 16.
138 * Matching substring for STANDARD_JAVA_PACKAGE is 5.
139 * </p>
140 *
141 * <p>
142 * 2. patterns STANDARD_JAVA_PACKAGE = "Check", SPECIAL_IMPORTS="Avoid" and file:
143 * </p>
144 * <div class="wrapper"><pre class="prettyprint"><code class="language-java">
145 * import com.puppycrawl.tools.checkstyle.checks.imports.AvoidStarImportCheck;
146 * </code></pre></div>
147 *
148 * <p>
149 * Result: import will be assigned to SPECIAL_IMPORTS. Matching substring length is 5 for both
150 * patterns. However, "Avoid" position is lower than "Check" position.
151 * </p>
152 *
153 * @since 5.8
154 */
155 @FileStatefulCheck
156 public class CustomImportOrderCheck extends AbstractCheck {
157
158 /**
159 * A key is pointing to the warning message text in "messages.properties"
160 * file.
161 */
162 public static final String MSG_LINE_SEPARATOR = "custom.import.order.line.separator";
163
164 /**
165 * A key is pointing to the warning message text in "messages.properties"
166 * file.
167 */
168 public static final String MSG_SEPARATED_IN_GROUP = "custom.import.order.separated.internally";
169
170 /**
171 * A key is pointing to the warning message text in "messages.properties"
172 * file.
173 */
174 public static final String MSG_LEX = "custom.import.order.lex";
175
176 /**
177 * A key is pointing to the warning message text in "messages.properties"
178 * file.
179 */
180 public static final String MSG_NONGROUP_IMPORT = "custom.import.order.nonGroup.import";
181
182 /**
183 * A key is pointing to the warning message text in "messages.properties"
184 * file.
185 */
186 public static final String MSG_NONGROUP_EXPECTED = "custom.import.order.nonGroup.expected";
187
188 /**
189 * A key is pointing to the warning message text in "messages.properties"
190 * file.
191 */
192 public static final String MSG_ORDER = "custom.import.order";
193
194 /** STATIC group name. */
195 public static final String STATIC_RULE_GROUP = "STATIC";
196
197 /** SAME_PACKAGE group name. */
198 public static final String SAME_PACKAGE_RULE_GROUP = "SAME_PACKAGE";
199
200 /** THIRD_PARTY_PACKAGE group name. */
201 public static final String THIRD_PARTY_PACKAGE_RULE_GROUP = "THIRD_PARTY_PACKAGE";
202
203 /** STANDARD_JAVA_PACKAGE group name. */
204 public static final String STANDARD_JAVA_PACKAGE_RULE_GROUP = "STANDARD_JAVA_PACKAGE";
205
206 /** SPECIAL_IMPORTS group name. */
207 public static final String SPECIAL_IMPORTS_RULE_GROUP = "SPECIAL_IMPORTS";
208
209 /** NON_GROUP group name. */
210 private static final String NON_GROUP_RULE_GROUP = "NOT_ASSIGNED_TO_ANY_GROUP";
211
212 /** Pattern used to separate groups of imports. */
213 private static final Pattern GROUP_SEPARATOR_PATTERN = Pattern.compile("\\s*###\\s*");
214
215 /** Specify ordered list of import groups. */
216 private final List<String> customImportOrderRules = new ArrayList<>();
217
218 /** Contains objects with import attributes. */
219 private final List<ImportDetails> importToGroupList = new ArrayList<>();
220
221 /** Specify RegExp for SAME_PACKAGE group imports. */
222 private String samePackageDomainsRegExp = "";
223
224 /** Specify RegExp for STANDARD_JAVA_PACKAGE group imports. */
225 private Pattern standardPackageRegExp = Pattern.compile("^(java|javax)\\.");
226
227 /** Specify RegExp for THIRD_PARTY_PACKAGE group imports. */
228 private Pattern thirdPartyPackageRegExp = Pattern.compile(".*");
229
230 /** Specify RegExp for SPECIAL_IMPORTS group imports. */
231 private Pattern specialImportsRegExp = Pattern.compile("^$");
232
233 /** Force empty line separator between import groups. */
234 private boolean separateLineBetweenGroups = true;
235
236 /**
237 * Force grouping alphabetically,
238 * in <a href="https://en.wikipedia.org/wiki/ASCII#Order"> ASCII sort order</a>.
239 */
240 private boolean sortImportsInGroupAlphabetically;
241
242 /** Number of first domains for SAME_PACKAGE group. */
243 private int samePackageMatchingDepth;
244
245 /**
246 * Setter to specify RegExp for STANDARD_JAVA_PACKAGE group imports.
247 *
248 * @param regexp
249 * user value.
250 * @since 5.8
251 */
252 public final void setStandardPackageRegExp(Pattern regexp) {
253 standardPackageRegExp = regexp;
254 }
255
256 /**
257 * Setter to specify RegExp for THIRD_PARTY_PACKAGE group imports.
258 *
259 * @param regexp
260 * user value.
261 * @since 5.8
262 */
263 public final void setThirdPartyPackageRegExp(Pattern regexp) {
264 thirdPartyPackageRegExp = regexp;
265 }
266
267 /**
268 * Setter to specify RegExp for SPECIAL_IMPORTS group imports.
269 *
270 * @param regexp
271 * user value.
272 * @since 5.8
273 */
274 public final void setSpecialImportsRegExp(Pattern regexp) {
275 specialImportsRegExp = regexp;
276 }
277
278 /**
279 * Setter to force empty line separator between import groups.
280 *
281 * @param value
282 * user value.
283 * @since 5.8
284 */
285 public final void setSeparateLineBetweenGroups(boolean value) {
286 separateLineBetweenGroups = value;
287 }
288
289 /**
290 * Setter to force grouping alphabetically, in
291 * <a href="https://en.wikipedia.org/wiki/ASCII#Order">ASCII sort order</a>.
292 *
293 * @param value
294 * user value.
295 * @since 5.8
296 */
297 public final void setSortImportsInGroupAlphabetically(boolean value) {
298 sortImportsInGroupAlphabetically = value;
299 }
300
301 /**
302 * Setter to specify ordered list of import groups.
303 *
304 * @param rules
305 * user value.
306 * @since 5.8
307 */
308 public final void setCustomImportOrderRules(String... rules) {
309 Arrays.stream(rules)
310 .map(GROUP_SEPARATOR_PATTERN::split)
311 .flatMap(Arrays::stream)
312 .forEach(this::addRulesToList);
313
314 customImportOrderRules.add(NON_GROUP_RULE_GROUP);
315 }
316
317 @Override
318 public int[] getDefaultTokens() {
319 return getRequiredTokens();
320 }
321
322 @Override
323 public int[] getAcceptableTokens() {
324 return getRequiredTokens();
325 }
326
327 @Override
328 public int[] getRequiredTokens() {
329 return new int[] {
330 TokenTypes.IMPORT,
331 TokenTypes.STATIC_IMPORT,
332 TokenTypes.PACKAGE_DEF,
333 };
334 }
335
336 @Override
337 public void beginTree(DetailAST rootAST) {
338 importToGroupList.clear();
339 }
340
341 @Override
342 public void visitToken(DetailAST ast) {
343 if (ast.getType() == TokenTypes.PACKAGE_DEF) {
344 samePackageDomainsRegExp = createSamePackageRegexp(
345 samePackageMatchingDepth, ast);
346 }
347 else {
348 final String importFullPath = getFullImportIdent(ast);
349 final boolean isStatic = ast.getType() == TokenTypes.STATIC_IMPORT;
350 importToGroupList.add(new ImportDetails(importFullPath,
351 getImportGroup(isStatic, importFullPath), isStatic, ast));
352 }
353 }
354
355 @Override
356 public void finishTree(DetailAST rootAST) {
357 if (!importToGroupList.isEmpty()) {
358 finishImportList();
359 }
360 }
361
362 /** Examine the order of all the imports and log any violations. */
363 private void finishImportList() {
364 String currentGroup = getFirstGroup();
365 int currentGroupNumber = customImportOrderRules.lastIndexOf(currentGroup);
366 ImportDetails previousImportObjectFromCurrentGroup = null;
367 String previousImportFromCurrentGroup = null;
368
369 for (ImportDetails importObject : importToGroupList) {
370 final String importGroup = importObject.getImportGroup();
371 final String fullImportIdent = importObject.getImportFullPath();
372
373 if (importGroup.equals(currentGroup)) {
374 validateExtraEmptyLine(previousImportObjectFromCurrentGroup,
375 importObject, fullImportIdent);
376 if (isAlphabeticalOrderBroken(previousImportFromCurrentGroup, fullImportIdent)) {
377 log(importObject.getImportAST(), MSG_LEX,
378 fullImportIdent, previousImportFromCurrentGroup);
379 }
380 else {
381 previousImportFromCurrentGroup = fullImportIdent;
382 }
383 previousImportObjectFromCurrentGroup = importObject;
384 }
385 else {
386 // not the last group, last one is always NON_GROUP
387 if (customImportOrderRules.size() > currentGroupNumber + 1) {
388 final String nextGroup = getNextImportGroup(currentGroupNumber + 1);
389 if (importGroup.equals(nextGroup)) {
390 validateMissedEmptyLine(previousImportObjectFromCurrentGroup,
391 importObject, fullImportIdent);
392 currentGroup = nextGroup;
393 currentGroupNumber = customImportOrderRules.lastIndexOf(nextGroup);
394 previousImportFromCurrentGroup = fullImportIdent;
395 }
396 else {
397 logWrongImportGroupOrder(importObject.getImportAST(),
398 importGroup, nextGroup, fullImportIdent);
399 }
400 previousImportObjectFromCurrentGroup = importObject;
401 }
402 else {
403 logWrongImportGroupOrder(importObject.getImportAST(),
404 importGroup, currentGroup, fullImportIdent);
405 }
406 }
407 }
408 }
409
410 /**
411 * Log violation if empty line is missed.
412 *
413 * @param previousImport previous import from current group.
414 * @param importObject current import.
415 * @param fullImportIdent full import identifier.
416 */
417 private void validateMissedEmptyLine(ImportDetails previousImport,
418 ImportDetails importObject, String fullImportIdent) {
419 if (isEmptyLineMissed(previousImport, importObject)) {
420 log(importObject.getImportAST(), MSG_LINE_SEPARATOR, fullImportIdent);
421 }
422 }
423
424 /**
425 * Log violation if extra empty line is present.
426 *
427 * @param previousImport previous import from current group.
428 * @param importObject current import.
429 * @param fullImportIdent full import identifier.
430 */
431 private void validateExtraEmptyLine(ImportDetails previousImport,
432 ImportDetails importObject, String fullImportIdent) {
433 if (isSeparatedByExtraEmptyLine(previousImport, importObject)) {
434 log(importObject.getImportAST(), MSG_SEPARATED_IN_GROUP, fullImportIdent);
435 }
436 }
437
438 /**
439 * Get first import group.
440 *
441 * @return
442 * first import group of file.
443 */
444 private String getFirstGroup() {
445 final ImportDetails firstImport = importToGroupList.get(0);
446 return getImportGroup(firstImport.isStaticImport(),
447 firstImport.getImportFullPath());
448 }
449
450 /**
451 * Examine alphabetical order of imports.
452 *
453 * @param previousImport
454 * previous import of current group.
455 * @param currentImport
456 * current import.
457 * @return
458 * true, if previous and current import are not in alphabetical order.
459 */
460 private boolean isAlphabeticalOrderBroken(String previousImport,
461 String currentImport) {
462 return sortImportsInGroupAlphabetically
463 && previousImport != null
464 && compareImports(currentImport, previousImport) < 0;
465 }
466
467 /**
468 * Examine empty lines between groups.
469 *
470 * @param previousImportObject
471 * previous import in current group.
472 * @param currentImportObject
473 * current import.
474 * @return
475 * true, if current import NOT separated from previous import by empty line.
476 */
477 private boolean isEmptyLineMissed(ImportDetails previousImportObject,
478 ImportDetails currentImportObject) {
479 return separateLineBetweenGroups
480 && getCountOfEmptyLinesBetween(
481 previousImportObject.getEndLineNumber(),
482 currentImportObject.getStartLineNumber()) != 1;
483 }
484
485 /**
486 * Examine that imports separated by more than one empty line.
487 *
488 * @param previousImportObject
489 * previous import in current group.
490 * @param currentImportObject
491 * current import.
492 * @return
493 * true, if current import separated from previous by more than one empty line.
494 */
495 private boolean isSeparatedByExtraEmptyLine(ImportDetails previousImportObject,
496 ImportDetails currentImportObject) {
497 return previousImportObject != null
498 && getCountOfEmptyLinesBetween(
499 previousImportObject.getEndLineNumber(),
500 currentImportObject.getStartLineNumber()) > 0;
501 }
502
503 /**
504 * Log wrong import group order.
505 *
506 * @param importAST
507 * import ast.
508 * @param importGroup
509 * import group.
510 * @param currentGroupNumber
511 * current group number we are checking.
512 * @param fullImportIdent
513 * full import name.
514 */
515 private void logWrongImportGroupOrder(DetailAST importAST, String importGroup,
516 String currentGroupNumber, String fullImportIdent) {
517 if (NON_GROUP_RULE_GROUP.equals(importGroup)) {
518 log(importAST, MSG_NONGROUP_IMPORT, fullImportIdent);
519 }
520 else if (NON_GROUP_RULE_GROUP.equals(currentGroupNumber)) {
521 log(importAST, MSG_NONGROUP_EXPECTED, importGroup, fullImportIdent);
522 }
523 else {
524 log(importAST, MSG_ORDER, importGroup, currentGroupNumber, fullImportIdent);
525 }
526 }
527
528 /**
529 * Get next import group.
530 *
531 * @param currentGroupNumber
532 * current group number.
533 * @return
534 * next import group.
535 */
536 private String getNextImportGroup(int currentGroupNumber) {
537 int nextGroupNumber = currentGroupNumber;
538
539 while (customImportOrderRules.size() > nextGroupNumber + 1) {
540 if (hasAnyImportInCurrentGroup(customImportOrderRules.get(nextGroupNumber))) {
541 break;
542 }
543 nextGroupNumber++;
544 }
545 return customImportOrderRules.get(nextGroupNumber);
546 }
547
548 /**
549 * Checks if current group contains any import.
550 *
551 * @param currentGroup
552 * current group.
553 * @return
554 * true, if current group contains at least one import.
555 */
556 private boolean hasAnyImportInCurrentGroup(String currentGroup) {
557 boolean result = false;
558 for (ImportDetails currentImport : importToGroupList) {
559 if (currentGroup.equals(currentImport.getImportGroup())) {
560 result = true;
561 break;
562 }
563 }
564 return result;
565 }
566
567 /**
568 * Get import valid group.
569 *
570 * @param isStatic
571 * is static import.
572 * @param importPath
573 * full import path.
574 * @return import valid group.
575 */
576 private String getImportGroup(boolean isStatic, String importPath) {
577 RuleMatchForImport bestMatch = new RuleMatchForImport(NON_GROUP_RULE_GROUP, 0, 0);
578 if (isStatic && customImportOrderRules.contains(STATIC_RULE_GROUP)) {
579 bestMatch.group = STATIC_RULE_GROUP;
580 bestMatch.matchLength = importPath.length();
581 }
582 else if (customImportOrderRules.contains(SAME_PACKAGE_RULE_GROUP)) {
583 final String importPathTrimmedToSamePackageDepth =
584 getFirstDomainsFromIdent(samePackageMatchingDepth, importPath);
585 if (samePackageDomainsRegExp.equals(importPathTrimmedToSamePackageDepth)) {
586 bestMatch.group = SAME_PACKAGE_RULE_GROUP;
587 bestMatch.matchLength = importPath.length();
588 }
589 }
590 for (String group : customImportOrderRules) {
591 if (STANDARD_JAVA_PACKAGE_RULE_GROUP.equals(group)) {
592 bestMatch = findBetterPatternMatch(importPath,
593 STANDARD_JAVA_PACKAGE_RULE_GROUP, standardPackageRegExp, bestMatch);
594 }
595 if (SPECIAL_IMPORTS_RULE_GROUP.equals(group)) {
596 bestMatch = findBetterPatternMatch(importPath,
597 group, specialImportsRegExp, bestMatch);
598 }
599 }
600
601 if (NON_GROUP_RULE_GROUP.equals(bestMatch.group)
602 && customImportOrderRules.contains(THIRD_PARTY_PACKAGE_RULE_GROUP)
603 && thirdPartyPackageRegExp.matcher(importPath).find()) {
604 bestMatch.group = THIRD_PARTY_PACKAGE_RULE_GROUP;
605 }
606 return bestMatch.group;
607 }
608
609 /**
610 * Tries to find better matching regular expression:
611 * longer matching substring wins; in case of the same length,
612 * lower position of matching substring wins.
613 *
614 * @param importPath
615 * Full import identifier
616 * @param group
617 * Import group we are trying to assign the import
618 * @param regExp
619 * Regular expression for import group
620 * @param currentBestMatch
621 * object with currently best match
622 * @return better match (if found) or the same (currentBestMatch)
623 */
624 private static RuleMatchForImport findBetterPatternMatch(String importPath, String group,
625 Pattern regExp, RuleMatchForImport currentBestMatch) {
626 RuleMatchForImport betterMatchCandidate = currentBestMatch;
627 final Matcher matcher = regExp.matcher(importPath);
628 while (matcher.find()) {
629 final int matchStart = matcher.start();
630 final int length = matcher.end() - matchStart;
631 if (length > betterMatchCandidate.matchLength
632 || length == betterMatchCandidate.matchLength
633 && matchStart < betterMatchCandidate.matchPosition) {
634 betterMatchCandidate = new RuleMatchForImport(group, length, matchStart);
635 }
636 }
637 return betterMatchCandidate;
638 }
639
640 /**
641 * Checks compare two import paths.
642 *
643 * @param import1
644 * current import.
645 * @param import2
646 * previous import.
647 * @return a negative integer, zero, or a positive integer as the
648 * specified String is greater than, equal to, or less
649 * than this String, ignoring case considerations.
650 */
651 private static int compareImports(String import1, String import2) {
652 int result = 0;
653 final String separator = "\\.";
654 final String[] import1Tokens = import1.split(separator);
655 final String[] import2Tokens = import2.split(separator);
656 for (int i = 0; i != import1Tokens.length && i != import2Tokens.length; i++) {
657 final String import1Token = import1Tokens[i];
658 final String import2Token = import2Tokens[i];
659 result = import1Token.compareTo(import2Token);
660 if (result != 0) {
661 break;
662 }
663 }
664 if (result == 0) {
665 result = Integer.compare(import1Tokens.length, import2Tokens.length);
666 }
667 return result;
668 }
669
670 /**
671 * Counts empty lines between given parameters.
672 *
673 * @param fromLineNo
674 * One-based line number of previous import.
675 * @param toLineNo
676 * One-based line number of current import.
677 * @return count of empty lines between given parameters, exclusive,
678 * eg., (fromLineNo, toLineNo).
679 */
680 private int getCountOfEmptyLinesBetween(int fromLineNo, int toLineNo) {
681 int result = 0;
682 final String[] lines = getLines();
683
684 for (int i = fromLineNo + 1; i <= toLineNo - 1; i++) {
685 // "- 1" because the numbering is one-based
686 if (CommonUtil.isBlank(lines[i - 1])) {
687 result++;
688 }
689 }
690 return result;
691 }
692
693 /**
694 * Forms import full path.
695 *
696 * @param token
697 * current token.
698 * @return full path or null.
699 */
700 private static String getFullImportIdent(DetailAST token) {
701 String ident = "";
702 if (token != null) {
703 ident = FullIdent.createFullIdent(token.findFirstToken(TokenTypes.DOT)).getText();
704 }
705 return ident;
706 }
707
708 /**
709 * Parses ordering rule and adds it to the list with rules.
710 *
711 * @param ruleStr
712 * String with rule.
713 * @throws IllegalArgumentException when SAME_PACKAGE rule parameter is not positive integer
714 * @throws IllegalStateException when ruleStr is unexpected value
715 */
716 private void addRulesToList(String ruleStr) {
717 if (STATIC_RULE_GROUP.equals(ruleStr)
718 || THIRD_PARTY_PACKAGE_RULE_GROUP.equals(ruleStr)
719 || STANDARD_JAVA_PACKAGE_RULE_GROUP.equals(ruleStr)
720 || SPECIAL_IMPORTS_RULE_GROUP.equals(ruleStr)) {
721 customImportOrderRules.add(ruleStr);
722 }
723 else if (ruleStr.startsWith(SAME_PACKAGE_RULE_GROUP)) {
724 final String rule = ruleStr.substring(ruleStr.indexOf('(') + 1,
725 ruleStr.indexOf(')'));
726 samePackageMatchingDepth = Integer.parseInt(rule);
727 if (samePackageMatchingDepth <= 0) {
728 throw new IllegalArgumentException(
729 "SAME_PACKAGE rule parameter should be positive integer: " + ruleStr);
730 }
731 customImportOrderRules.add(SAME_PACKAGE_RULE_GROUP);
732 }
733 else {
734 throw new IllegalStateException("Unexpected rule: " + ruleStr);
735 }
736 }
737
738 /**
739 * Creates samePackageDomainsRegExp of the first package domains.
740 *
741 * @param firstPackageDomainsCount
742 * number of first package domains.
743 * @param packageNode
744 * package node.
745 * @return same package regexp.
746 */
747 private static String createSamePackageRegexp(int firstPackageDomainsCount,
748 DetailAST packageNode) {
749 final String packageFullPath = getFullImportIdent(packageNode);
750 return getFirstDomainsFromIdent(firstPackageDomainsCount, packageFullPath);
751 }
752
753 /**
754 * Extracts defined amount of domains from the left side of package/import identifier.
755 *
756 * @param firstPackageDomainsCount
757 * number of first package domains.
758 * @param packageFullPath
759 * full identifier containing path to package or imported object.
760 * @return String with defined amount of domains or full identifier
761 * (if full identifier had less domain than specified)
762 */
763 private static String getFirstDomainsFromIdent(
764 final int firstPackageDomainsCount, final String packageFullPath) {
765 final StringBuilder builder = new StringBuilder(256);
766 final StringTokenizer tokens = new StringTokenizer(packageFullPath, ".");
767 int count = firstPackageDomainsCount;
768
769 while (count > 0 && tokens.hasMoreTokens()) {
770 builder.append(tokens.nextToken());
771 count--;
772 }
773 return builder.toString();
774 }
775
776 /**
777 * Contains import attributes as line number, import full path, import
778 * group.
779 */
780 private static final class ImportDetails {
781
782 /** Import full path. */
783 private final String importFullPath;
784
785 /** Import group. */
786 private final String importGroup;
787
788 /** Is static import. */
789 private final boolean staticImport;
790
791 /** Import AST. */
792 private final DetailAST importAST;
793
794 /**
795 * Initialise importFullPath, importGroup, staticImport, importAST.
796 *
797 * @param importFullPath
798 * import full path.
799 * @param importGroup
800 * import group.
801 * @param staticImport
802 * if import is static.
803 * @param importAST
804 * import ast
805 */
806 private ImportDetails(String importFullPath, String importGroup, boolean staticImport,
807 DetailAST importAST) {
808 this.importFullPath = importFullPath;
809 this.importGroup = importGroup;
810 this.staticImport = staticImport;
811 this.importAST = importAST;
812 }
813
814 /**
815 * Get import full path variable.
816 *
817 * @return import full path variable.
818 */
819 public String getImportFullPath() {
820 return importFullPath;
821 }
822
823 /**
824 * Get import start line number from ast.
825 *
826 * @return import start line from ast.
827 */
828 public int getStartLineNumber() {
829 return importAST.getLineNo();
830 }
831
832 /**
833 * Get import end line number from ast.
834 *
835 * <p>
836 * <b>Note:</b> It can be different from <b>startLineNumber</b> when import statement span
837 * multiple lines.
838 * </p>
839 *
840 * @return import end line from ast.
841 */
842 public int getEndLineNumber() {
843 return importAST.getLastChild().getLineNo();
844 }
845
846 /**
847 * Get import group.
848 *
849 * @return import group.
850 */
851 public String getImportGroup() {
852 return importGroup;
853 }
854
855 /**
856 * Checks if import is static.
857 *
858 * @return true, if import is static.
859 */
860 public boolean isStaticImport() {
861 return staticImport;
862 }
863
864 /**
865 * Get import ast.
866 *
867 * @return import ast.
868 */
869 public DetailAST getImportAST() {
870 return importAST;
871 }
872
873 }
874
875 /**
876 * Contains matching attributes assisting in definition of "best matching"
877 * group for import.
878 */
879 private static final class RuleMatchForImport {
880
881 /** Position of matching string for current best match. */
882 private final int matchPosition;
883 /** Length of matching string for current best match. */
884 private int matchLength;
885 /** Import group for current best match. */
886 private String group;
887
888 /**
889 * Constructor to initialize the fields.
890 *
891 * @param group
892 * Matched group.
893 * @param length
894 * Matching length.
895 * @param position
896 * Matching position.
897 */
898 private RuleMatchForImport(String group, int length, int position) {
899 this.group = group;
900 matchLength = length;
901 matchPosition = position;
902 }
903
904 }
905
906 }