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