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