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