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