View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2026 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.utils;
21  
22  import java.io.Closeable;
23  import java.io.File;
24  import java.io.IOException;
25  import java.lang.reflect.Constructor;
26  import java.lang.reflect.InvocationTargetException;
27  import java.net.MalformedURLException;
28  import java.net.URI;
29  import java.net.URISyntaxException;
30  import java.net.URL;
31  import java.nio.file.Files;
32  import java.nio.file.Path;
33  import java.util.BitSet;
34  import java.util.Objects;
35  import java.util.regex.Matcher;
36  import java.util.regex.Pattern;
37  import java.util.regex.PatternSyntaxException;
38  
39  import org.xml.sax.InputSource;
40  
41  import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
42  
43  /**
44   * Contains utility methods.
45   *
46   */
47  public final class CommonUtil {
48  
49      /** Default tab width for column reporting. */
50      public static final int DEFAULT_TAB_WIDTH = 8;
51  
52      /** For cases where no tokens should be accepted. */
53      public static final BitSet EMPTY_BIT_SET = new BitSet();
54      /** Copied from org.apache.commons.lang3.ArrayUtils. */
55      public static final String[] EMPTY_STRING_ARRAY = new String[0];
56      /** Copied from org.apache.commons.lang3.ArrayUtils. */
57      public static final Integer[] EMPTY_INTEGER_OBJECT_ARRAY = new Integer[0];
58      /** Copied from org.apache.commons.lang3.ArrayUtils. */
59      public static final Object[] EMPTY_OBJECT_ARRAY = new Object[0];
60      /** Copied from org.apache.commons.lang3.ArrayUtils. */
61      public static final int[] EMPTY_INT_ARRAY = new int[0];
62      /** Copied from org.apache.commons.lang3.ArrayUtils. */
63      public static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
64      /** Copied from org.apache.commons.lang3.ArrayUtils. */
65      public static final double[] EMPTY_DOUBLE_ARRAY = new double[0];
66      /** Pseudo URL protocol for loading from the class path. */
67      public static final String CLASSPATH_URL_PROTOCOL = "classpath:";
68  
69      /** Prefix for the exception when unable to find resource. */
70      private static final String UNABLE_TO_FIND_EXCEPTION_PREFIX = "Unable to find: ";
71  
72      /** The extension separator. */
73      private static final String EXTENSION_SEPARATOR = ".";
74  
75      /** Stop instances being created. **/
76      private CommonUtil() {
77      }
78  
79      /**
80       * Helper method to create a regular expression.
81       *
82       * @param pattern
83       *            the pattern to match
84       * @return a created regexp object
85       * @throws IllegalArgumentException
86       *             if unable to create Pattern object.
87       **/
88      public static Pattern createPattern(String pattern) {
89          return createPattern(pattern, 0);
90      }
91  
92      /**
93       * Helper method to create a regular expression with a specific flags.
94       *
95       * @param pattern
96       *            the pattern to match
97       * @param flags
98       *            the flags to set
99       * @return a created regexp object
100      * @throws IllegalArgumentException
101      *             if unable to create Pattern object.
102      **/
103     public static Pattern createPattern(String pattern, int flags) {
104         try {
105             return Pattern.compile(pattern, flags);
106         }
107         catch (final PatternSyntaxException exc) {
108             throw new IllegalArgumentException(
109                 "Failed to initialise regular expression " + pattern, exc);
110         }
111     }
112 
113     /**
114      * Returns whether the file extension matches what we are meant to process.
115      *
116      * @param file
117      *            the file to be checked.
118      * @param fileExtensions
119      *            files extensions, empty property in config makes it matches to all.
120      * @return whether there is a match.
121      */
122     public static boolean matchesFileExtension(File file, String... fileExtensions) {
123         boolean result = false;
124         if (fileExtensions == null || fileExtensions.length == 0) {
125             result = true;
126         }
127         else {
128             // normalize extensions so all of them have a leading dot
129             final String[] withDotExtensions = new String[fileExtensions.length];
130             for (int i = 0; i < fileExtensions.length; i++) {
131                 final String extension = fileExtensions[i];
132                 if (extension.startsWith(EXTENSION_SEPARATOR)) {
133                     withDotExtensions[i] = extension;
134                 }
135                 else {
136                     withDotExtensions[i] = EXTENSION_SEPARATOR + extension;
137                 }
138             }
139 
140             final String fileName = file.getName();
141             for (final String fileExtension : withDotExtensions) {
142                 if (fileName.endsWith(fileExtension)) {
143                     result = true;
144                     break;
145                 }
146             }
147         }
148 
149         return result;
150     }
151 
152     /**
153      * Returns whether the specified string contains only whitespace up to the specified index.
154      *
155      * @param index
156      *            index to check up to
157      * @param line
158      *            the line to check
159      * @return whether there is only whitespace
160      */
161     public static boolean hasWhitespaceBefore(int index, String line) {
162         boolean result = true;
163         for (int i = 0; i < index; i++) {
164             if (!Character.isWhitespace(line.charAt(i))) {
165                 result = false;
166                 break;
167             }
168         }
169         return result;
170     }
171 
172     /**
173      * Returns the length of a string ignoring all trailing whitespace.
174      * It is a pity that there is not a trim() like
175      * method that only removed the trailing whitespace.
176      *
177      * @param line
178      *            the string to process
179      * @return the length of the string ignoring all trailing whitespace
180      **/
181     public static int lengthMinusTrailingWhitespace(String line) {
182         int len = line.length();
183         for (int i = len - 1; i >= 0; i--) {
184             if (!Character.isWhitespace(line.charAt(i))) {
185                 break;
186             }
187             len--;
188         }
189         return len;
190     }
191 
192     /**
193      * Returns the length of a String prefix with tabs expanded.
194      * Each tab is counted as the number of characters is
195      * takes to jump to the next tab stop.
196      *
197      * @param inputString
198      *            the input String
199      * @param toIdx
200      *            index in string (exclusive) where the calculation stops
201      * @param tabWidth
202      *            the distance between tab stop position.
203      * @return the length of string.substring(0, toIdx) with tabs expanded.
204      */
205     public static int lengthExpandedTabs(String inputString,
206             int toIdx,
207             int tabWidth) {
208         int len = 0;
209         for (int idx = 0; idx < toIdx; idx++) {
210             if (inputString.codePointAt(idx) == '\t') {
211                 len = (len / tabWidth + 1) * tabWidth;
212             }
213             else {
214                 len++;
215             }
216         }
217         return len;
218     }
219 
220     /**
221      * Validates whether passed string is a valid pattern or not.
222      *
223      * @param pattern
224      *            string to validate
225      * @return true if the pattern is valid false otherwise
226      */
227     public static boolean isPatternValid(String pattern) {
228         boolean isValid = true;
229         try {
230             Pattern.compile(pattern);
231         }
232         catch (final PatternSyntaxException ignored) {
233             isValid = false;
234         }
235         return isValid;
236     }
237 
238     /**
239      * Returns base class name from qualified name.
240      *
241      * @param type
242      *            the fully qualified name. Cannot be null
243      * @return the base class name from a fully qualified name
244      */
245     public static String baseClassName(String type) {
246         final int index = type.lastIndexOf('.');
247         return type.substring(index + 1);
248     }
249 
250     /**
251      * Constructs a relative path between base directory and a given path.
252      *
253      * @param baseDirectory
254      *            the base path to which given path is relativized
255      * @param path
256      *            the path to relativize against base directory
257      * @return the relative normalized path between base directory and
258      *     path or path if base directory is null.
259      */
260     public static String relativizePath(final String baseDirectory, final String path) {
261         final String resultPath;
262         if (baseDirectory == null) {
263             resultPath = path;
264         }
265         else {
266             final Path pathAbsolute = Path.of(path);
267             final Path pathBase = Path.of(baseDirectory);
268             resultPath = pathBase.relativize(pathAbsolute).toString();
269         }
270         return resultPath;
271     }
272 
273     /**
274      * Gets constructor of targetClass.
275      *
276      * @param <T> type of the target class object.
277      * @param targetClass
278      *            from which constructor is returned
279      * @param parameterTypes
280      *            of constructor
281      * @return constructor of targetClass
282      * @throws IllegalStateException if any exception occurs
283      * @see Class#getConstructor(Class[])
284      */
285     public static <T> Constructor<T> getConstructor(Class<T> targetClass,
286                                                     Class<?>... parameterTypes) {
287         try {
288             return targetClass.getConstructor(parameterTypes);
289         }
290         catch (NoSuchMethodException exc) {
291             throw new IllegalStateException(exc);
292         }
293     }
294 
295     /**
296      * Returns new instance of a class.
297      *
298      * @param <T>
299      *            type of constructor
300      * @param constructor
301      *            to invoke
302      * @param parameters
303      *            to pass to constructor
304      * @return new instance of class
305      * @throws IllegalStateException if any exception occurs
306      * @see Constructor#newInstance(Object...)
307      */
308     public static <T> T invokeConstructor(Constructor<T> constructor, Object... parameters) {
309         try {
310             return constructor.newInstance(parameters);
311         }
312         catch (InstantiationException | IllegalAccessException | InvocationTargetException exc) {
313             throw new IllegalStateException(exc);
314         }
315     }
316 
317     /**
318      * Closes a stream re-throwing IOException as IllegalStateException.
319      *
320      * @param closeable
321      *            Closeable object
322      * @throws IllegalStateException when any IOException occurs
323      */
324     public static void close(Closeable closeable) {
325         if (closeable != null) {
326             try {
327                 closeable.close();
328             }
329             catch (IOException exc) {
330                 throw new IllegalStateException("Cannot close the stream", exc);
331             }
332         }
333     }
334 
335     /**
336      * Creates an input source from a file.
337      *
338      * @param filename name of the file.
339      * @return input source.
340      * @throws CheckstyleException if an error occurs.
341      */
342     public static InputSource sourceFromFilename(String filename) throws CheckstyleException {
343         // figure out if this is a File or a URL
344         final URI uri = getUriByFilename(filename);
345         return new InputSource(uri.toASCIIString());
346     }
347 
348     /**
349      * Resolve the specified filename to a URI.
350      *
351      * @param filename name of the file
352      * @return resolved file URI
353      * @throws CheckstyleException on failure
354      */
355     public static URI getUriByFilename(String filename) throws CheckstyleException {
356         URI uri = getWebOrFileProtocolUri(filename);
357 
358         if (uri == null) {
359             uri = getFilepathOrClasspathUri(filename);
360         }
361 
362         return uri;
363     }
364 
365     /**
366      * Resolves the specified filename containing 'http', 'https', 'ftp',
367      * and 'file' protocols (or any RFC 2396 compliant URL) to a URI.
368      *
369      * @param filename name of the file
370      * @return resolved file URI or null if URL is malformed or non-existent
371      */
372     public static URI getWebOrFileProtocolUri(String filename) {
373         URI uri;
374         try {
375             uri = URI.create(filename);
376             uri = uri.toURL().toURI();
377         }
378         catch (IllegalArgumentException | MalformedURLException | URISyntaxException ignored) {
379             uri = null;
380         }
381         return uri;
382     }
383 
384     /**
385      * Resolves the specified local filename, possibly with 'classpath:'
386      * protocol, to a URI.  First we attempt to create a new file with
387      * given filename, then attempt to load file from class path.
388      *
389      * @param filename name of the file
390      * @return resolved file URI
391      * @throws CheckstyleException on failure
392      */
393     private static URI getFilepathOrClasspathUri(String filename)
394             throws CheckstyleException {
395         final URI uri;
396         if (filename.startsWith(CLASSPATH_URL_PROTOCOL)) {
397             uri = getResourceFromClassPath(
398                 filename.substring(CLASSPATH_URL_PROTOCOL.length()));
399         }
400         else {
401             final Path path = Path.of(filename);
402 
403             if (Files.exists(path)) {
404                 uri = path.toFile().toURI();
405             }
406             else {
407                 uri = getResourceFromClassPath(filename);
408             }
409         }
410         return uri;
411     }
412 
413     /**
414      * Gets a resource from the classpath.
415      *
416      * @param filename name of file
417      * @return URI of file in classpath
418      * @throws CheckstyleException on failure
419      */
420     public static URI getResourceFromClassPath(String filename) throws CheckstyleException {
421         final URL configUrl;
422         if (filename.charAt(0) == '/') {
423             configUrl = getCheckstyleResource(filename);
424         }
425         else {
426             configUrl = ClassLoader.getSystemResource(filename);
427         }
428 
429         if (configUrl == null) {
430             throw new CheckstyleException(UNABLE_TO_FIND_EXCEPTION_PREFIX + filename);
431         }
432 
433         final URI uri;
434         try {
435             uri = configUrl.toURI();
436         }
437         catch (final URISyntaxException exc) {
438             throw new CheckstyleException(UNABLE_TO_FIND_EXCEPTION_PREFIX + filename, exc);
439         }
440 
441         return uri;
442     }
443 
444     /**
445      * Finds a resource with a given name in the Checkstyle resource bundle.
446      * This method is intended only for internal use in Checkstyle tests for
447      * easy mocking to gain 100% coverage.
448      *
449      * @param name name of the desired resource
450      * @return URI of the resource
451      */
452     public static URL getCheckstyleResource(String name) {
453         return CommonUtil.class.getResource(name);
454     }
455 
456     /**
457      * Puts part of line, which matches regexp into given template
458      * on positions $n where 'n' is number of matched part in line.
459      *
460      * @param template the string to expand.
461      * @param lineToPlaceInTemplate contains expression which should be placed into string.
462      * @param regexp expression to find in comment.
463      * @return the string, based on template filled with given lines
464      */
465     public static String fillTemplateWithStringsByRegexp(
466         String template, String lineToPlaceInTemplate, Pattern regexp) {
467         final Matcher matcher = regexp.matcher(lineToPlaceInTemplate);
468         String result = template;
469         if (matcher.find()) {
470             for (int i = 0; i <= matcher.groupCount(); i++) {
471                 // $n expands comment match like in Pattern.subst().
472                 result = result.replaceAll("\\$" + i, matcher.group(i));
473             }
474         }
475         return result;
476     }
477 
478     /**
479      * Returns file name without extension.
480      * We do not use the method from Guava library to reduce Checkstyle's dependencies
481      * on external libraries.
482      *
483      * @param fullFilename file name with extension.
484      * @return file name without extension.
485      */
486     public static String getFileNameWithoutExtension(String fullFilename) {
487         final String fileName = Path.of(fullFilename).toFile().getName();
488         final int dotIndex = fileName.lastIndexOf('.');
489         final String fileNameWithoutExtension;
490         if (dotIndex == -1) {
491             fileNameWithoutExtension = fileName;
492         }
493         else {
494             fileNameWithoutExtension = fileName.substring(0, dotIndex);
495         }
496         return fileNameWithoutExtension;
497     }
498 
499     /**
500      * Returns file extension for the given file name
501      * or empty string if file does not have an extension.
502      * We do not use the method from Guava library to reduce Checkstyle's dependencies
503      * on external libraries.
504      *
505      * @param fileNameWithExtension file name with extension.
506      * @return file extension for the given file name
507      *         or empty string if file does not have an extension.
508      */
509     public static String getFileExtension(String fileNameWithExtension) {
510         final String fileName = Path.of(fileNameWithExtension).toString();
511         final int dotIndex = fileName.lastIndexOf('.');
512         final String extension;
513         if (dotIndex == -1) {
514             extension = "";
515         }
516         else {
517             extension = fileName.substring(dotIndex + 1);
518         }
519         return extension;
520     }
521 
522     /**
523      * Checks whether the given string is a valid identifier.
524      *
525      * @param str A string to check.
526      * @return true when the given string contains valid identifier.
527      */
528     public static boolean isIdentifier(String str) {
529         boolean isIdentifier = !str.isEmpty();
530 
531         for (int i = 0; isIdentifier && i < str.length(); i++) {
532             if (i == 0) {
533                 isIdentifier = Character.isJavaIdentifierStart(str.charAt(0));
534             }
535             else {
536                 isIdentifier = Character.isJavaIdentifierPart(str.charAt(i));
537             }
538         }
539 
540         return isIdentifier;
541     }
542 
543     /**
544      * Checks whether the given string is a valid name.
545      *
546      * @param str A string to check.
547      * @return true when the given string contains valid name.
548      */
549     public static boolean isName(String str) {
550         boolean isName = false;
551 
552         final String[] identifiers = str.split("\\.", -1);
553         for (String identifier : identifiers) {
554             isName = isIdentifier(identifier);
555             if (!isName) {
556                 break;
557             }
558         }
559 
560         return isName;
561     }
562 
563     /**
564      * Checks if the value arg is blank by either being null,
565      * empty, or contains only whitespace characters.
566      *
567      * @param value A string to check.
568      * @return true if the arg is blank.
569      */
570     public static boolean isBlank(String value) {
571         return Objects.isNull(value)
572                 || indexOfNonWhitespace(value) >= value.length();
573     }
574 
575     /**
576      * Method to find the index of the first non-whitespace character in a string.
577      *
578      * @param value the string to find the first index of a non-whitespace character for.
579      * @return the index of the first non-whitespace character.
580      */
581     public static int indexOfNonWhitespace(String value) {
582         final int length = value.length();
583         int left = 0;
584         while (left < length) {
585             final int codePointAt = value.codePointAt(left);
586             if (!Character.isWhitespace(codePointAt)) {
587                 break;
588             }
589             left += Character.charCount(codePointAt);
590         }
591         return left;
592     }
593 
594     /**
595      * Converts the Unicode code point at index {@code index} to it's UTF-16
596      * representation, then checks if the character is whitespace. Note that the given
597      * index {@code index} should correspond to the location of the character
598      * to check in the string, not in code points.
599      *
600      * @param codePoints the array of Unicode code points
601      * @param index the index of the character to check
602      * @return true if character at {@code index} is whitespace
603      */
604     public static boolean isCodePointWhitespace(int[] codePoints, int index) {
605         //  We only need to check the first member of a surrogate pair to verify that
606         //  it is not whitespace.
607         final char character = Character.toChars(codePoints[index])[0];
608         return Character.isWhitespace(character);
609     }
610 
611 }