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