001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2023 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.utils;
021
022import java.io.Closeable;
023import java.io.File;
024import java.io.IOException;
025import java.lang.reflect.Constructor;
026import java.lang.reflect.InvocationTargetException;
027import java.net.MalformedURLException;
028import java.net.URI;
029import java.net.URISyntaxException;
030import java.net.URL;
031import java.nio.file.Path;
032import java.nio.file.Paths;
033import java.util.BitSet;
034import java.util.Objects;
035import java.util.regex.Matcher;
036import java.util.regex.Pattern;
037import java.util.regex.PatternSyntaxException;
038
039import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
040
041/**
042 * Contains utility methods.
043 *
044 */
045public final class CommonUtil {
046
047    /** Default tab width for column reporting. */
048    public static final int DEFAULT_TAB_WIDTH = 8;
049
050    /** For cases where no tokens should be accepted. */
051    public static final BitSet EMPTY_BIT_SET = new BitSet();
052    /** Copied from org.apache.commons.lang3.ArrayUtils. */
053    public static final String[] EMPTY_STRING_ARRAY = new String[0];
054    /** Copied from org.apache.commons.lang3.ArrayUtils. */
055    public static final Integer[] EMPTY_INTEGER_OBJECT_ARRAY = new Integer[0];
056    /** Copied from org.apache.commons.lang3.ArrayUtils. */
057    public static final Object[] EMPTY_OBJECT_ARRAY = new Object[0];
058    /** Copied from org.apache.commons.lang3.ArrayUtils. */
059    public static final int[] EMPTY_INT_ARRAY = new int[0];
060    /** Copied from org.apache.commons.lang3.ArrayUtils. */
061    public static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
062    /** Copied from org.apache.commons.lang3.ArrayUtils. */
063    public static final double[] EMPTY_DOUBLE_ARRAY = new double[0];
064    /** Pseudo URL protocol for loading from the class path. */
065    public static final String CLASSPATH_URL_PROTOCOL = "classpath:";
066
067    /** Prefix for the exception when unable to find resource. */
068    private static final String UNABLE_TO_FIND_EXCEPTION_PREFIX = "Unable to find: ";
069
070    /** Stop instances being created. **/
071    private CommonUtil() {
072    }
073
074    /**
075     * Helper method to create a regular expression.
076     *
077     * @param pattern
078     *            the pattern to match
079     * @return a created regexp object
080     * @throws IllegalArgumentException
081     *             if unable to create Pattern object.
082     **/
083    public static Pattern createPattern(String pattern) {
084        return createPattern(pattern, 0);
085    }
086
087    /**
088     * Helper method to create a regular expression with a specific flags.
089     *
090     * @param pattern
091     *            the pattern to match
092     * @param flags
093     *            the flags to set
094     * @return a created regexp object
095     * @throws IllegalArgumentException
096     *             if unable to create Pattern object.
097     **/
098    public static Pattern createPattern(String pattern, int flags) {
099        try {
100            return Pattern.compile(pattern, flags);
101        }
102        catch (final PatternSyntaxException ex) {
103            throw new IllegalArgumentException(
104                "Failed to initialise regular expression " + pattern, ex);
105        }
106    }
107
108    /**
109     * Returns whether the file extension matches what we are meant to process.
110     *
111     * @param file
112     *            the file to be checked.
113     * @param fileExtensions
114     *            files extensions, empty property in config makes it matches to all.
115     * @return whether there is a match.
116     */
117    public static boolean matchesFileExtension(File file, String... fileExtensions) {
118        boolean result = false;
119        if (fileExtensions == null || fileExtensions.length == 0) {
120            result = true;
121        }
122        else {
123            // normalize extensions so all of them have a leading dot
124            final String[] withDotExtensions = new String[fileExtensions.length];
125            for (int i = 0; i < fileExtensions.length; i++) {
126                final String extension = fileExtensions[i];
127                if (startsWithChar(extension, '.')) {
128                    withDotExtensions[i] = extension;
129                }
130                else {
131                    withDotExtensions[i] = "." + extension;
132                }
133            }
134
135            final String fileName = file.getName();
136            for (final String fileExtension : withDotExtensions) {
137                if (fileName.endsWith(fileExtension)) {
138                    result = true;
139                    break;
140                }
141            }
142        }
143
144        return result;
145    }
146
147    /**
148     * Returns whether the specified string contains only whitespace up to the specified index.
149     *
150     * @param index
151     *            index to check up to
152     * @param line
153     *            the line to check
154     * @return whether there is only whitespace
155     */
156    public static boolean hasWhitespaceBefore(int index, String line) {
157        boolean result = true;
158        for (int i = 0; i < index; i++) {
159            if (!Character.isWhitespace(line.charAt(i))) {
160                result = false;
161                break;
162            }
163        }
164        return result;
165    }
166
167    /**
168     * Returns the length of a string ignoring all trailing whitespace.
169     * It is a pity that there is not a trim() like
170     * method that only removed the trailing whitespace.
171     *
172     * @param line
173     *            the string to process
174     * @return the length of the string ignoring all trailing whitespace
175     **/
176    public static int lengthMinusTrailingWhitespace(String line) {
177        int len = line.length();
178        for (int i = len - 1; i >= 0; i--) {
179            if (!Character.isWhitespace(line.charAt(i))) {
180                break;
181            }
182            len--;
183        }
184        return len;
185    }
186
187    /**
188     * Returns the length of a String prefix with tabs expanded.
189     * Each tab is counted as the number of characters is
190     * takes to jump to the next tab stop.
191     *
192     * @param inputString
193     *            the input String
194     * @param toIdx
195     *            index in string (exclusive) where the calculation stops
196     * @param tabWidth
197     *            the distance between tab stop position.
198     * @return the length of string.substring(0, toIdx) with tabs expanded.
199     */
200    public static int lengthExpandedTabs(String inputString,
201            int toIdx,
202            int tabWidth) {
203        int len = 0;
204        for (int idx = 0; idx < toIdx; idx++) {
205            if (inputString.codePointAt(idx) == '\t') {
206                len = (len / tabWidth + 1) * tabWidth;
207            }
208            else {
209                len++;
210            }
211        }
212        return len;
213    }
214
215    /**
216     * Validates whether passed string is a valid pattern or not.
217     *
218     * @param pattern
219     *            string to validate
220     * @return true if the pattern is valid false otherwise
221     */
222    public static boolean isPatternValid(String pattern) {
223        boolean isValid = true;
224        try {
225            Pattern.compile(pattern);
226        }
227        catch (final PatternSyntaxException ignored) {
228            isValid = false;
229        }
230        return isValid;
231    }
232
233    /**
234     * Returns base class name from qualified name.
235     *
236     * @param type
237     *            the fully qualified name. Cannot be null
238     * @return the base class name from a fully qualified name
239     */
240    public static String baseClassName(String type) {
241        final String className;
242        final int index = type.lastIndexOf('.');
243        if (index == -1) {
244            className = type;
245        }
246        else {
247            className = type.substring(index + 1);
248        }
249        return className;
250    }
251
252    /**
253     * Constructs a normalized relative path between base directory and a given path.
254     *
255     * @param baseDirectory
256     *            the base path to which given path is relativized
257     * @param path
258     *            the path to relativize against base directory
259     * @return the relative normalized path between base directory and
260     *     path or path if base directory is null.
261     */
262    public static String relativizeAndNormalizePath(final String baseDirectory, final String path) {
263        final String resultPath;
264        if (baseDirectory == null) {
265            resultPath = path;
266        }
267        else {
268            final Path pathAbsolute = Paths.get(path).normalize();
269            final Path pathBase = Paths.get(baseDirectory).normalize();
270            resultPath = pathBase.relativize(pathAbsolute).toString();
271        }
272        return resultPath;
273    }
274
275    /**
276     * Tests if this string starts with the specified prefix.
277     * <p>
278     * It is faster version of {@link String#startsWith(String)} optimized for
279     *  one-character prefixes at the expense of
280     * some readability. Suggested by
281     * <a href="https://pmd.github.io/latest/pmd_rules_java_performance.html#simplifystartswith">
282     * SimplifyStartsWith</a> PMD rule:
283     * </p>
284     *
285     * @param value
286     *            the {@code String} to check
287     * @param prefix
288     *            the prefix to find
289     * @return {@code true} if the {@code char} is a prefix of the given {@code String};
290     *     {@code false} otherwise.
291     */
292    public static boolean startsWithChar(String value, char prefix) {
293        return !value.isEmpty() && value.charAt(0) == prefix;
294    }
295
296    /**
297     * Tests if this string ends with the specified suffix.
298     * <p>
299     * It is faster version of {@link String#endsWith(String)} optimized for
300     *  one-character suffixes at the expense of
301     * some readability. Suggested by
302     * <a href="https://pmd.github.io/latest/pmd_rules_java_performance.html#simplifystartswith">
303     * SimplifyStartsWith</a> PMD rule:
304     * </p>
305     *
306     * @param value
307     *            the {@code String} to check
308     * @param suffix
309     *            the suffix to find
310     * @return {@code true} if the {@code char} is a suffix of the given {@code String};
311     *     {@code false} otherwise.
312     */
313    public static boolean endsWithChar(String value, char suffix) {
314        return !value.isEmpty() && value.charAt(value.length() - 1) == suffix;
315    }
316
317    /**
318     * Gets constructor of targetClass.
319     *
320     * @param <T> type of the target class object.
321     * @param targetClass
322     *            from which constructor is returned
323     * @param parameterTypes
324     *            of constructor
325     * @return constructor of targetClass
326     * @throws IllegalStateException if any exception occurs
327     * @see Class#getConstructor(Class[])
328     */
329    public static <T> Constructor<T> getConstructor(Class<T> targetClass,
330                                                    Class<?>... parameterTypes) {
331        try {
332            return targetClass.getConstructor(parameterTypes);
333        }
334        catch (NoSuchMethodException ex) {
335            throw new IllegalStateException(ex);
336        }
337    }
338
339    /**
340     * Returns new instance of a class.
341     *
342     * @param <T>
343     *            type of constructor
344     * @param constructor
345     *            to invoke
346     * @param parameters
347     *            to pass to constructor
348     * @return new instance of class
349     * @throws IllegalStateException if any exception occurs
350     * @see Constructor#newInstance(Object...)
351     */
352    public static <T> T invokeConstructor(Constructor<T> constructor, Object... parameters) {
353        try {
354            return constructor.newInstance(parameters);
355        }
356        catch (InstantiationException | IllegalAccessException | InvocationTargetException ex) {
357            throw new IllegalStateException(ex);
358        }
359    }
360
361    /**
362     * Closes a stream re-throwing IOException as IllegalStateException.
363     *
364     * @param closeable
365     *            Closeable object
366     * @throws IllegalStateException when any IOException occurs
367     */
368    public static void close(Closeable closeable) {
369        if (closeable != null) {
370            try {
371                closeable.close();
372            }
373            catch (IOException ex) {
374                throw new IllegalStateException("Cannot close the stream", ex);
375            }
376        }
377    }
378
379    /**
380     * Resolve the specified filename to a URI.
381     *
382     * @param filename name of the file
383     * @return resolved file URI
384     * @throws CheckstyleException on failure
385     */
386    public static URI getUriByFilename(String filename) throws CheckstyleException {
387        URI uri = getWebOrFileProtocolUri(filename);
388
389        if (uri == null) {
390            uri = getFilepathOrClasspathUri(filename);
391        }
392
393        return uri;
394    }
395
396    /**
397     * Resolves the specified filename containing 'http', 'https', 'ftp',
398     * and 'file' protocols (or any RFC 2396 compliant URL) to a URI.
399     *
400     * @param filename name of the file
401     * @return resolved file URI or null if URL is malformed or non-existent
402     */
403    public static URI getWebOrFileProtocolUri(String filename) {
404        URI uri;
405        try {
406            final URL url = new URL(filename);
407            uri = url.toURI();
408        }
409        catch (URISyntaxException | MalformedURLException ignored) {
410            uri = null;
411        }
412        return uri;
413    }
414
415    /**
416     * Resolves the specified local filename, possibly with 'classpath:'
417     * protocol, to a URI.  First we attempt to create a new file with
418     * given filename, then attempt to load file from class path.
419     *
420     * @param filename name of the file
421     * @return resolved file URI
422     * @throws CheckstyleException on failure
423     */
424    private static URI getFilepathOrClasspathUri(String filename) throws CheckstyleException {
425        final URI uri;
426        final File file = new File(filename);
427
428        if (file.exists()) {
429            uri = file.toURI();
430        }
431        else {
432            final int lastIndexOfClasspathProtocol;
433            if (filename.lastIndexOf(CLASSPATH_URL_PROTOCOL) == 0) {
434                lastIndexOfClasspathProtocol = CLASSPATH_URL_PROTOCOL.length();
435            }
436            else {
437                lastIndexOfClasspathProtocol = 0;
438            }
439            uri = getResourceFromClassPath(filename
440                .substring(lastIndexOfClasspathProtocol));
441        }
442        return uri;
443    }
444
445    /**
446     * Gets a resource from the classpath.
447     *
448     * @param filename name of file
449     * @return URI of file in classpath
450     * @throws CheckstyleException on failure
451     */
452    public static URI getResourceFromClassPath(String filename) throws CheckstyleException {
453        final URL configUrl;
454        if (filename.charAt(0) == '/') {
455            configUrl = getCheckstyleResource(filename);
456        }
457        else {
458            configUrl = ClassLoader.getSystemResource(filename);
459        }
460
461        if (configUrl == null) {
462            throw new CheckstyleException(UNABLE_TO_FIND_EXCEPTION_PREFIX + filename);
463        }
464
465        final URI uri;
466        try {
467            uri = configUrl.toURI();
468        }
469        catch (final URISyntaxException ex) {
470            throw new CheckstyleException(UNABLE_TO_FIND_EXCEPTION_PREFIX + filename, ex);
471        }
472
473        return uri;
474    }
475
476    /**
477     * Finds a resource with a given name in the Checkstyle resource bundle.
478     * This method is intended only for internal use in Checkstyle tests for
479     * easy mocking to gain 100% coverage.
480     *
481     * @param name name of the desired resource
482     * @return URI of the resource
483     */
484    public static URL getCheckstyleResource(String name) {
485        return CommonUtil.class.getResource(name);
486    }
487
488    /**
489     * Puts part of line, which matches regexp into given template
490     * on positions $n where 'n' is number of matched part in line.
491     *
492     * @param template the string to expand.
493     * @param lineToPlaceInTemplate contains expression which should be placed into string.
494     * @param regexp expression to find in comment.
495     * @return the string, based on template filled with given lines
496     */
497    public static String fillTemplateWithStringsByRegexp(
498        String template, String lineToPlaceInTemplate, Pattern regexp) {
499        final Matcher matcher = regexp.matcher(lineToPlaceInTemplate);
500        String result = template;
501        if (matcher.find()) {
502            for (int i = 0; i <= matcher.groupCount(); i++) {
503                // $n expands comment match like in Pattern.subst().
504                result = result.replaceAll("\\$" + i, matcher.group(i));
505            }
506        }
507        return result;
508    }
509
510    /**
511     * Returns file name without extension.
512     * We do not use the method from Guava library to reduce Checkstyle's dependencies
513     * on external libraries.
514     *
515     * @param fullFilename file name with extension.
516     * @return file name without extension.
517     */
518    public static String getFileNameWithoutExtension(String fullFilename) {
519        final String fileName = new File(fullFilename).getName();
520        final int dotIndex = fileName.lastIndexOf('.');
521        final String fileNameWithoutExtension;
522        if (dotIndex == -1) {
523            fileNameWithoutExtension = fileName;
524        }
525        else {
526            fileNameWithoutExtension = fileName.substring(0, dotIndex);
527        }
528        return fileNameWithoutExtension;
529    }
530
531    /**
532     * Returns file extension for the given file name
533     * or empty string if file does not have an extension.
534     * We do not use the method from Guava library to reduce Checkstyle's dependencies
535     * on external libraries.
536     *
537     * @param fileNameWithExtension file name with extension.
538     * @return file extension for the given file name
539     *         or empty string if file does not have an extension.
540     */
541    public static String getFileExtension(String fileNameWithExtension) {
542        final String fileName = Paths.get(fileNameWithExtension).toString();
543        final int dotIndex = fileName.lastIndexOf('.');
544        final String extension;
545        if (dotIndex == -1) {
546            extension = "";
547        }
548        else {
549            extension = fileName.substring(dotIndex + 1);
550        }
551        return extension;
552    }
553
554    /**
555     * Checks whether the given string is a valid identifier.
556     *
557     * @param str A string to check.
558     * @return true when the given string contains valid identifier.
559     */
560    public static boolean isIdentifier(String str) {
561        boolean isIdentifier = !str.isEmpty();
562
563        for (int i = 0; isIdentifier && i < str.length(); i++) {
564            if (i == 0) {
565                isIdentifier = Character.isJavaIdentifierStart(str.charAt(0));
566            }
567            else {
568                isIdentifier = Character.isJavaIdentifierPart(str.charAt(i));
569            }
570        }
571
572        return isIdentifier;
573    }
574
575    /**
576     * Checks whether the given string is a valid name.
577     *
578     * @param str A string to check.
579     * @return true when the given string contains valid name.
580     */
581    public static boolean isName(String str) {
582        boolean isName = !str.isEmpty();
583
584        final String[] identifiers = str.split("\\.", -1);
585        for (int i = 0; isName && i < identifiers.length; i++) {
586            isName = isIdentifier(identifiers[i]);
587        }
588
589        return isName;
590    }
591
592    /**
593     * Checks if the value arg is blank by either being null,
594     * empty, or contains only whitespace characters.
595     *
596     * @param value A string to check.
597     * @return true if the arg is blank.
598     */
599    public static boolean isBlank(String value) {
600        return Objects.isNull(value)
601                || indexOfNonWhitespace(value) >= value.length();
602    }
603
604    /**
605     * Method to find the index of the first non-whitespace character in a string.
606     *
607     * @param value the string to find the first index of a non-whitespace character for.
608     * @return the index of the first non-whitespace character.
609     */
610    public static int indexOfNonWhitespace(String value) {
611        final int length = value.length();
612        int left = 0;
613        while (left < length) {
614            final int codePointAt = value.codePointAt(left);
615            if (!Character.isWhitespace(codePointAt)) {
616                break;
617            }
618            left += Character.charCount(codePointAt);
619        }
620        return left;
621    }
622
623    /**
624     * Checks whether the string contains an integer value.
625     *
626     * @param str a string to check
627     * @return true if the given string is an integer, false otherwise.
628     */
629    public static boolean isInt(String str) {
630        boolean isInt;
631        if (str == null) {
632            isInt = false;
633        }
634        else {
635            try {
636                Integer.parseInt(str);
637                isInt = true;
638            }
639            catch (NumberFormatException ignored) {
640                isInt = false;
641            }
642        }
643        return isInt;
644    }
645
646    /**
647     * Converts the Unicode code point at index {@code index} to it's UTF-16
648     * representation, then checks if the character is whitespace. Note that the given
649     * index {@code index} should correspond to the location of the character
650     * to check in the string, not in code points.
651     *
652     * @param codePoints the array of Unicode code points
653     * @param index the index of the character to check
654     * @return true if character at {@code index} is whitespace
655     */
656    public static boolean isCodePointWhitespace(int[] codePoints, int index) {
657        //  We only need to check the first member of a surrogate pair to verify that
658        //  it is not whitespace.
659        final char character = Character.toChars(codePoints[index])[0];
660        return Character.isWhitespace(character);
661    }
662
663}