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