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