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