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