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 * @param languageCodes user specified language codes for the check. 297 * @throws IllegalArgumentException when any item of languageCodes is not valid language code 298 */ 299 private void validateUserSpecifiedLanguageCodes(Set<String> languageCodes) { 300 for (String code : languageCodes) { 301 if (!isValidLanguageCode(code)) { 302 final LocalizedMessage msg = new LocalizedMessage(1, TRANSLATION_BUNDLE, 303 WRONG_LANGUAGE_CODE_KEY, new Object[] {code}, getId(), getClass(), null); 304 final String exceptionMessage = String.format(Locale.ROOT, 305 "%s [%s]", msg.getMessage(), TranslationCheck.class.getSimpleName()); 306 throw new IllegalArgumentException(exceptionMessage); 307 } 308 } 309 } 310 311 /** 312 * Checks whether user specified language code is correct (is contained in available locales). 313 * @param userSpecifiedLanguageCode user specified language code. 314 * @return true if user specified language code is correct. 315 */ 316 private static boolean isValidLanguageCode(final String userSpecifiedLanguageCode) { 317 boolean valid = false; 318 final Locale[] locales = Locale.getAvailableLocales(); 319 for (Locale locale : locales) { 320 if (userSpecifiedLanguageCode.equals(locale.toString())) { 321 valid = true; 322 break; 323 } 324 } 325 return valid; 326 } 327 328 @Override 329 public void beginProcessing(String charset) { 330 filesToProcess.clear(); 331 } 332 333 @Override 334 protected void processFiltered(File file, FileText fileText) { 335 // We just collecting files for processing at finishProcessing() 336 filesToProcess.add(file); 337 } 338 339 @Override 340 public void finishProcessing() { 341 final Set<ResourceBundle> bundles = groupFilesIntoBundles(filesToProcess, baseName); 342 for (ResourceBundle currentBundle : bundles) { 343 checkExistenceOfDefaultTranslation(currentBundle); 344 checkExistenceOfRequiredTranslations(currentBundle); 345 checkTranslationKeys(currentBundle); 346 } 347 } 348 349 /** 350 * Checks an existence of default translation file in the resource bundle. 351 * @param bundle resource bundle. 352 */ 353 private void checkExistenceOfDefaultTranslation(ResourceBundle bundle) { 354 getMissingFileName(bundle, null) 355 .ifPresent(fileName -> logMissingTranslation(bundle.getPath(), fileName)); 356 } 357 358 /** 359 * Checks an existence of translation files in the resource bundle. 360 * The name of translation file begins with the base name of resource bundle which is followed 361 * by '_' and a language code (country and variant are optional), it ends with the extension 362 * suffix. 363 * @param bundle resource bundle. 364 */ 365 private void checkExistenceOfRequiredTranslations(ResourceBundle bundle) { 366 for (String languageCode : requiredTranslations) { 367 getMissingFileName(bundle, languageCode) 368 .ifPresent(fileName -> logMissingTranslation(bundle.getPath(), fileName)); 369 } 370 } 371 372 /** 373 * Returns the name of translation file which is absent in resource bundle or Guava's Optional, 374 * if there is not missing translation. 375 * @param bundle resource bundle. 376 * @param languageCode language code. 377 * @return the name of translation file which is absent in resource bundle or Guava's Optional, 378 * if there is not missing translation. 379 */ 380 private static Optional<String> getMissingFileName(ResourceBundle bundle, String languageCode) { 381 final String fileNameRegexp; 382 final boolean searchForDefaultTranslation; 383 final String extension = bundle.getExtension(); 384 final String baseName = bundle.getBaseName(); 385 if (languageCode == null) { 386 searchForDefaultTranslation = true; 387 fileNameRegexp = String.format(Locale.ROOT, REGEXP_FORMAT_TO_CHECK_DEFAULT_TRANSLATIONS, 388 baseName, extension); 389 } 390 else { 391 searchForDefaultTranslation = false; 392 fileNameRegexp = String.format(Locale.ROOT, 393 REGEXP_FORMAT_TO_CHECK_REQUIRED_TRANSLATIONS, baseName, languageCode, extension); 394 } 395 Optional<String> missingFileName = Optional.empty(); 396 if (!bundle.containsFile(fileNameRegexp)) { 397 if (searchForDefaultTranslation) { 398 missingFileName = Optional.of(String.format(Locale.ROOT, 399 DEFAULT_TRANSLATION_FILE_NAME_FORMATTER, baseName, extension)); 400 } 401 else { 402 missingFileName = Optional.of(String.format(Locale.ROOT, 403 FILE_NAME_WITH_LANGUAGE_CODE_FORMATTER, baseName, languageCode, extension)); 404 } 405 } 406 return missingFileName; 407 } 408 409 /** 410 * Logs that translation file is missing. 411 * @param filePath file path. 412 * @param fileName file name. 413 */ 414 private void logMissingTranslation(String filePath, String fileName) { 415 final MessageDispatcher dispatcher = getMessageDispatcher(); 416 dispatcher.fireFileStarted(filePath); 417 log(1, MSG_KEY_MISSING_TRANSLATION_FILE, fileName); 418 fireErrors(filePath); 419 dispatcher.fireFileFinished(filePath); 420 } 421 422 /** 423 * Groups a set of files into bundles. 424 * Only files, which names match base name regexp pattern will be grouped. 425 * @param files set of files. 426 * @param baseNameRegexp base name regexp pattern. 427 * @return set of ResourceBundles. 428 */ 429 private static Set<ResourceBundle> groupFilesIntoBundles(Set<File> files, 430 Pattern baseNameRegexp) { 431 final Set<ResourceBundle> resourceBundles = new HashSet<>(); 432 for (File currentFile : files) { 433 final String fileName = currentFile.getName(); 434 final String baseName = extractBaseName(fileName); 435 final Matcher baseNameMatcher = baseNameRegexp.matcher(baseName); 436 if (baseNameMatcher.matches()) { 437 final String extension = CommonUtil.getFileExtension(fileName); 438 final String path = getPath(currentFile.getAbsolutePath()); 439 final ResourceBundle newBundle = new ResourceBundle(baseName, path, extension); 440 final Optional<ResourceBundle> bundle = findBundle(resourceBundles, newBundle); 441 if (bundle.isPresent()) { 442 bundle.get().addFile(currentFile); 443 } 444 else { 445 newBundle.addFile(currentFile); 446 resourceBundles.add(newBundle); 447 } 448 } 449 } 450 return resourceBundles; 451 } 452 453 /** 454 * Searches for specific resource bundle in a set of resource bundles. 455 * @param bundles set of resource bundles. 456 * @param targetBundle target bundle to search for. 457 * @return Guava's Optional of resource bundle (present if target bundle is found). 458 */ 459 private static Optional<ResourceBundle> findBundle(Set<ResourceBundle> bundles, 460 ResourceBundle targetBundle) { 461 Optional<ResourceBundle> result = Optional.empty(); 462 for (ResourceBundle currentBundle : bundles) { 463 if (targetBundle.getBaseName().equals(currentBundle.getBaseName()) 464 && targetBundle.getExtension().equals(currentBundle.getExtension()) 465 && targetBundle.getPath().equals(currentBundle.getPath())) { 466 result = Optional.of(currentBundle); 467 break; 468 } 469 } 470 return result; 471 } 472 473 /** 474 * Extracts the base name (the unique prefix) of resource bundle from translation file name. 475 * For example "messages" is the base name of "messages.properties", 476 * "messages_de_AT.properties", "messages_en.properties", etc. 477 * @param fileName the fully qualified name of the translation file. 478 * @return the extracted base name. 479 */ 480 private static String extractBaseName(String fileName) { 481 final String regexp; 482 final Matcher languageCountryVariantMatcher = 483 LANGUAGE_COUNTRY_VARIANT_PATTERN.matcher(fileName); 484 final Matcher languageCountryMatcher = LANGUAGE_COUNTRY_PATTERN.matcher(fileName); 485 final Matcher languageMatcher = LANGUAGE_PATTERN.matcher(fileName); 486 if (languageCountryVariantMatcher.matches()) { 487 regexp = LANGUAGE_COUNTRY_VARIANT_PATTERN.pattern(); 488 } 489 else if (languageCountryMatcher.matches()) { 490 regexp = LANGUAGE_COUNTRY_PATTERN.pattern(); 491 } 492 else if (languageMatcher.matches()) { 493 regexp = LANGUAGE_PATTERN.pattern(); 494 } 495 else { 496 regexp = DEFAULT_TRANSLATION_REGEXP; 497 } 498 // We use substring(...) instead of replace(...), so that the regular expression does 499 // not have to be compiled each time it is used inside 'replace' method. 500 final String removePattern = regexp.substring("^.+".length()); 501 return fileName.replaceAll(removePattern, ""); 502 } 503 504 /** 505 * Extracts path from a file name which contains the path. 506 * For example, if file nam is /xyz/messages.properties, then the method 507 * will return /xyz/. 508 * @param fileNameWithPath file name which contains the path. 509 * @return file path. 510 */ 511 private static String getPath(String fileNameWithPath) { 512 return fileNameWithPath 513 .substring(0, fileNameWithPath.lastIndexOf(File.separator)); 514 } 515 516 /** 517 * Checks resource files in bundle for consistency regarding their keys. 518 * All files in bundle must have the same key set. If this is not the case 519 * an audit event message is posted giving information which key misses in which file. 520 * @param bundle resource bundle. 521 */ 522 private void checkTranslationKeys(ResourceBundle bundle) { 523 final Set<File> filesInBundle = bundle.getFiles(); 524 // build a map from files to the keys they contain 525 final Set<String> allTranslationKeys = new HashSet<>(); 526 final Map<File, Set<String>> filesAssociatedWithKeys = new HashMap<>(); 527 for (File currentFile : filesInBundle) { 528 final Set<String> keysInCurrentFile = getTranslationKeys(currentFile); 529 allTranslationKeys.addAll(keysInCurrentFile); 530 filesAssociatedWithKeys.put(currentFile, keysInCurrentFile); 531 } 532 checkFilesForConsistencyRegardingTheirKeys(filesAssociatedWithKeys, allTranslationKeys); 533 } 534 535 /** 536 * Compares th the specified key set with the key sets of the given translation files (arranged 537 * in a map). All missing keys are reported. 538 * @param fileKeys a Map from translation files to their key sets. 539 * @param keysThatMustExist the set of keys to compare with. 540 */ 541 private void checkFilesForConsistencyRegardingTheirKeys(Map<File, Set<String>> fileKeys, 542 Set<String> keysThatMustExist) { 543 for (Entry<File, Set<String>> fileKey : fileKeys.entrySet()) { 544 final Set<String> currentFileKeys = fileKey.getValue(); 545 final Set<String> missingKeys = keysThatMustExist.stream() 546 .filter(key -> !currentFileKeys.contains(key)).collect(Collectors.toSet()); 547 if (!missingKeys.isEmpty()) { 548 final MessageDispatcher dispatcher = getMessageDispatcher(); 549 final String path = fileKey.getKey().getAbsolutePath(); 550 dispatcher.fireFileStarted(path); 551 for (Object key : missingKeys) { 552 log(1, MSG_KEY, key); 553 } 554 fireErrors(path); 555 dispatcher.fireFileFinished(path); 556 } 557 } 558 } 559 560 /** 561 * Loads the keys from the specified translation file into a set. 562 * @param file translation file. 563 * @return a Set object which holds the loaded keys. 564 */ 565 private Set<String> getTranslationKeys(File file) { 566 Set<String> keys = new HashSet<>(); 567 try (InputStream inStream = Files.newInputStream(file.toPath())) { 568 final Properties translations = new Properties(); 569 translations.load(inStream); 570 keys = translations.stringPropertyNames(); 571 } 572 // -@cs[IllegalCatch] It is better to catch all exceptions since it can throw 573 // a runtime exception. 574 catch (final Exception ex) { 575 logException(ex, file); 576 } 577 return keys; 578 } 579 580 /** 581 * Helper method to log an exception. 582 * @param exception the exception that occurred 583 * @param file the file that could not be processed 584 */ 585 private void logException(Exception exception, File file) { 586 final String[] args; 587 final String key; 588 if (exception instanceof NoSuchFileException) { 589 args = null; 590 key = "general.fileNotFound"; 591 } 592 else { 593 args = new String[] {exception.getMessage()}; 594 key = "general.exception"; 595 } 596 final LocalizedMessage message = 597 new LocalizedMessage( 598 0, 599 Definitions.CHECKSTYLE_BUNDLE, 600 key, 601 args, 602 getId(), 603 getClass(), null); 604 final SortedSet<LocalizedMessage> messages = new TreeSet<>(); 605 messages.add(message); 606 getMessageDispatcher().fireErrors(file.getPath(), messages); 607 log.debug("Exception occurred.", exception); 608 } 609 610 /** Class which represents a resource bundle. */ 611 private static class ResourceBundle { 612 613 /** Bundle base name. */ 614 private final String baseName; 615 /** Common extension of files which are included in the resource bundle. */ 616 private final String extension; 617 /** Common path of files which are included in the resource bundle. */ 618 private final String path; 619 /** Set of files which are included in the resource bundle. */ 620 private final Set<File> files; 621 622 /** 623 * Creates a ResourceBundle object with specific base name, common files extension. 624 * @param baseName bundle base name. 625 * @param path common path of files which are included in the resource bundle. 626 * @param extension common extension of files which are included in the resource bundle. 627 */ 628 /* package */ ResourceBundle(String baseName, String path, String extension) { 629 this.baseName = baseName; 630 this.path = path; 631 this.extension = extension; 632 files = new HashSet<>(); 633 } 634 635 public String getBaseName() { 636 return baseName; 637 } 638 639 public String getPath() { 640 return path; 641 } 642 643 public String getExtension() { 644 return extension; 645 } 646 647 public Set<File> getFiles() { 648 return Collections.unmodifiableSet(files); 649 } 650 651 /** 652 * Adds a file into resource bundle. 653 * @param file file which should be added into resource bundle. 654 */ 655 public void addFile(File file) { 656 files.add(file); 657 } 658 659 /** 660 * Checks whether a resource bundle contains a file which name matches file name regexp. 661 * @param fileNameRegexp file name regexp. 662 * @return true if a resource bundle contains a file which name matches file name regexp. 663 */ 664 public boolean containsFile(String fileNameRegexp) { 665 boolean containsFile = false; 666 for (File currentFile : files) { 667 if (Pattern.matches(fileNameRegexp, currentFile.getName())) { 668 containsFile = true; 669 break; 670 } 671 } 672 return containsFile; 673 } 674 675 } 676 677}