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