001//////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code for adherence to a set of rules. 003// Copyright (C) 2001-2019 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 * A <a href="https://checkstyle.org/config.html#Overview">FileSetCheck</a> that 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} Default value is {@code .properties}. 112 * </li> 113 * <li> 114 * Property {@code baseName} - Specify 115 * <a href="https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/ResourceBundle.html"> 116 * Base name</a> of resource bundles which contain message resources. 117 * It helps the check to distinguish config and localization resources. 118 * Default value is {@code "^messages.*$"}. 119 * </li> 120 * <li> 121 * Property {@code requiredTranslations} - Specify language codes of required 122 * translations which must exist in project. 123 * Default value is {@code {}}. 124 * </li> 125 * </ul> 126 * <p> 127 * To configure the check to check only files which have '.properties' and 128 * '.translations' extensions: 129 * </p> 130 * <pre> 131 * <module name="Translation"> 132 * <property name="fileExtensions" value="properties, translations"/> 133 * </module> 134 * </pre> 135 * <p> 136 * Note, that files with the same path and base name but which have different 137 * extensions will be considered as files that belong to different resource bundles. 138 * </p> 139 * <p> 140 * An example of how to configure the check to validate only bundles which base 141 * names start with "ButtonLabels": 142 * </p> 143 * <pre> 144 * <module name="Translation"> 145 * <property name="baseName" value="^ButtonLabels.*$"/> 146 * </module> 147 * </pre> 148 * <p> 149 * To configure the check to check existence of Japanese and French translations: 150 * </p> 151 * <pre> 152 * <module name="Translation"> 153 * <property name="requiredTranslations" value="ja, fr"/> 154 * </module> 155 * </pre> 156 * <p> 157 * The following example shows how the check works if there is a message bundle 158 * which element name contains language code, county code, platform name. 159 * Consider that we have the below configuration: 160 * </p> 161 * <pre> 162 * <module name="Translation"> 163 * <property name="requiredTranslations" value="es, fr, de"/> 164 * </module> 165 * </pre> 166 * <p> 167 * As we can see from the configuration, the TranslationCheck was configured 168 * to check an existence of 'es', 'fr' and 'de' translations. Lets assume that 169 * we have the resource bundle: 170 * </p> 171 * <pre> 172 * messages_home.properties 173 * messages_home_es_US.properties 174 * messages_home_fr_CA_UNIX.properties 175 * </pre> 176 * <p> 177 * Than the check will rise the following violation: "0: Properties file 178 * 'messages_home_de.properties' is missing." 179 * </p> 180 * 181 * @since 3.0 182 */ 183@GlobalStatefulCheck 184public class TranslationCheck extends AbstractFileSetCheck { 185 186 /** 187 * A key is pointing to the warning message text for missing key 188 * in "messages.properties" file. 189 */ 190 public static final String MSG_KEY = "translation.missingKey"; 191 192 /** 193 * A key is pointing to the warning message text for missing translation file 194 * in "messages.properties" file. 195 */ 196 public static final String MSG_KEY_MISSING_TRANSLATION_FILE = 197 "translation.missingTranslationFile"; 198 199 /** Resource bundle which contains messages for TranslationCheck. */ 200 private static final String TRANSLATION_BUNDLE = 201 "com.puppycrawl.tools.checkstyle.checks.messages"; 202 203 /** 204 * A key is pointing to the warning message text for wrong language code 205 * in "messages.properties" file. 206 */ 207 private static final String WRONG_LANGUAGE_CODE_KEY = "translation.wrongLanguageCode"; 208 209 /** 210 * Regexp string for default translation files. 211 * For example, messages.properties. 212 */ 213 private static final String DEFAULT_TRANSLATION_REGEXP = "^.+\\..+$"; 214 215 /** 216 * Regexp pattern for bundles names which end with language code, followed by country code and 217 * variant suffix. For example, messages_es_ES_UNIX.properties. 218 */ 219 private static final Pattern LANGUAGE_COUNTRY_VARIANT_PATTERN = 220 CommonUtil.createPattern("^.+\\_[a-z]{2}\\_[A-Z]{2}\\_[A-Za-z]+\\..+$"); 221 /** 222 * Regexp pattern for bundles names which end with language code, followed by country code 223 * suffix. For example, messages_es_ES.properties. 224 */ 225 private static final Pattern LANGUAGE_COUNTRY_PATTERN = 226 CommonUtil.createPattern("^.+\\_[a-z]{2}\\_[A-Z]{2}\\..+$"); 227 /** 228 * Regexp pattern for bundles names which end with language code suffix. 229 * For example, messages_es.properties. 230 */ 231 private static final Pattern LANGUAGE_PATTERN = 232 CommonUtil.createPattern("^.+\\_[a-z]{2}\\..+$"); 233 234 /** File name format for default translation. */ 235 private static final String DEFAULT_TRANSLATION_FILE_NAME_FORMATTER = "%s.%s"; 236 /** File name format with language code. */ 237 private static final String FILE_NAME_WITH_LANGUAGE_CODE_FORMATTER = "%s_%s.%s"; 238 239 /** Formatting string to form regexp to validate required translations file names. */ 240 private static final String REGEXP_FORMAT_TO_CHECK_REQUIRED_TRANSLATIONS = 241 "^%1$s\\_%2$s(\\_[A-Z]{2})?\\.%3$s$|^%1$s\\_%2$s\\_[A-Z]{2}\\_[A-Za-z]+\\.%3$s$"; 242 /** Formatting string to form regexp to validate default translations file names. */ 243 private static final String REGEXP_FORMAT_TO_CHECK_DEFAULT_TRANSLATIONS = "^%s\\.%s$"; 244 245 /** Logger for TranslationCheck. */ 246 private final Log log; 247 248 /** The files to process. */ 249 private final Set<File> filesToProcess = ConcurrentHashMap.newKeySet(); 250 251 /** 252 * Specify 253 * <a href="https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/ResourceBundle.html"> 254 * Base name</a> of resource bundles which contain message resources. 255 * It helps the check to distinguish config and localization resources. 256 */ 257 private Pattern baseName; 258 259 /** 260 * Specify language codes of required translations which must exist in project. 261 */ 262 private Set<String> requiredTranslations = new HashSet<>(); 263 264 /** 265 * Creates a new {@code TranslationCheck} instance. 266 */ 267 public TranslationCheck() { 268 setFileExtensions("properties"); 269 baseName = CommonUtil.createPattern("^messages.*$"); 270 log = LogFactory.getLog(TranslationCheck.class); 271 } 272 273 /** 274 * Setter to specify 275 * <a href="https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/ResourceBundle.html"> 276 * Base name</a> of resource bundles which contain message resources. 277 * It helps the check to distinguish config and localization resources. 278 * 279 * @param baseName base name regexp. 280 */ 281 public void setBaseName(Pattern baseName) { 282 this.baseName = baseName; 283 } 284 285 /** 286 * Setter to specify language codes of required translations which must exist in project. 287 * 288 * @param translationCodes a comma separated list of language codes. 289 */ 290 public void setRequiredTranslations(String... translationCodes) { 291 requiredTranslations = Arrays.stream(translationCodes).collect(Collectors.toSet()); 292 validateUserSpecifiedLanguageCodes(requiredTranslations); 293 } 294 295 /** 296 * Validates the correctness of user specified language codes for the check. 297 * @param languageCodes user specified language codes for the check. 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}