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