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