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