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