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