View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2024 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.Path;
32  import java.nio.file.Paths;
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 ex) {
108             throw new IllegalArgumentException(
109                 "Failed to initialise regular expression " + pattern, ex);
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 = Paths.get(path);
267             final Path pathBase = Paths.get(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 ex) {
291             throw new IllegalStateException(ex);
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 ex) {
313             throw new IllegalStateException(ex);
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 ex) {
330                 throw new IllegalStateException("Cannot close the stream", ex);
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             final URL url = new URL(filename);
376             uri = url.toURI();
377         }
378         catch (URISyntaxException | MalformedURLException 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) throws CheckstyleException {
394         final URI uri;
395         final File file = new File(filename);
396 
397         if (file.exists()) {
398             uri = file.toURI();
399         }
400         else {
401             final int lastIndexOfClasspathProtocol;
402             if (filename.lastIndexOf(CLASSPATH_URL_PROTOCOL) == 0) {
403                 lastIndexOfClasspathProtocol = CLASSPATH_URL_PROTOCOL.length();
404             }
405             else {
406                 lastIndexOfClasspathProtocol = 0;
407             }
408             uri = getResourceFromClassPath(filename
409                 .substring(lastIndexOfClasspathProtocol));
410         }
411         return uri;
412     }
413 
414     /**
415      * Gets a resource from the classpath.
416      *
417      * @param filename name of file
418      * @return URI of file in classpath
419      * @throws CheckstyleException on failure
420      */
421     public static URI getResourceFromClassPath(String filename) throws CheckstyleException {
422         final URL configUrl;
423         if (filename.charAt(0) == '/') {
424             configUrl = getCheckstyleResource(filename);
425         }
426         else {
427             configUrl = ClassLoader.getSystemResource(filename);
428         }
429 
430         if (configUrl == null) {
431             throw new CheckstyleException(UNABLE_TO_FIND_EXCEPTION_PREFIX + filename);
432         }
433 
434         final URI uri;
435         try {
436             uri = configUrl.toURI();
437         }
438         catch (final URISyntaxException ex) {
439             throw new CheckstyleException(UNABLE_TO_FIND_EXCEPTION_PREFIX + filename, ex);
440         }
441 
442         return uri;
443     }
444 
445     /**
446      * Finds a resource with a given name in the Checkstyle resource bundle.
447      * This method is intended only for internal use in Checkstyle tests for
448      * easy mocking to gain 100% coverage.
449      *
450      * @param name name of the desired resource
451      * @return URI of the resource
452      */
453     public static URL getCheckstyleResource(String name) {
454         return CommonUtil.class.getResource(name);
455     }
456 
457     /**
458      * Puts part of line, which matches regexp into given template
459      * on positions $n where 'n' is number of matched part in line.
460      *
461      * @param template the string to expand.
462      * @param lineToPlaceInTemplate contains expression which should be placed into string.
463      * @param regexp expression to find in comment.
464      * @return the string, based on template filled with given lines
465      */
466     public static String fillTemplateWithStringsByRegexp(
467         String template, String lineToPlaceInTemplate, Pattern regexp) {
468         final Matcher matcher = regexp.matcher(lineToPlaceInTemplate);
469         String result = template;
470         if (matcher.find()) {
471             for (int i = 0; i <= matcher.groupCount(); i++) {
472                 // $n expands comment match like in Pattern.subst().
473                 result = result.replaceAll("\\$" + i, matcher.group(i));
474             }
475         }
476         return result;
477     }
478 
479     /**
480      * Returns file name without extension.
481      * We do not use the method from Guava library to reduce Checkstyle's dependencies
482      * on external libraries.
483      *
484      * @param fullFilename file name with extension.
485      * @return file name without extension.
486      */
487     public static String getFileNameWithoutExtension(String fullFilename) {
488         final String fileName = new File(fullFilename).getName();
489         final int dotIndex = fileName.lastIndexOf('.');
490         final String fileNameWithoutExtension;
491         if (dotIndex == -1) {
492             fileNameWithoutExtension = fileName;
493         }
494         else {
495             fileNameWithoutExtension = fileName.substring(0, dotIndex);
496         }
497         return fileNameWithoutExtension;
498     }
499 
500     /**
501      * Returns file extension for the given file name
502      * or empty string if file does not have an extension.
503      * We do not use the method from Guava library to reduce Checkstyle's dependencies
504      * on external libraries.
505      *
506      * @param fileNameWithExtension file name with extension.
507      * @return file extension for the given file name
508      *         or empty string if file does not have an extension.
509      */
510     public static String getFileExtension(String fileNameWithExtension) {
511         final String fileName = Paths.get(fileNameWithExtension).toString();
512         final int dotIndex = fileName.lastIndexOf('.');
513         final String extension;
514         if (dotIndex == -1) {
515             extension = "";
516         }
517         else {
518             extension = fileName.substring(dotIndex + 1);
519         }
520         return extension;
521     }
522 
523     /**
524      * Checks whether the given string is a valid identifier.
525      *
526      * @param str A string to check.
527      * @return true when the given string contains valid identifier.
528      */
529     public static boolean isIdentifier(String str) {
530         boolean isIdentifier = !str.isEmpty();
531 
532         for (int i = 0; isIdentifier && i < str.length(); i++) {
533             if (i == 0) {
534                 isIdentifier = Character.isJavaIdentifierStart(str.charAt(0));
535             }
536             else {
537                 isIdentifier = Character.isJavaIdentifierPart(str.charAt(i));
538             }
539         }
540 
541         return isIdentifier;
542     }
543 
544     /**
545      * Checks whether the given string is a valid name.
546      *
547      * @param str A string to check.
548      * @return true when the given string contains valid name.
549      */
550     public static boolean isName(String str) {
551         boolean isName = false;
552 
553         final String[] identifiers = str.split("\\.", -1);
554         for (String identifier : identifiers) {
555             isName = isIdentifier(identifier);
556             if (!isName) {
557                 break;
558             }
559         }
560 
561         return isName;
562     }
563 
564     /**
565      * Checks if the value arg is blank by either being null,
566      * empty, or contains only whitespace characters.
567      *
568      * @param value A string to check.
569      * @return true if the arg is blank.
570      */
571     public static boolean isBlank(String value) {
572         return Objects.isNull(value)
573                 || indexOfNonWhitespace(value) >= value.length();
574     }
575 
576     /**
577      * Method to find the index of the first non-whitespace character in a string.
578      *
579      * @param value the string to find the first index of a non-whitespace character for.
580      * @return the index of the first non-whitespace character.
581      */
582     public static int indexOfNonWhitespace(String value) {
583         final int length = value.length();
584         int left = 0;
585         while (left < length) {
586             final int codePointAt = value.codePointAt(left);
587             if (!Character.isWhitespace(codePointAt)) {
588                 break;
589             }
590             left += Character.charCount(codePointAt);
591         }
592         return left;
593     }
594 
595     /**
596      * Converts the Unicode code point at index {@code index} to it's UTF-16
597      * representation, then checks if the character is whitespace. Note that the given
598      * index {@code index} should correspond to the location of the character
599      * to check in the string, not in code points.
600      *
601      * @param codePoints the array of Unicode code points
602      * @param index the index of the character to check
603      * @return true if character at {@code index} is whitespace
604      */
605     public static boolean isCodePointWhitespace(int[] codePoints, int index) {
606         //  We only need to check the first member of a surrogate pair to verify that
607         //  it is not whitespace.
608         final char character = Character.toChars(codePoints[index])[0];
609         return Character.isWhitespace(character);
610     }
611 
612 }