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