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.checks; 021 022import java.io.File; 023import java.io.InputStream; 024import java.nio.file.Files; 025import java.nio.file.NoSuchFileException; 026import java.util.Arrays; 027import java.util.Collections; 028import java.util.HashSet; 029import java.util.Locale; 030import java.util.Map; 031import java.util.Map.Entry; 032import java.util.Optional; 033import java.util.Properties; 034import java.util.Set; 035import java.util.SortedSet; 036import java.util.TreeMap; 037import java.util.TreeSet; 038import java.util.regex.Matcher; 039import java.util.regex.Pattern; 040import java.util.stream.Collectors; 041 042import org.apache.commons.logging.Log; 043import org.apache.commons.logging.LogFactory; 044 045import com.puppycrawl.tools.checkstyle.Definitions; 046import com.puppycrawl.tools.checkstyle.GlobalStatefulCheck; 047import com.puppycrawl.tools.checkstyle.LocalizedMessage; 048import com.puppycrawl.tools.checkstyle.api.AbstractFileSetCheck; 049import com.puppycrawl.tools.checkstyle.api.FileText; 050import com.puppycrawl.tools.checkstyle.api.MessageDispatcher; 051import com.puppycrawl.tools.checkstyle.api.Violation; 052import com.puppycrawl.tools.checkstyle.utils.CommonUtil; 053 054/** 055 * <div> 056 * Ensures the correct translation of code by checking property files for consistency 057 * regarding their keys. Two property files describing one and the same context 058 * are consistent if they contain the same keys. TranslationCheck also can check 059 * an existence of required translations which must exist in project, if 060 * {@code requiredTranslations} option is used. 061 * </div> 062 * 063 * <p> 064 * Notes: 065 * Language code for the property {@code requiredTranslations} is composed of 066 * the lowercase, two-letter codes as defined by 067 * <a href="https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes">ISO 639-1</a>. 068 * Default value is empty String Set which means that only the existence of default 069 * translation is checked. Note, if you specify language codes (or just one 070 * language code) of required translations the check will also check for existence 071 * of default translation files in project. 072 * </p> 073 * 074 * <p> 075 * Note: If your project uses preprocessed translation files and the original files do not have the 076 * {@code properties} extension, you can specify additional file extensions 077 * via the {@code fileExtensions} property. 078 * </p> 079 * 080 * <p> 081 * Attention: the check will perform the validation of ISO codes if the option 082 * is used. So, if you specify, for example, "mm" for language code, 083 * TranslationCheck will rise violation that the language code is incorrect. 084 * </p> 085 * 086 * <p> 087 * Attention: this Check could produce false-positives if it is used with 088 * <a href="https://checkstyle.org/config.html#Checker">Checker</a> that use cache 089 * (property "cacheFile") This is known design problem, will be addressed at 090 * <a href="https://github.com/checkstyle/checkstyle/issues/3539">issue</a>. 091 * </p> 092 * <ul> 093 * <li> 094 * Property {@code baseName} - Specify 095 * <a href="https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/ResourceBundle.html"> 096 * Base name</a> of resource bundles which contain message resources. 097 * It helps the check to distinguish config and localization resources. 098 * Type is {@code java.util.regex.Pattern}. 099 * Default value is {@code "^messages.*$"}. 100 * </li> 101 * <li> 102 * Property {@code fileExtensions} - Specify the file extensions of the files to process. 103 * Type is {@code java.lang.String[]}. 104 * Default value is {@code .properties}. 105 * </li> 106 * <li> 107 * Property {@code requiredTranslations} - Specify language codes of required 108 * translations which must exist in project. 109 * Type is {@code java.lang.String[]}. 110 * Default value is {@code ""}. 111 * </li> 112 * </ul> 113 * 114 * <p> 115 * Parent is {@code com.puppycrawl.tools.checkstyle.Checker} 116 * </p> 117 * 118 * <p> 119 * Violation Message Keys: 120 * </p> 121 * <ul> 122 * <li> 123 * {@code translation.missingKey} 124 * </li> 125 * <li> 126 * {@code translation.missingTranslationFile} 127 * </li> 128 * </ul> 129 * 130 * @since 3.0 131 */ 132@GlobalStatefulCheck 133public class TranslationCheck extends AbstractFileSetCheck { 134 135 /** 136 * A key is pointing to the warning message text for missing key 137 * in "messages.properties" file. 138 */ 139 public static final String MSG_KEY = "translation.missingKey"; 140 141 /** 142 * A key is pointing to the warning message text for missing translation file 143 * in "messages.properties" file. 144 */ 145 public static final String MSG_KEY_MISSING_TRANSLATION_FILE = 146 "translation.missingTranslationFile"; 147 148 /** Resource bundle which contains messages for TranslationCheck. */ 149 private static final String TRANSLATION_BUNDLE = 150 "com.puppycrawl.tools.checkstyle.checks.messages"; 151 152 /** 153 * A key is pointing to the warning message text for wrong language code 154 * in "messages.properties" file. 155 */ 156 private static final String WRONG_LANGUAGE_CODE_KEY = "translation.wrongLanguageCode"; 157 158 /** 159 * Regexp string for default translation files. 160 * For example, messages.properties. 161 */ 162 private static final String DEFAULT_TRANSLATION_REGEXP = "^.+\\..+$"; 163 164 /** 165 * Regexp pattern for bundles names which end with language code, followed by country code and 166 * variant suffix. For example, messages_es_ES_UNIX.properties. 167 */ 168 private static final Pattern LANGUAGE_COUNTRY_VARIANT_PATTERN = 169 CommonUtil.createPattern("^.+\\_[a-z]{2}\\_[A-Z]{2}\\_[A-Za-z]+\\..+$"); 170 /** 171 * Regexp pattern for bundles names which end with language code, followed by country code 172 * suffix. For example, messages_es_ES.properties. 173 */ 174 private static final Pattern LANGUAGE_COUNTRY_PATTERN = 175 CommonUtil.createPattern("^.+\\_[a-z]{2}\\_[A-Z]{2}\\..+$"); 176 /** 177 * Regexp pattern for bundles names which end with language code suffix. 178 * For example, messages_es.properties. 179 */ 180 private static final Pattern LANGUAGE_PATTERN = 181 CommonUtil.createPattern("^.+\\_[a-z]{2}\\..+$"); 182 183 /** File name format for default translation. */ 184 private static final String DEFAULT_TRANSLATION_FILE_NAME_FORMATTER = "%s.%s"; 185 /** File name format with language code. */ 186 private static final String FILE_NAME_WITH_LANGUAGE_CODE_FORMATTER = "%s_%s.%s"; 187 188 /** Formatting string to form regexp to validate required translations file names. */ 189 private static final String REGEXP_FORMAT_TO_CHECK_REQUIRED_TRANSLATIONS = 190 "^%1$s\\_%2$s(\\_[A-Z]{2})?\\.%3$s$|^%1$s\\_%2$s\\_[A-Z]{2}\\_[A-Za-z]+\\.%3$s$"; 191 /** Formatting string to form regexp to validate default translations file names. */ 192 private static final String REGEXP_FORMAT_TO_CHECK_DEFAULT_TRANSLATIONS = "^%s\\.%s$"; 193 194 /** Logger for TranslationCheck. */ 195 private final Log log; 196 197 /** The files to process. */ 198 private final Set<File> filesToProcess = new HashSet<>(); 199 200 /** 201 * Specify 202 * <a href="https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/ResourceBundle.html"> 203 * Base name</a> of resource bundles which contain message resources. 204 * It helps the check to distinguish config and localization resources. 205 */ 206 private Pattern baseName; 207 208 /** 209 * Specify language codes of required translations which must exist in project. 210 */ 211 private Set<String> requiredTranslations = new HashSet<>(); 212 213 /** 214 * Creates a new {@code TranslationCheck} instance. 215 */ 216 public TranslationCheck() { 217 setFileExtensions("properties"); 218 baseName = CommonUtil.createPattern("^messages.*$"); 219 log = LogFactory.getLog(TranslationCheck.class); 220 } 221 222 /** 223 * Setter to specify the file extensions of the files to process. 224 * 225 * @param extensions the set of file extensions. A missing 226 * initial '.' character of an extension is automatically added. 227 * @throws IllegalArgumentException is argument is null 228 */ 229 @Override 230 public final void setFileExtensions(String... extensions) { 231 super.setFileExtensions(extensions); 232 } 233 234 /** 235 * Setter to specify 236 * <a href="https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/ResourceBundle.html"> 237 * Base name</a> of resource bundles which contain message resources. 238 * It helps the check to distinguish config and localization resources. 239 * 240 * @param baseName base name regexp. 241 * @since 6.17 242 */ 243 public void setBaseName(Pattern baseName) { 244 this.baseName = baseName; 245 } 246 247 /** 248 * Setter to specify language codes of required translations which must exist in project. 249 * 250 * @param translationCodes language codes. 251 * @since 6.11 252 */ 253 public void setRequiredTranslations(String... translationCodes) { 254 requiredTranslations = Arrays.stream(translationCodes) 255 .collect(Collectors.toUnmodifiableSet()); 256 validateUserSpecifiedLanguageCodes(requiredTranslations); 257 } 258 259 /** 260 * Validates the correctness of user specified language codes for the check. 261 * 262 * @param languageCodes user specified language codes for the check. 263 * @throws IllegalArgumentException when any item of languageCodes is not valid language code 264 */ 265 private void validateUserSpecifiedLanguageCodes(Set<String> languageCodes) { 266 for (String code : languageCodes) { 267 if (!isValidLanguageCode(code)) { 268 final LocalizedMessage msg = new LocalizedMessage(TRANSLATION_BUNDLE, 269 getClass(), WRONG_LANGUAGE_CODE_KEY, code); 270 throw new IllegalArgumentException(msg.getMessage()); 271 } 272 } 273 } 274 275 /** 276 * Checks whether user specified language code is correct (is contained in available locales). 277 * 278 * @param userSpecifiedLanguageCode user specified language code. 279 * @return true if user specified language code is correct. 280 */ 281 private static boolean isValidLanguageCode(final String userSpecifiedLanguageCode) { 282 boolean valid = false; 283 final Locale[] locales = Locale.getAvailableLocales(); 284 for (Locale locale : locales) { 285 if (userSpecifiedLanguageCode.equals(locale.toString())) { 286 valid = true; 287 break; 288 } 289 } 290 return valid; 291 } 292 293 @Override 294 public void beginProcessing(String charset) { 295 filesToProcess.clear(); 296 } 297 298 @Override 299 protected void processFiltered(File file, FileText fileText) { 300 // We are just collecting files for processing at finishProcessing() 301 filesToProcess.add(file); 302 } 303 304 @Override 305 public void finishProcessing() { 306 final Set<ResourceBundle> bundles = groupFilesIntoBundles(filesToProcess, baseName); 307 for (ResourceBundle currentBundle : bundles) { 308 checkExistenceOfDefaultTranslation(currentBundle); 309 checkExistenceOfRequiredTranslations(currentBundle); 310 checkTranslationKeys(currentBundle); 311 } 312 } 313 314 /** 315 * Checks an existence of default translation file in the resource bundle. 316 * 317 * @param bundle resource bundle. 318 */ 319 private void checkExistenceOfDefaultTranslation(ResourceBundle bundle) { 320 getMissingFileName(bundle, null) 321 .ifPresent(fileName -> logMissingTranslation(bundle.getPath(), fileName)); 322 } 323 324 /** 325 * Checks an existence of translation files in the resource bundle. 326 * The name of translation file begins with the base name of resource bundle which is followed 327 * by '_' and a language code (country and variant are optional), it ends with the extension 328 * suffix. 329 * 330 * @param bundle resource bundle. 331 */ 332 private void checkExistenceOfRequiredTranslations(ResourceBundle bundle) { 333 for (String languageCode : requiredTranslations) { 334 getMissingFileName(bundle, languageCode) 335 .ifPresent(fileName -> logMissingTranslation(bundle.getPath(), fileName)); 336 } 337 } 338 339 /** 340 * Returns the name of translation file which is absent in resource bundle or Guava's Optional, 341 * if there is not missing translation. 342 * 343 * @param bundle resource bundle. 344 * @param languageCode language code. 345 * @return the name of translation file which is absent in resource bundle or Guava's Optional, 346 * if there is not missing translation. 347 */ 348 private static Optional<String> getMissingFileName(ResourceBundle bundle, String languageCode) { 349 final String fileNameRegexp; 350 final boolean searchForDefaultTranslation; 351 final String extension = bundle.getExtension(); 352 final String baseName = bundle.getBaseName(); 353 if (languageCode == null) { 354 searchForDefaultTranslation = true; 355 fileNameRegexp = String.format(Locale.ROOT, REGEXP_FORMAT_TO_CHECK_DEFAULT_TRANSLATIONS, 356 baseName, extension); 357 } 358 else { 359 searchForDefaultTranslation = false; 360 fileNameRegexp = String.format(Locale.ROOT, 361 REGEXP_FORMAT_TO_CHECK_REQUIRED_TRANSLATIONS, baseName, languageCode, extension); 362 } 363 Optional<String> missingFileName = Optional.empty(); 364 if (!bundle.containsFile(fileNameRegexp)) { 365 if (searchForDefaultTranslation) { 366 missingFileName = Optional.of(String.format(Locale.ROOT, 367 DEFAULT_TRANSLATION_FILE_NAME_FORMATTER, baseName, extension)); 368 } 369 else { 370 missingFileName = Optional.of(String.format(Locale.ROOT, 371 FILE_NAME_WITH_LANGUAGE_CODE_FORMATTER, baseName, languageCode, extension)); 372 } 373 } 374 return missingFileName; 375 } 376 377 /** 378 * Logs that translation file is missing. 379 * 380 * @param filePath file path. 381 * @param fileName file name. 382 */ 383 private void logMissingTranslation(String filePath, String fileName) { 384 final MessageDispatcher dispatcher = getMessageDispatcher(); 385 dispatcher.fireFileStarted(filePath); 386 log(1, MSG_KEY_MISSING_TRANSLATION_FILE, fileName); 387 fireErrors(filePath); 388 dispatcher.fireFileFinished(filePath); 389 } 390 391 /** 392 * Groups a set of files into bundles. 393 * Only files, which names match base name regexp pattern will be grouped. 394 * 395 * @param files set of files. 396 * @param baseNameRegexp base name regexp pattern. 397 * @return set of ResourceBundles. 398 */ 399 private static Set<ResourceBundle> groupFilesIntoBundles(Set<File> files, 400 Pattern baseNameRegexp) { 401 final Set<ResourceBundle> resourceBundles = new HashSet<>(); 402 for (File currentFile : files) { 403 final String fileName = currentFile.getName(); 404 final String baseName = extractBaseName(fileName); 405 final Matcher baseNameMatcher = baseNameRegexp.matcher(baseName); 406 if (baseNameMatcher.matches()) { 407 final String extension = CommonUtil.getFileExtension(fileName); 408 final String path = getPath(currentFile.getAbsolutePath()); 409 final ResourceBundle newBundle = new ResourceBundle(baseName, path, extension); 410 final Optional<ResourceBundle> bundle = findBundle(resourceBundles, newBundle); 411 if (bundle.isPresent()) { 412 bundle.orElseThrow().addFile(currentFile); 413 } 414 else { 415 newBundle.addFile(currentFile); 416 resourceBundles.add(newBundle); 417 } 418 } 419 } 420 return resourceBundles; 421 } 422 423 /** 424 * Searches for specific resource bundle in a set of resource bundles. 425 * 426 * @param bundles set of resource bundles. 427 * @param targetBundle target bundle to search for. 428 * @return Guava's Optional of resource bundle (present if target bundle is found). 429 */ 430 private static Optional<ResourceBundle> findBundle(Set<ResourceBundle> bundles, 431 ResourceBundle targetBundle) { 432 Optional<ResourceBundle> result = Optional.empty(); 433 for (ResourceBundle currentBundle : bundles) { 434 if (targetBundle.getBaseName().equals(currentBundle.getBaseName()) 435 && targetBundle.getExtension().equals(currentBundle.getExtension()) 436 && targetBundle.getPath().equals(currentBundle.getPath())) { 437 result = Optional.of(currentBundle); 438 break; 439 } 440 } 441 return result; 442 } 443 444 /** 445 * Extracts the base name (the unique prefix) of resource bundle from translation file name. 446 * For example "messages" is the base name of "messages.properties", 447 * "messages_de_AT.properties", "messages_en.properties", etc. 448 * 449 * @param fileName the fully qualified name of the translation file. 450 * @return the extracted base name. 451 */ 452 private static String extractBaseName(String fileName) { 453 final String regexp; 454 final Matcher languageCountryVariantMatcher = 455 LANGUAGE_COUNTRY_VARIANT_PATTERN.matcher(fileName); 456 final Matcher languageCountryMatcher = LANGUAGE_COUNTRY_PATTERN.matcher(fileName); 457 final Matcher languageMatcher = LANGUAGE_PATTERN.matcher(fileName); 458 if (languageCountryVariantMatcher.matches()) { 459 regexp = LANGUAGE_COUNTRY_VARIANT_PATTERN.pattern(); 460 } 461 else if (languageCountryMatcher.matches()) { 462 regexp = LANGUAGE_COUNTRY_PATTERN.pattern(); 463 } 464 else if (languageMatcher.matches()) { 465 regexp = LANGUAGE_PATTERN.pattern(); 466 } 467 else { 468 regexp = DEFAULT_TRANSLATION_REGEXP; 469 } 470 // We use substring(...) instead of replace(...), so that the regular expression does 471 // not have to be compiled each time it is used inside 'replace' method. 472 final String removePattern = regexp.substring("^.+".length()); 473 return fileName.replaceAll(removePattern, ""); 474 } 475 476 /** 477 * Extracts path from a file name which contains the path. 478 * For example, if the file name is /xyz/messages.properties, 479 * then the method will return /xyz/. 480 * 481 * @param fileNameWithPath file name which contains the path. 482 * @return file path. 483 */ 484 private static String getPath(String fileNameWithPath) { 485 return fileNameWithPath 486 .substring(0, fileNameWithPath.lastIndexOf(File.separator)); 487 } 488 489 /** 490 * Checks resource files in bundle for consistency regarding their keys. 491 * All files in bundle must have the same key set. If this is not the case 492 * an audit event message is posted giving information which key misses in which file. 493 * 494 * @param bundle resource bundle. 495 */ 496 private void checkTranslationKeys(ResourceBundle bundle) { 497 final Set<File> filesInBundle = bundle.getFiles(); 498 // build a map from files to the keys they contain 499 final Set<String> allTranslationKeys = new HashSet<>(); 500 final Map<File, Set<String>> filesAssociatedWithKeys = new TreeMap<>(); 501 for (File currentFile : filesInBundle) { 502 final Set<String> keysInCurrentFile = getTranslationKeys(currentFile); 503 allTranslationKeys.addAll(keysInCurrentFile); 504 filesAssociatedWithKeys.put(currentFile, keysInCurrentFile); 505 } 506 checkFilesForConsistencyRegardingTheirKeys(filesAssociatedWithKeys, allTranslationKeys); 507 } 508 509 /** 510 * Compares th the specified key set with the key sets of the given translation files (arranged 511 * in a map). All missing keys are reported. 512 * 513 * @param fileKeys a Map from translation files to their key sets. 514 * @param keysThatMustExist the set of keys to compare with. 515 */ 516 private void checkFilesForConsistencyRegardingTheirKeys(Map<File, Set<String>> fileKeys, 517 Set<String> keysThatMustExist) { 518 for (Entry<File, Set<String>> fileKey : fileKeys.entrySet()) { 519 final Set<String> currentFileKeys = fileKey.getValue(); 520 final Set<String> missingKeys = keysThatMustExist.stream() 521 .filter(key -> !currentFileKeys.contains(key)) 522 .collect(Collectors.toUnmodifiableSet()); 523 if (!missingKeys.isEmpty()) { 524 final MessageDispatcher dispatcher = getMessageDispatcher(); 525 final String path = fileKey.getKey().getAbsolutePath(); 526 dispatcher.fireFileStarted(path); 527 for (Object key : missingKeys) { 528 log(1, MSG_KEY, key); 529 } 530 fireErrors(path); 531 dispatcher.fireFileFinished(path); 532 } 533 } 534 } 535 536 /** 537 * Loads the keys from the specified translation file into a set. 538 * 539 * @param file translation file. 540 * @return a Set object which holds the loaded keys. 541 */ 542 private Set<String> getTranslationKeys(File file) { 543 Set<String> keys = new HashSet<>(); 544 try (InputStream inStream = Files.newInputStream(file.toPath())) { 545 final Properties translations = new Properties(); 546 translations.load(inStream); 547 keys = translations.stringPropertyNames(); 548 } 549 // -@cs[IllegalCatch] It is better to catch all exceptions since it can throw 550 // a runtime exception. 551 catch (final Exception exc) { 552 logException(exc, file); 553 } 554 return keys; 555 } 556 557 /** 558 * Helper method to log an exception. 559 * 560 * @param exception the exception that occurred 561 * @param file the file that could not be processed 562 */ 563 private void logException(Exception exception, File file) { 564 final String[] args; 565 final String key; 566 if (exception instanceof NoSuchFileException) { 567 args = null; 568 key = "general.fileNotFound"; 569 } 570 else { 571 args = new String[] {exception.getMessage()}; 572 key = "general.exception"; 573 } 574 final Violation message = 575 new Violation( 576 0, 577 Definitions.CHECKSTYLE_BUNDLE, 578 key, 579 args, 580 getId(), 581 getClass(), null); 582 final SortedSet<Violation> messages = new TreeSet<>(); 583 messages.add(message); 584 getMessageDispatcher().fireErrors(file.getPath(), messages); 585 log.debug("Exception occurred.", exception); 586 } 587 588 /** Class which represents a resource bundle. */ 589 private static final class ResourceBundle { 590 591 /** Bundle base name. */ 592 private final String baseName; 593 /** Common extension of files which are included in the resource bundle. */ 594 private final String extension; 595 /** Common path of files which are included in the resource bundle. */ 596 private final String path; 597 /** Set of files which are included in the resource bundle. */ 598 private final Set<File> files; 599 600 /** 601 * Creates a ResourceBundle object with specific base name, common files extension. 602 * 603 * @param baseName bundle base name. 604 * @param path common path of files which are included in the resource bundle. 605 * @param extension common extension of files which are included in the resource bundle. 606 */ 607 private ResourceBundle(String baseName, String path, String extension) { 608 this.baseName = baseName; 609 this.path = path; 610 this.extension = extension; 611 files = new HashSet<>(); 612 } 613 614 /** 615 * Returns the bundle base name. 616 * 617 * @return the bundle base name 618 */ 619 public String getBaseName() { 620 return baseName; 621 } 622 623 /** 624 * Returns the common path of files which are included in the resource bundle. 625 * 626 * @return the common path of files 627 */ 628 public String getPath() { 629 return path; 630 } 631 632 /** 633 * Returns the common extension of files which are included in the resource bundle. 634 * 635 * @return the common extension of files 636 */ 637 public String getExtension() { 638 return extension; 639 } 640 641 /** 642 * Returns the set of files which are included in the resource bundle. 643 * 644 * @return the set of files 645 */ 646 public Set<File> getFiles() { 647 return Collections.unmodifiableSet(files); 648 } 649 650 /** 651 * Adds a file into resource bundle. 652 * 653 * @param file file which should be added into resource bundle. 654 */ 655 public void addFile(File file) { 656 files.add(file); 657 } 658 659 /** 660 * Checks whether a resource bundle contains a file which name matches file name regexp. 661 * 662 * @param fileNameRegexp file name regexp. 663 * @return true if a resource bundle contains a file which name matches file name regexp. 664 */ 665 public boolean containsFile(String fileNameRegexp) { 666 boolean containsFile = false; 667 for (File currentFile : files) { 668 if (Pattern.matches(fileNameRegexp, currentFile.getName())) { 669 containsFile = true; 670 break; 671 } 672 } 673 return containsFile; 674 } 675 676 } 677 678}