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