1   ////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code for adherence to a set of rules.
3   // Copyright (C) 2001-2019 the original author or authors.
4   //
5   // This library is free software; you can redistribute it and/or
6   // modify it under the terms of the GNU Lesser General Public
7   // License as published by the Free Software Foundation; either
8   // version 2.1 of the License, or (at your option) any later version.
9   //
10  // This library is distributed in the hope that it will be useful,
11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  // Lesser General Public License for more details.
14  //
15  // You should have received a copy of the GNU Lesser General Public
16  // License along with this library; if not, write to the Free Software
17  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18  ////////////////////////////////////////////////////////////////////////////////
19  
20  package com.puppycrawl.tools.checkstyle.utils;
21  
22  import java.io.Closeable;
23  import java.io.File;
24  import java.io.IOException;
25  import java.lang.reflect.Constructor;
26  import java.lang.reflect.InvocationTargetException;
27  import java.net.MalformedURLException;
28  import java.net.URI;
29  import java.net.URISyntaxException;
30  import java.net.URL;
31  import java.nio.file.Path;
32  import java.nio.file.Paths;
33  import java.util.AbstractMap;
34  import java.util.Map;
35  import java.util.regex.Matcher;
36  import java.util.regex.Pattern;
37  import java.util.regex.PatternSyntaxException;
38  
39  import org.apache.commons.beanutils.ConversionException;
40  
41  import antlr.Token;
42  import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
43  import com.puppycrawl.tools.checkstyle.api.DetailAST;
44  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
45  
46  /**
47   * Contains utility methods.
48   *
49   */
50  public final class CommonUtil {
51  
52      /** Default tab width for column reporting. */
53      public static final int DEFAULT_TAB_WIDTH = 8;
54  
55      /** Copied from org.apache.commons.lang3.ArrayUtils. */
56      public static final String[] EMPTY_STRING_ARRAY = new String[0];
57      /** Copied from org.apache.commons.lang3.ArrayUtils. */
58      public static final Integer[] EMPTY_INTEGER_OBJECT_ARRAY = new Integer[0];
59      /** Copied from org.apache.commons.lang3.ArrayUtils. */
60      public static final Object[] EMPTY_OBJECT_ARRAY = new Object[0];
61      /** Copied from org.apache.commons.lang3.ArrayUtils. */
62      public static final int[] EMPTY_INT_ARRAY = new int[0];
63      /** Copied from org.apache.commons.lang3.ArrayUtils. */
64      public static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
65      /** Copied from org.apache.commons.lang3.ArrayUtils. */
66      public static final double[] EMPTY_DOUBLE_ARRAY = new double[0];
67  
68      /** Prefix for the exception when unable to find resource. */
69      private static final String UNABLE_TO_FIND_EXCEPTION_PREFIX = "Unable to find: ";
70  
71      /** Symbols with which javadoc starts. */
72      private static final String JAVADOC_START = "/**";
73      /** Symbols with which multiple comment starts. */
74      private static final String BLOCK_MULTIPLE_COMMENT_BEGIN = "/*";
75      /** Symbols with which multiple comment ends. */
76      private static final String BLOCK_MULTIPLE_COMMENT_END = "*/";
77  
78      /** Stop instances being created. **/
79      private CommonUtil() {
80      }
81  
82      /**
83       * Helper method to create a regular expression.
84       *
85       * @param pattern
86       *            the pattern to match
87       * @return a created regexp object
88       * @throws ConversionException
89       *             if unable to create Pattern object.
90       **/
91      public static Pattern createPattern(String pattern) {
92          return createPattern(pattern, 0);
93      }
94  
95      /**
96       * Helper method to create a regular expression with a specific flags.
97       *
98       * @param pattern
99       *            the pattern to match
100      * @param flags
101      *            the flags to set
102      * @return a created regexp object
103      * @throws IllegalArgumentException
104      *             if unable to create Pattern object.
105      **/
106     public static Pattern createPattern(String pattern, int flags) {
107         try {
108             return Pattern.compile(pattern, flags);
109         }
110         catch (final PatternSyntaxException ex) {
111             throw new IllegalArgumentException(
112                 "Failed to initialise regular expression " + pattern, ex);
113         }
114     }
115 
116     /**
117      * Create block comment from string content.
118      * @param content comment content.
119      * @return DetailAST block comment
120      */
121     public static DetailAST createBlockCommentNode(String content) {
122         final DetailAST blockCommentBegin = new DetailAST();
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 DetailAST commentContent = new DetailAST();
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 DetailAST blockCommentEnd = new DetailAST();
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      * @param token
148      *        Token object.
149      * @return DetailAST with BLOCK_COMMENT type.
150      */
151     public static DetailAST createBlockCommentNode(Token token) {
152         final DetailAST blockComment = new DetailAST();
153         blockComment.initialize(TokenTypes.BLOCK_COMMENT_BEGIN, BLOCK_MULTIPLE_COMMENT_BEGIN);
154 
155         // column counting begins from 0
156         blockComment.setColumnNo(token.getColumn() - 1);
157         blockComment.setLineNo(token.getLine());
158 
159         final DetailAST blockCommentContent = new DetailAST();
160         blockCommentContent.setType(TokenTypes.COMMENT_CONTENT);
161 
162         // column counting begins from 0
163         // plus length of '/*'
164         blockCommentContent.setColumnNo(token.getColumn() - 1 + 2);
165         blockCommentContent.setLineNo(token.getLine());
166         blockCommentContent.setText(token.getText());
167 
168         final DetailAST blockCommentClose = new DetailAST();
169         blockCommentClose.initialize(TokenTypes.BLOCK_COMMENT_END, BLOCK_MULTIPLE_COMMENT_END);
170 
171         final Map.Entry<Integer, Integer> linesColumns = countLinesColumns(
172                 token.getText(), token.getLine(), token.getColumn());
173         blockCommentClose.setLineNo(linesColumns.getKey());
174         blockCommentClose.setColumnNo(linesColumns.getValue());
175 
176         blockComment.addChild(blockCommentContent);
177         blockComment.addChild(blockCommentClose);
178         return blockComment;
179     }
180 
181     /**
182      * Count lines and columns (in last line) in text.
183      * @param text
184      *        String.
185      * @param initialLinesCnt
186      *        initial value of lines counter.
187      * @param initialColumnsCnt
188      *        initial value of columns counter.
189      * @return entry(pair), first element is lines counter, second - columns
190      *         counter.
191      */
192     private static Map.Entry<Integer, Integer> countLinesColumns(
193             String text, int initialLinesCnt, int initialColumnsCnt) {
194         int lines = initialLinesCnt;
195         int columns = initialColumnsCnt;
196         boolean foundCr = false;
197         for (char c : text.toCharArray()) {
198             if (c == '\n') {
199                 foundCr = false;
200                 lines++;
201                 columns = 0;
202             }
203             else {
204                 if (foundCr) {
205                     foundCr = false;
206                     lines++;
207                     columns = 0;
208                 }
209                 if (c == '\r') {
210                     foundCr = true;
211                 }
212                 columns++;
213             }
214         }
215         if (foundCr) {
216             lines++;
217             columns = 0;
218         }
219         return new AbstractMap.SimpleEntry<>(lines, columns);
220     }
221 
222     /**
223      * Returns whether the file extension matches what we are meant to process.
224      *
225      * @param file
226      *            the file to be checked.
227      * @param fileExtensions
228      *            files extensions, empty property in config makes it matches to all.
229      * @return whether there is a match.
230      */
231     public static boolean matchesFileExtension(File file, String... fileExtensions) {
232         boolean result = false;
233         if (fileExtensions == null || fileExtensions.length == 0) {
234             result = true;
235         }
236         else {
237             // normalize extensions so all of them have a leading dot
238             final String[] withDotExtensions = new String[fileExtensions.length];
239             for (int i = 0; i < fileExtensions.length; i++) {
240                 final String extension = fileExtensions[i];
241                 if (startsWithChar(extension, '.')) {
242                     withDotExtensions[i] = extension;
243                 }
244                 else {
245                     withDotExtensions[i] = "." + extension;
246                 }
247             }
248 
249             final String fileName = file.getName();
250             for (final String fileExtension : withDotExtensions) {
251                 if (fileName.endsWith(fileExtension)) {
252                     result = true;
253                     break;
254                 }
255             }
256         }
257 
258         return result;
259     }
260 
261     /**
262      * Returns whether the specified string contains only whitespace up to the specified index.
263      *
264      * @param index
265      *            index to check up to
266      * @param line
267      *            the line to check
268      * @return whether there is only whitespace
269      */
270     public static boolean hasWhitespaceBefore(int index, String line) {
271         boolean result = true;
272         for (int i = 0; i < index; i++) {
273             if (!Character.isWhitespace(line.charAt(i))) {
274                 result = false;
275                 break;
276             }
277         }
278         return result;
279     }
280 
281     /**
282      * Returns the length of a string ignoring all trailing whitespace.
283      * It is a pity that there is not a trim() like
284      * method that only removed the trailing whitespace.
285      *
286      * @param line
287      *            the string to process
288      * @return the length of the string ignoring all trailing whitespace
289      **/
290     public static int lengthMinusTrailingWhitespace(String line) {
291         int len = line.length();
292         for (int i = len - 1; i >= 0; i--) {
293             if (!Character.isWhitespace(line.charAt(i))) {
294                 break;
295             }
296             len--;
297         }
298         return len;
299     }
300 
301     /**
302      * Returns the length of a String prefix with tabs expanded.
303      * Each tab is counted as the number of characters is
304      * takes to jump to the next tab stop.
305      *
306      * @param inputString
307      *            the input String
308      * @param toIdx
309      *            index in string (exclusive) where the calculation stops
310      * @param tabWidth
311      *            the distance between tab stop position.
312      * @return the length of string.substring(0, toIdx) with tabs expanded.
313      */
314     public static int lengthExpandedTabs(String inputString,
315             int toIdx,
316             int tabWidth) {
317         int len = 0;
318         for (int idx = 0; idx < toIdx; idx++) {
319             if (inputString.charAt(idx) == '\t') {
320                 len = (len / tabWidth + 1) * tabWidth;
321             }
322             else {
323                 len++;
324             }
325         }
326         return len;
327     }
328 
329     /**
330      * Validates whether passed string is a valid pattern or not.
331      *
332      * @param pattern
333      *            string to validate
334      * @return true if the pattern is valid false otherwise
335      */
336     public static boolean isPatternValid(String pattern) {
337         boolean isValid = true;
338         try {
339             Pattern.compile(pattern);
340         }
341         catch (final PatternSyntaxException ignored) {
342             isValid = false;
343         }
344         return isValid;
345     }
346 
347     /**
348      * Returns base class name from qualified name.
349      * @param type
350      *            the fully qualified name. Cannot be null
351      * @return the base class name from a fully qualified name
352      */
353     public static String baseClassName(String type) {
354         final String className;
355         final int index = type.lastIndexOf('.');
356         if (index == -1) {
357             className = type;
358         }
359         else {
360             className = type.substring(index + 1);
361         }
362         return className;
363     }
364 
365     /**
366      * Constructs a normalized relative path between base directory and a given path.
367      *
368      * @param baseDirectory
369      *            the base path to which given path is relativized
370      * @param path
371      *            the path to relativize against base directory
372      * @return the relative normalized path between base directory and
373      *     path or path if base directory is null.
374      */
375     public static String relativizeAndNormalizePath(final String baseDirectory, final String path) {
376         final String resultPath;
377         if (baseDirectory == null) {
378             resultPath = path;
379         }
380         else {
381             final Path pathAbsolute = Paths.get(path).normalize();
382             final Path pathBase = Paths.get(baseDirectory).normalize();
383             resultPath = pathBase.relativize(pathAbsolute).toString();
384         }
385         return resultPath;
386     }
387 
388     /**
389      * Tests if this string starts with the specified prefix.
390      * <p>
391      * It is faster version of {@link String#startsWith(String)} optimized for
392      *  one-character prefixes at the expense of
393      * some readability. Suggested by SimplifyStartsWith PMD rule:
394      * http://pmd.sourceforge.net/pmd-5.3.1/pmd-java/rules/java/optimizations.html#SimplifyStartsWith
395      * </p>
396      *
397      * @param value
398      *            the {@code String} to check
399      * @param prefix
400      *            the prefix to find
401      * @return {@code true} if the {@code char} is a prefix of the given {@code String};
402      *  {@code false} otherwise.
403      */
404     public static boolean startsWithChar(String value, char prefix) {
405         return !value.isEmpty() && value.charAt(0) == prefix;
406     }
407 
408     /**
409      * Tests if this string ends with the specified suffix.
410      * <p>
411      * It is faster version of {@link String#endsWith(String)} optimized for
412      *  one-character suffixes at the expense of
413      * some readability. Suggested by SimplifyStartsWith PMD rule:
414      * http://pmd.sourceforge.net/pmd-5.3.1/pmd-java/rules/java/optimizations.html#SimplifyStartsWith
415      * </p>
416      *
417      * @param value
418      *            the {@code String} to check
419      * @param suffix
420      *            the suffix to find
421      * @return {@code true} if the {@code char} is a suffix of the given {@code String};
422      *  {@code false} otherwise.
423      */
424     public static boolean endsWithChar(String value, char suffix) {
425         return !value.isEmpty() && value.charAt(value.length() - 1) == suffix;
426     }
427 
428     /**
429      * Gets constructor of targetClass.
430      * @param targetClass
431      *            from which constructor is returned
432      * @param parameterTypes
433      *            of constructor
434      * @param <T> type of the target class object.
435      * @return constructor of targetClass or {@link IllegalStateException} if any exception occurs
436      * @see Class#getConstructor(Class[])
437      */
438     public static <T> Constructor<T> getConstructor(Class<T> targetClass,
439                                                     Class<?>... parameterTypes) {
440         try {
441             return targetClass.getConstructor(parameterTypes);
442         }
443         catch (NoSuchMethodException ex) {
444             throw new IllegalStateException(ex);
445         }
446     }
447 
448     /**
449      * Returns new instance of a class.
450      * @param constructor
451      *            to invoke
452      * @param parameters
453      *            to pass to constructor
454      * @param <T>
455      *            type of constructor
456      * @return new instance of class or {@link IllegalStateException} if any exception occurs
457      * @see Constructor#newInstance(Object...)
458      */
459     public static <T> T invokeConstructor(Constructor<T> constructor, Object... parameters) {
460         try {
461             return constructor.newInstance(parameters);
462         }
463         catch (InstantiationException | IllegalAccessException | InvocationTargetException ex) {
464             throw new IllegalStateException(ex);
465         }
466     }
467 
468     /**
469      * Closes a stream re-throwing IOException as IllegalStateException.
470      *
471      * @param closeable
472      *            Closeable object
473      */
474     public static void close(Closeable closeable) {
475         if (closeable != null) {
476             try {
477                 closeable.close();
478             }
479             catch (IOException ex) {
480                 throw new IllegalStateException("Cannot close the stream", ex);
481             }
482         }
483     }
484 
485     /**
486      * Resolve the specified filename to a URI.
487      * @param filename name os the file
488      * @return resolved header file URI
489      * @throws CheckstyleException on failure
490      */
491     public static URI getUriByFilename(String filename) throws CheckstyleException {
492         // figure out if this is a File or a URL
493         URI uri;
494         try {
495             final URL url = new URL(filename);
496             uri = url.toURI();
497         }
498         catch (final URISyntaxException | MalformedURLException ignored) {
499             uri = null;
500         }
501 
502         if (uri == null) {
503             final File file = new File(filename);
504             if (file.exists()) {
505                 uri = file.toURI();
506             }
507             else {
508                 // check to see if the file is in the classpath
509                 try {
510                     final URL configUrl = CommonUtil.class
511                             .getResource(filename);
512                     if (configUrl == null) {
513                         throw new CheckstyleException(UNABLE_TO_FIND_EXCEPTION_PREFIX + filename);
514                     }
515                     uri = configUrl.toURI();
516                 }
517                 catch (final URISyntaxException ex) {
518                     throw new CheckstyleException(UNABLE_TO_FIND_EXCEPTION_PREFIX + filename, ex);
519                 }
520             }
521         }
522 
523         return uri;
524     }
525 
526     /**
527      * Puts part of line, which matches regexp into given template
528      * on positions $n where 'n' is number of matched part in line.
529      * @param template the string to expand.
530      * @param lineToPlaceInTemplate contains expression which should be placed into string.
531      * @param regexp expression to find in comment.
532      * @return the string, based on template filled with given lines
533      */
534     public static String fillTemplateWithStringsByRegexp(
535         String template, String lineToPlaceInTemplate, Pattern regexp) {
536         final Matcher matcher = regexp.matcher(lineToPlaceInTemplate);
537         String result = template;
538         if (matcher.find()) {
539             for (int i = 0; i <= matcher.groupCount(); i++) {
540                 // $n expands comment match like in Pattern.subst().
541                 result = result.replaceAll("\\$" + i, matcher.group(i));
542             }
543         }
544         return result;
545     }
546 
547     /**
548      * Returns file name without extension.
549      * We do not use the method from Guava library to reduce Checkstyle's dependencies
550      * on external libraries.
551      * @param fullFilename file name with extension.
552      * @return file name without extension.
553      */
554     public static String getFileNameWithoutExtension(String fullFilename) {
555         final String fileName = new File(fullFilename).getName();
556         final int dotIndex = fileName.lastIndexOf('.');
557         final String fileNameWithoutExtension;
558         if (dotIndex == -1) {
559             fileNameWithoutExtension = fileName;
560         }
561         else {
562             fileNameWithoutExtension = fileName.substring(0, dotIndex);
563         }
564         return fileNameWithoutExtension;
565     }
566 
567     /**
568      * Returns file extension for the given file name
569      * or empty string if file does not have an extension.
570      * We do not use the method from Guava library to reduce Checkstyle's dependencies
571      * on external libraries.
572      * @param fileNameWithExtension file name with extension.
573      * @return file extension for the given file name
574      *         or empty string if file does not have an extension.
575      */
576     public static String getFileExtension(String fileNameWithExtension) {
577         final String fileName = Paths.get(fileNameWithExtension).toString();
578         final int dotIndex = fileName.lastIndexOf('.');
579         final String extension;
580         if (dotIndex == -1) {
581             extension = "";
582         }
583         else {
584             extension = fileName.substring(dotIndex + 1);
585         }
586         return extension;
587     }
588 
589     /**
590      * Checks whether the given string is a valid identifier.
591      * @param str A string to check.
592      * @return true when the given string contains valid identifier.
593      */
594     public static boolean isIdentifier(String str) {
595         boolean isIdentifier = !str.isEmpty();
596 
597         for (int i = 0; isIdentifier && i < str.length(); i++) {
598             if (i == 0) {
599                 isIdentifier = Character.isJavaIdentifierStart(str.charAt(0));
600             }
601             else {
602                 isIdentifier = Character.isJavaIdentifierPart(str.charAt(i));
603             }
604         }
605 
606         return isIdentifier;
607     }
608 
609     /**
610      * Checks whether the given string is a valid name.
611      * @param str A string to check.
612      * @return true when the given string contains valid name.
613      */
614     public static boolean isName(String str) {
615         boolean isName = !str.isEmpty();
616 
617         final String[] identifiers = str.split("\\.", -1);
618         for (int i = 0; isName && i < identifiers.length; i++) {
619             isName = isIdentifier(identifiers[i]);
620         }
621 
622         return isName;
623     }
624 
625     /**
626      * Checks if the value arg is blank by either being null,
627      * empty, or contains only whitespace characters.
628      * @param value A string to check.
629      * @return true if the arg is blank.
630      */
631     public static boolean isBlank(String value) {
632         boolean result = true;
633         if (value != null && !value.isEmpty()) {
634             for (int i = 0; i < value.length(); i++) {
635                 if (!Character.isWhitespace(value.charAt(i))) {
636                     result = false;
637                     break;
638                 }
639             }
640         }
641         return result;
642     }
643 
644     /**
645      * Checks whether the string contains an integer value.
646      * @param str a string to check
647      * @return true if the given string is an integer, false otherwise.
648      */
649     public static boolean isInt(String str) {
650         boolean isInt;
651         if (str == null) {
652             isInt = false;
653         }
654         else {
655             try {
656                 Integer.parseInt(str);
657                 isInt = true;
658             }
659             catch (NumberFormatException ignored) {
660                 isInt = false;
661             }
662         }
663         return isInt;
664     }
665 
666 }