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