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