001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2025 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 exc) {
108 throw new IllegalArgumentException(
109 "Failed to initialise regular expression " + pattern, exc);
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 = Path.of(path);
267 final Path pathBase = Path.of(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 exc) {
291 throw new IllegalStateException(exc);
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 exc) {
313 throw new IllegalStateException(exc);
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 exc) {
330 throw new IllegalStateException("Cannot close the stream", exc);
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 * @noinspection deprecation
372 * @noinspectionreason Disabled until #17646
373 */
374 public static URI getWebOrFileProtocolUri(String filename) {
375 URI uri;
376 try {
377 final URL url = new URL(filename);
378 uri = url.toURI();
379 }
380 catch (URISyntaxException | MalformedURLException ignored) {
381 uri = null;
382 }
383 return uri;
384 }
385
386 /**
387 * Resolves the specified local filename, possibly with 'classpath:'
388 * protocol, to a URI. First we attempt to create a new file with
389 * given filename, then attempt to load file from class path.
390 *
391 * @param filename name of the file
392 * @return resolved file URI
393 * @throws CheckstyleException on failure
394 */
395 private static URI getFilepathOrClasspathUri(String filename) throws CheckstyleException {
396 final URI uri;
397 final File file = new File(filename);
398
399 if (file.exists()) {
400 uri = file.toURI();
401 }
402 else {
403 final int lastIndexOfClasspathProtocol;
404 if (filename.lastIndexOf(CLASSPATH_URL_PROTOCOL) == 0) {
405 lastIndexOfClasspathProtocol = CLASSPATH_URL_PROTOCOL.length();
406 }
407 else {
408 lastIndexOfClasspathProtocol = 0;
409 }
410 uri = getResourceFromClassPath(filename
411 .substring(lastIndexOfClasspathProtocol));
412 }
413 return uri;
414 }
415
416 /**
417 * Gets a resource from the classpath.
418 *
419 * @param filename name of file
420 * @return URI of file in classpath
421 * @throws CheckstyleException on failure
422 */
423 public static URI getResourceFromClassPath(String filename) throws CheckstyleException {
424 final URL configUrl;
425 if (filename.charAt(0) == '/') {
426 configUrl = getCheckstyleResource(filename);
427 }
428 else {
429 configUrl = ClassLoader.getSystemResource(filename);
430 }
431
432 if (configUrl == null) {
433 throw new CheckstyleException(UNABLE_TO_FIND_EXCEPTION_PREFIX + filename);
434 }
435
436 final URI uri;
437 try {
438 uri = configUrl.toURI();
439 }
440 catch (final URISyntaxException exc) {
441 throw new CheckstyleException(UNABLE_TO_FIND_EXCEPTION_PREFIX + filename, exc);
442 }
443
444 return uri;
445 }
446
447 /**
448 * Finds a resource with a given name in the Checkstyle resource bundle.
449 * This method is intended only for internal use in Checkstyle tests for
450 * easy mocking to gain 100% coverage.
451 *
452 * @param name name of the desired resource
453 * @return URI of the resource
454 */
455 public static URL getCheckstyleResource(String name) {
456 return CommonUtil.class.getResource(name);
457 }
458
459 /**
460 * Puts part of line, which matches regexp into given template
461 * on positions $n where 'n' is number of matched part in line.
462 *
463 * @param template the string to expand.
464 * @param lineToPlaceInTemplate contains expression which should be placed into string.
465 * @param regexp expression to find in comment.
466 * @return the string, based on template filled with given lines
467 */
468 public static String fillTemplateWithStringsByRegexp(
469 String template, String lineToPlaceInTemplate, Pattern regexp) {
470 final Matcher matcher = regexp.matcher(lineToPlaceInTemplate);
471 String result = template;
472 if (matcher.find()) {
473 for (int i = 0; i <= matcher.groupCount(); i++) {
474 // $n expands comment match like in Pattern.subst().
475 result = result.replaceAll("\\$" + i, matcher.group(i));
476 }
477 }
478 return result;
479 }
480
481 /**
482 * Returns file name without extension.
483 * We do not use the method from Guava library to reduce Checkstyle's dependencies
484 * on external libraries.
485 *
486 * @param fullFilename file name with extension.
487 * @return file name without extension.
488 */
489 public static String getFileNameWithoutExtension(String fullFilename) {
490 final String fileName = new File(fullFilename).getName();
491 final int dotIndex = fileName.lastIndexOf('.');
492 final String fileNameWithoutExtension;
493 if (dotIndex == -1) {
494 fileNameWithoutExtension = fileName;
495 }
496 else {
497 fileNameWithoutExtension = fileName.substring(0, dotIndex);
498 }
499 return fileNameWithoutExtension;
500 }
501
502 /**
503 * Returns file extension for the given file name
504 * or empty string if file does not have an extension.
505 * We do not use the method from Guava library to reduce Checkstyle's dependencies
506 * on external libraries.
507 *
508 * @param fileNameWithExtension file name with extension.
509 * @return file extension for the given file name
510 * or empty string if file does not have an extension.
511 */
512 public static String getFileExtension(String fileNameWithExtension) {
513 final String fileName = Paths.get(fileNameWithExtension).toString();
514 final int dotIndex = fileName.lastIndexOf('.');
515 final String extension;
516 if (dotIndex == -1) {
517 extension = "";
518 }
519 else {
520 extension = fileName.substring(dotIndex + 1);
521 }
522 return extension;
523 }
524
525 /**
526 * Checks whether the given string is a valid identifier.
527 *
528 * @param str A string to check.
529 * @return true when the given string contains valid identifier.
530 */
531 public static boolean isIdentifier(String str) {
532 boolean isIdentifier = !str.isEmpty();
533
534 for (int i = 0; isIdentifier && i < str.length(); i++) {
535 if (i == 0) {
536 isIdentifier = Character.isJavaIdentifierStart(str.charAt(0));
537 }
538 else {
539 isIdentifier = Character.isJavaIdentifierPart(str.charAt(i));
540 }
541 }
542
543 return isIdentifier;
544 }
545
546 /**
547 * Checks whether the given string is a valid name.
548 *
549 * @param str A string to check.
550 * @return true when the given string contains valid name.
551 */
552 public static boolean isName(String str) {
553 boolean isName = false;
554
555 final String[] identifiers = str.split("\\.", -1);
556 for (String identifier : identifiers) {
557 isName = isIdentifier(identifier);
558 if (!isName) {
559 break;
560 }
561 }
562
563 return isName;
564 }
565
566 /**
567 * Checks if the value arg is blank by either being null,
568 * empty, or contains only whitespace characters.
569 *
570 * @param value A string to check.
571 * @return true if the arg is blank.
572 */
573 public static boolean isBlank(String value) {
574 return Objects.isNull(value)
575 || indexOfNonWhitespace(value) >= value.length();
576 }
577
578 /**
579 * Method to find the index of the first non-whitespace character in a string.
580 *
581 * @param value the string to find the first index of a non-whitespace character for.
582 * @return the index of the first non-whitespace character.
583 */
584 public static int indexOfNonWhitespace(String value) {
585 final int length = value.length();
586 int left = 0;
587 while (left < length) {
588 final int codePointAt = value.codePointAt(left);
589 if (!Character.isWhitespace(codePointAt)) {
590 break;
591 }
592 left += Character.charCount(codePointAt);
593 }
594 return left;
595 }
596
597 /**
598 * Converts the Unicode code point at index {@code index} to it's UTF-16
599 * representation, then checks if the character is whitespace. Note that the given
600 * index {@code index} should correspond to the location of the character
601 * to check in the string, not in code points.
602 *
603 * @param codePoints the array of Unicode code points
604 * @param index the index of the character to check
605 * @return true if character at {@code index} is whitespace
606 */
607 public static boolean isCodePointWhitespace(int[] codePoints, int index) {
608 // We only need to check the first member of a surrogate pair to verify that
609 // it is not whitespace.
610 final char character = Character.toChars(codePoints[index])[0];
611 return Character.isWhitespace(character);
612 }
613
614}