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