View Javadoc
1   ////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code for adherence to a set of rules.
3   // Copyright (C) 2001-2017 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   * @author <a href="mailto:nesterenko-aleksey@list.ru">Aleksey Nesterenko</a>
50   */
51  public final class CommonUtils {
52  
53      /** Copied from org.apache.commons.lang3.ArrayUtils. */
54      public static final String[] EMPTY_STRING_ARRAY = new String[0];
55      /** Copied from org.apache.commons.lang3.ArrayUtils. */
56      public static final Integer[] EMPTY_INTEGER_OBJECT_ARRAY = new Integer[0];
57      /** Copied from org.apache.commons.lang3.ArrayUtils. */
58      public static final Object[] EMPTY_OBJECT_ARRAY = new Object[0];
59      /** Copied from org.apache.commons.lang3.ArrayUtils. */
60      public static final int[] EMPTY_INT_ARRAY = new int[0];
61      /** Copied from org.apache.commons.lang3.ArrayUtils. */
62      public static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
63      /** Copied from org.apache.commons.lang3.ArrayUtils. */
64      public static final double[] EMPTY_DOUBLE_ARRAY = new double[0];
65  
66      /** Prefix for the exception when unable to find resource. */
67      private static final String UNABLE_TO_FIND_EXCEPTION_PREFIX = "Unable to find: ";
68  
69      /** Symbols with which javadoc starts. */
70      private static final String JAVADOC_START = "/**";
71      /** Symbols with which multiple comment starts. */
72      private static final String BLOCK_MULTIPLE_COMMENT_BEGIN = "/*";
73      /** Symbols with which multiple comment ends. */
74      private static final String BLOCK_MULTIPLE_COMMENT_END = "*/";
75  
76      /** Stop instances being created. **/
77      private CommonUtils() {
78  
79      }
80  
81      /**
82       * Helper method to create a regular expression.
83       *
84       * @param pattern
85       *            the pattern to match
86       * @return a created regexp object
87       * @throws ConversionException
88       *             if unable to create Pattern object.
89       **/
90      public static Pattern createPattern(String pattern) {
91          return createPattern(pattern, 0);
92      }
93  
94      /**
95       * Helper method to create a regular expression with a specific flags.
96       *
97       * @param pattern
98       *            the pattern to match
99       * @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 DetailAST blockCommentBegin = new DetailAST();
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 DetailAST commentContent = new DetailAST();
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 DetailAST blockCommentEnd = new DetailAST();
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 DetailAST blockComment = new DetailAST();
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 DetailAST blockCommentContent = new DetailAST();
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 DetailAST blockCommentClose = new DetailAST();
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 = CommonUtils.class
510                             .getResource(filename);
511                     if (configUrl == null) {
512                         throw new CheckstyleException(UNABLE_TO_FIND_EXCEPTION_PREFIX + filename);
513                     }
514                     uri = configUrl.toURI();
515                 }
516                 catch (final URISyntaxException ex) {
517                     throw new CheckstyleException(UNABLE_TO_FIND_EXCEPTION_PREFIX + filename, ex);
518                 }
519             }
520         }
521 
522         return uri;
523     }
524 
525     /**
526      * Puts part of line, which matches regexp into given template
527      * on positions $n where 'n' is number of matched part in line.
528      * @param template the string to expand.
529      * @param lineToPlaceInTemplate contains expression which should be placed into string.
530      * @param regexp expression to find in comment.
531      * @return the string, based on template filled with given lines
532      */
533     public static String fillTemplateWithStringsByRegexp(
534         String template, String lineToPlaceInTemplate, Pattern regexp) {
535         final Matcher matcher = regexp.matcher(lineToPlaceInTemplate);
536         String result = template;
537         if (matcher.find()) {
538             for (int i = 0; i <= matcher.groupCount(); i++) {
539                 // $n expands comment match like in Pattern.subst().
540                 result = result.replaceAll("\\$" + i, matcher.group(i));
541             }
542         }
543         return result;
544     }
545 
546     /**
547      * Returns file name without extension.
548      * We do not use the method from Guava library to reduce Checkstyle's dependencies
549      * on external libraries.
550      * @param fullFilename file name with extension.
551      * @return file name without extension.
552      */
553     public static String getFileNameWithoutExtension(String fullFilename) {
554         final String fileName = new File(fullFilename).getName();
555         final int dotIndex = fileName.lastIndexOf('.');
556         final String fileNameWithoutExtension;
557         if (dotIndex == -1) {
558             fileNameWithoutExtension = fileName;
559         }
560         else {
561             fileNameWithoutExtension = fileName.substring(0, dotIndex);
562         }
563         return fileNameWithoutExtension;
564     }
565 
566     /**
567      * Returns file extension for the given file name
568      * or empty string if file does not have an extension.
569      * We do not use the method from Guava library to reduce Checkstyle's dependencies
570      * on external libraries.
571      * @param fileNameWithExtension file name with extension.
572      * @return file extension for the given file name
573      *         or empty string if file does not have an extension.
574      */
575     public static String getFileExtension(String fileNameWithExtension) {
576         final String fileName = Paths.get(fileNameWithExtension).toString();
577         final int dotIndex = fileName.lastIndexOf('.');
578         final String extension;
579         if (dotIndex == -1) {
580             extension = "";
581         }
582         else {
583             extension = fileName.substring(dotIndex + 1);
584         }
585         return extension;
586     }
587 
588     /**
589      * Checks whether the given string is a valid identifier.
590      * @param str A string to check.
591      * @return true when the given string contains valid identifier.
592      */
593     public static boolean isIdentifier(String str) {
594         boolean isIdentifier = !str.isEmpty();
595 
596         for (int i = 0; isIdentifier && i < str.length(); i++) {
597             if (i == 0) {
598                 isIdentifier = Character.isJavaIdentifierStart(str.charAt(0));
599             }
600             else {
601                 isIdentifier = Character.isJavaIdentifierPart(str.charAt(i));
602             }
603         }
604 
605         return isIdentifier;
606     }
607 
608     /**
609      * Checks whether the given string is a valid name.
610      * @param str A string to check.
611      * @return true when the given string contains valid name.
612      */
613     public static boolean isName(String str) {
614         boolean isName = !str.isEmpty();
615 
616         final String[] identifiers = str.split("\\.", -1);
617         for (int i = 0; isName && i < identifiers.length; i++) {
618             isName = isIdentifier(identifiers[i]);
619         }
620 
621         return isName;
622     }
623 
624     /**
625      * Checks if the value arg is blank by either being null,
626      * empty, or contains only whitespace characters.
627      * @param value A string to check.
628      * @return true if the arg is blank.
629      */
630     public static boolean isBlank(String value) {
631         boolean result = true;
632         if (value != null && !value.isEmpty()) {
633             for (int i = 0; i < value.length(); i++) {
634                 if (!Character.isWhitespace(value.charAt(i))) {
635                     result = false;
636                     break;
637                 }
638             }
639         }
640         return result;
641     }
642 
643     /**
644      * Checks whether the string contains an integer value.
645      * @param str a string to check
646      * @return true if the given string is an integer, false otherwise.
647      */
648     public static boolean isInt(String str) {
649         boolean isInt;
650         if (str == null) {
651             isInt = false;
652         }
653         else {
654             try {
655                 Integer.parseInt(str);
656                 isInt = true;
657             }
658             catch (NumberFormatException ignored) {
659                 isInt = false;
660             }
661         }
662         return isInt;
663     }
664 }