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