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