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.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 org.xml.sax.InputSource;
040
041import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
042
043/**
044 * Contains utility methods.
045 *
046 */
047public final class CommonUtil {
048
049    /** Default tab width for column reporting. */
050    public static final int DEFAULT_TAB_WIDTH = 8;
051
052    /** For cases where no tokens should be accepted. */
053    public static final BitSet EMPTY_BIT_SET = new BitSet();
054    /** Copied from org.apache.commons.lang3.ArrayUtils. */
055    public static final String[] EMPTY_STRING_ARRAY = new String[0];
056    /** Copied from org.apache.commons.lang3.ArrayUtils. */
057    public static final Integer[] EMPTY_INTEGER_OBJECT_ARRAY = new Integer[0];
058    /** Copied from org.apache.commons.lang3.ArrayUtils. */
059    public static final Object[] EMPTY_OBJECT_ARRAY = new Object[0];
060    /** Copied from org.apache.commons.lang3.ArrayUtils. */
061    public static final int[] EMPTY_INT_ARRAY = new int[0];
062    /** Copied from org.apache.commons.lang3.ArrayUtils. */
063    public static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
064    /** Copied from org.apache.commons.lang3.ArrayUtils. */
065    public static final double[] EMPTY_DOUBLE_ARRAY = new double[0];
066    /** Pseudo URL protocol for loading from the class path. */
067    public static final String CLASSPATH_URL_PROTOCOL = "classpath:";
068
069    /** Prefix for the exception when unable to find resource. */
070    private static final String UNABLE_TO_FIND_EXCEPTION_PREFIX = "Unable to find: ";
071
072    /** The extension separator. */
073    private static final String EXTENSION_SEPARATOR = ".";
074
075    /** Stop instances being created. **/
076    private CommonUtil() {
077    }
078
079    /**
080     * Helper method to create a regular expression.
081     *
082     * @param pattern
083     *            the pattern to match
084     * @return a created regexp object
085     * @throws IllegalArgumentException
086     *             if unable to create Pattern object.
087     **/
088    public static Pattern createPattern(String pattern) {
089        return createPattern(pattern, 0);
090    }
091
092    /**
093     * Helper method to create a regular expression with a specific flags.
094     *
095     * @param pattern
096     *            the pattern to match
097     * @param flags
098     *            the flags to set
099     * @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     * @noinspection deprecation
372     * @noinspectionreason Disabled until #17646
373     */
374    public static URI getWebOrFileProtocolUri(String filename) {
375        URI uri;
376        try {
377            final URL url = new URL(filename);
378            uri = url.toURI();
379        }
380        catch (URISyntaxException | MalformedURLException ignored) {
381            uri = null;
382        }
383        return uri;
384    }
385
386    /**
387     * Resolves the specified local filename, possibly with 'classpath:'
388     * protocol, to a URI.  First we attempt to create a new file with
389     * given filename, then attempt to load file from class path.
390     *
391     * @param filename name of the file
392     * @return resolved file URI
393     * @throws CheckstyleException on failure
394     */
395    private static URI getFilepathOrClasspathUri(String filename) throws CheckstyleException {
396        final URI uri;
397        final File file = new File(filename);
398
399        if (file.exists()) {
400            uri = file.toURI();
401        }
402        else {
403            final int lastIndexOfClasspathProtocol;
404            if (filename.lastIndexOf(CLASSPATH_URL_PROTOCOL) == 0) {
405                lastIndexOfClasspathProtocol = CLASSPATH_URL_PROTOCOL.length();
406            }
407            else {
408                lastIndexOfClasspathProtocol = 0;
409            }
410            uri = getResourceFromClassPath(filename
411                .substring(lastIndexOfClasspathProtocol));
412        }
413        return uri;
414    }
415
416    /**
417     * Gets a resource from the classpath.
418     *
419     * @param filename name of file
420     * @return URI of file in classpath
421     * @throws CheckstyleException on failure
422     */
423    public static URI getResourceFromClassPath(String filename) throws CheckstyleException {
424        final URL configUrl;
425        if (filename.charAt(0) == '/') {
426            configUrl = getCheckstyleResource(filename);
427        }
428        else {
429            configUrl = ClassLoader.getSystemResource(filename);
430        }
431
432        if (configUrl == null) {
433            throw new CheckstyleException(UNABLE_TO_FIND_EXCEPTION_PREFIX + filename);
434        }
435
436        final URI uri;
437        try {
438            uri = configUrl.toURI();
439        }
440        catch (final URISyntaxException exc) {
441            throw new CheckstyleException(UNABLE_TO_FIND_EXCEPTION_PREFIX + filename, exc);
442        }
443
444        return uri;
445    }
446
447    /**
448     * Finds a resource with a given name in the Checkstyle resource bundle.
449     * This method is intended only for internal use in Checkstyle tests for
450     * easy mocking to gain 100% coverage.
451     *
452     * @param name name of the desired resource
453     * @return URI of the resource
454     */
455    public static URL getCheckstyleResource(String name) {
456        return CommonUtil.class.getResource(name);
457    }
458
459    /**
460     * Puts part of line, which matches regexp into given template
461     * on positions $n where 'n' is number of matched part in line.
462     *
463     * @param template the string to expand.
464     * @param lineToPlaceInTemplate contains expression which should be placed into string.
465     * @param regexp expression to find in comment.
466     * @return the string, based on template filled with given lines
467     */
468    public static String fillTemplateWithStringsByRegexp(
469        String template, String lineToPlaceInTemplate, Pattern regexp) {
470        final Matcher matcher = regexp.matcher(lineToPlaceInTemplate);
471        String result = template;
472        if (matcher.find()) {
473            for (int i = 0; i <= matcher.groupCount(); i++) {
474                // $n expands comment match like in Pattern.subst().
475                result = result.replaceAll("\\$" + i, matcher.group(i));
476            }
477        }
478        return result;
479    }
480
481    /**
482     * Returns file name without extension.
483     * We do not use the method from Guava library to reduce Checkstyle's dependencies
484     * on external libraries.
485     *
486     * @param fullFilename file name with extension.
487     * @return file name without extension.
488     */
489    public static String getFileNameWithoutExtension(String fullFilename) {
490        final String fileName = new File(fullFilename).getName();
491        final int dotIndex = fileName.lastIndexOf('.');
492        final String fileNameWithoutExtension;
493        if (dotIndex == -1) {
494            fileNameWithoutExtension = fileName;
495        }
496        else {
497            fileNameWithoutExtension = fileName.substring(0, dotIndex);
498        }
499        return fileNameWithoutExtension;
500    }
501
502    /**
503     * Returns file extension for the given file name
504     * or empty string if file does not have an extension.
505     * We do not use the method from Guava library to reduce Checkstyle's dependencies
506     * on external libraries.
507     *
508     * @param fileNameWithExtension file name with extension.
509     * @return file extension for the given file name
510     *         or empty string if file does not have an extension.
511     */
512    public static String getFileExtension(String fileNameWithExtension) {
513        final String fileName = Paths.get(fileNameWithExtension).toString();
514        final int dotIndex = fileName.lastIndexOf('.');
515        final String extension;
516        if (dotIndex == -1) {
517            extension = "";
518        }
519        else {
520            extension = fileName.substring(dotIndex + 1);
521        }
522        return extension;
523    }
524
525    /**
526     * Checks whether the given string is a valid identifier.
527     *
528     * @param str A string to check.
529     * @return true when the given string contains valid identifier.
530     */
531    public static boolean isIdentifier(String str) {
532        boolean isIdentifier = !str.isEmpty();
533
534        for (int i = 0; isIdentifier && i < str.length(); i++) {
535            if (i == 0) {
536                isIdentifier = Character.isJavaIdentifierStart(str.charAt(0));
537            }
538            else {
539                isIdentifier = Character.isJavaIdentifierPart(str.charAt(i));
540            }
541        }
542
543        return isIdentifier;
544    }
545
546    /**
547     * Checks whether the given string is a valid name.
548     *
549     * @param str A string to check.
550     * @return true when the given string contains valid name.
551     */
552    public static boolean isName(String str) {
553        boolean isName = false;
554
555        final String[] identifiers = str.split("\\.", -1);
556        for (String identifier : identifiers) {
557            isName = isIdentifier(identifier);
558            if (!isName) {
559                break;
560            }
561        }
562
563        return isName;
564    }
565
566    /**
567     * Checks if the value arg is blank by either being null,
568     * empty, or contains only whitespace characters.
569     *
570     * @param value A string to check.
571     * @return true if the arg is blank.
572     */
573    public static boolean isBlank(String value) {
574        return Objects.isNull(value)
575                || indexOfNonWhitespace(value) >= value.length();
576    }
577
578    /**
579     * Method to find the index of the first non-whitespace character in a string.
580     *
581     * @param value the string to find the first index of a non-whitespace character for.
582     * @return the index of the first non-whitespace character.
583     */
584    public static int indexOfNonWhitespace(String value) {
585        final int length = value.length();
586        int left = 0;
587        while (left < length) {
588            final int codePointAt = value.codePointAt(left);
589            if (!Character.isWhitespace(codePointAt)) {
590                break;
591            }
592            left += Character.charCount(codePointAt);
593        }
594        return left;
595    }
596
597    /**
598     * Converts the Unicode code point at index {@code index} to it's UTF-16
599     * representation, then checks if the character is whitespace. Note that the given
600     * index {@code index} should correspond to the location of the character
601     * to check in the string, not in code points.
602     *
603     * @param codePoints the array of Unicode code points
604     * @param index the index of the character to check
605     * @return true if character at {@code index} is whitespace
606     */
607    public static boolean isCodePointWhitespace(int[] codePoints, int index) {
608        //  We only need to check the first member of a surrogate pair to verify that
609        //  it is not whitespace.
610        final char character = Character.toChars(codePoints[index])[0];
611        return Character.isWhitespace(character);
612    }
613
614}