View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2024 the original author or authors.
4   //
5   // This library is free software; you can redistribute it and/or
6   // modify it under the terms of the GNU Lesser General Public
7   // License as published by the Free Software Foundation; either
8   // version 2.1 of the License, or (at your option) any later version.
9   //
10  // This library is distributed in the hope that it will be useful,
11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  // Lesser General Public License for more details.
14  //
15  // You should have received a copy of the GNU Lesser General Public
16  // License along with this library; if not, write to the Free Software
17  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18  ///////////////////////////////////////////////////////////////////////////////////////////////
19  
20  package com.puppycrawl.tools.checkstyle.checks;
21  
22  import java.io.File;
23  import java.io.InputStream;
24  import java.nio.file.Files;
25  import java.nio.file.NoSuchFileException;
26  import java.util.Arrays;
27  import java.util.Collections;
28  import java.util.HashSet;
29  import java.util.Locale;
30  import java.util.Map;
31  import java.util.Map.Entry;
32  import java.util.Optional;
33  import java.util.Properties;
34  import java.util.Set;
35  import java.util.SortedSet;
36  import java.util.TreeMap;
37  import java.util.TreeSet;
38  import java.util.concurrent.ConcurrentHashMap;
39  import java.util.regex.Matcher;
40  import java.util.regex.Pattern;
41  import java.util.stream.Collectors;
42  
43  import org.apache.commons.logging.Log;
44  import org.apache.commons.logging.LogFactory;
45  
46  import com.puppycrawl.tools.checkstyle.Definitions;
47  import com.puppycrawl.tools.checkstyle.GlobalStatefulCheck;
48  import com.puppycrawl.tools.checkstyle.LocalizedMessage;
49  import com.puppycrawl.tools.checkstyle.api.AbstractFileSetCheck;
50  import com.puppycrawl.tools.checkstyle.api.FileText;
51  import com.puppycrawl.tools.checkstyle.api.MessageDispatcher;
52  import com.puppycrawl.tools.checkstyle.api.Violation;
53  import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
54  
55  /**
56   * <p>
57   * Ensures the correct translation of code by checking property files for consistency
58   * regarding their keys. Two property files describing one and the same context
59   * are consistent if they contain the same keys. TranslationCheck also can check
60   * an existence of required translations which must exist in project, if
61   * {@code requiredTranslations} option is used.
62   * </p>
63   * <p>
64   * Consider the following properties file in the same directory:
65   * </p>
66   * <pre>
67   * #messages.properties
68   * hello=Hello
69   * cancel=Cancel
70   *
71   * #messages_de.properties
72   * hell=Hallo
73   * ok=OK
74   * </pre>
75   * <p>
76   * The Translation check will find the typo in the German {@code hello} key,
77   * the missing {@code ok} key in the default resource file and the missing
78   * {@code cancel} key in the German resource file:
79   * </p>
80   * <pre>
81   * messages_de.properties: Key 'hello' missing.
82   * messages_de.properties: Key 'cancel' missing.
83   * messages.properties: Key 'hell' missing.
84   * messages.properties: Key 'ok' missing.
85   * </pre>
86   * <p>
87   * Language code for the property {@code requiredTranslations} is composed of
88   * the lowercase, two-letter codes as defined by
89   * <a href="https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes">ISO 639-1</a>.
90   * Default value is empty String Set which means that only the existence of default
91   * translation is checked. Note, if you specify language codes (or just one
92   * language code) of required translations the check will also check for existence
93   * of default translation files in project.
94   * </p>
95   * <p>
96   * Note: If your project uses preprocessed translation files and the original files do not have the
97   * {@code properties} extension, you can specify additional file extensions
98   * via the {@code fileExtensions} property.
99   * </p>
100  * <p>
101  * Attention: the check will perform the validation of ISO codes if the option
102  * is used. So, if you specify, for example, "mm" for language code,
103  * TranslationCheck will rise violation that the language code is incorrect.
104  * </p>
105  * <p>
106  * Attention: this Check could produce false-positives if it is used with
107  * <a href="https://checkstyle.org/config.html#Checker">Checker</a> that use cache
108  * (property "cacheFile") This is known design problem, will be addressed at
109  * <a href="https://github.com/checkstyle/checkstyle/issues/3539">issue</a>.
110  * </p>
111  * <ul>
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  * Type is {@code java.util.regex.Pattern}.
118  * Default value is {@code "^messages.*$"}.
119  * </li>
120  * <li>
121  * Property {@code fileExtensions} - Specify the file extensions of the files to process.
122  * Type is {@code java.lang.String[]}.
123  * Default value is {@code .properties}.
124  * </li>
125  * <li>
126  * Property {@code requiredTranslations} - Specify language codes of required
127  * translations which must exist in project.
128  * Type is {@code java.lang.String[]}.
129  * Default value is {@code ""}.
130  * </li>
131  * </ul>
132  * <p>
133  * Parent is {@code com.puppycrawl.tools.checkstyle.Checker}
134  * </p>
135  * <p>
136  * Violation Message Keys:
137  * </p>
138  * <ul>
139  * <li>
140  * {@code translation.missingKey}
141  * </li>
142  * <li>
143  * {@code translation.missingTranslationFile}
144  * </li>
145  * </ul>
146  *
147  * @since 3.0
148  */
149 @GlobalStatefulCheck
150 public class TranslationCheck extends AbstractFileSetCheck {
151 
152     /**
153      * A key is pointing to the warning message text for missing key
154      * in "messages.properties" file.
155      */
156     public static final String MSG_KEY = "translation.missingKey";
157 
158     /**
159      * A key is pointing to the warning message text for missing translation file
160      * in "messages.properties" file.
161      */
162     public static final String MSG_KEY_MISSING_TRANSLATION_FILE =
163         "translation.missingTranslationFile";
164 
165     /** Resource bundle which contains messages for TranslationCheck. */
166     private static final String TRANSLATION_BUNDLE =
167         "com.puppycrawl.tools.checkstyle.checks.messages";
168 
169     /**
170      * A key is pointing to the warning message text for wrong language code
171      * in "messages.properties" file.
172      */
173     private static final String WRONG_LANGUAGE_CODE_KEY = "translation.wrongLanguageCode";
174 
175     /**
176      * Regexp string for default translation files.
177      * For example, messages.properties.
178      */
179     private static final String DEFAULT_TRANSLATION_REGEXP = "^.+\\..+$";
180 
181     /**
182      * Regexp pattern for bundles names which end with language code, followed by country code and
183      * variant suffix. For example, messages_es_ES_UNIX.properties.
184      */
185     private static final Pattern LANGUAGE_COUNTRY_VARIANT_PATTERN =
186         CommonUtil.createPattern("^.+\\_[a-z]{2}\\_[A-Z]{2}\\_[A-Za-z]+\\..+$");
187     /**
188      * Regexp pattern for bundles names which end with language code, followed by country code
189      * suffix. For example, messages_es_ES.properties.
190      */
191     private static final Pattern LANGUAGE_COUNTRY_PATTERN =
192         CommonUtil.createPattern("^.+\\_[a-z]{2}\\_[A-Z]{2}\\..+$");
193     /**
194      * Regexp pattern for bundles names which end with language code suffix.
195      * For example, messages_es.properties.
196      */
197     private static final Pattern LANGUAGE_PATTERN =
198         CommonUtil.createPattern("^.+\\_[a-z]{2}\\..+$");
199 
200     /** File name format for default translation. */
201     private static final String DEFAULT_TRANSLATION_FILE_NAME_FORMATTER = "%s.%s";
202     /** File name format with language code. */
203     private static final String FILE_NAME_WITH_LANGUAGE_CODE_FORMATTER = "%s_%s.%s";
204 
205     /** Formatting string to form regexp to validate required translations file names. */
206     private static final String REGEXP_FORMAT_TO_CHECK_REQUIRED_TRANSLATIONS =
207         "^%1$s\\_%2$s(\\_[A-Z]{2})?\\.%3$s$|^%1$s\\_%2$s\\_[A-Z]{2}\\_[A-Za-z]+\\.%3$s$";
208     /** Formatting string to form regexp to validate default translations file names. */
209     private static final String REGEXP_FORMAT_TO_CHECK_DEFAULT_TRANSLATIONS = "^%s\\.%s$";
210 
211     /** Logger for TranslationCheck. */
212     private final Log log;
213 
214     /** The files to process. */
215     private final Set<File> filesToProcess = ConcurrentHashMap.newKeySet();
216 
217     /**
218      * Specify
219      * <a href="https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/ResourceBundle.html">
220      * Base name</a> of resource bundles which contain message resources.
221      * It helps the check to distinguish config and localization resources.
222      */
223     private Pattern baseName;
224 
225     /**
226      * Specify language codes of required translations which must exist in project.
227      */
228     private Set<String> requiredTranslations = new HashSet<>();
229 
230     /**
231      * Creates a new {@code TranslationCheck} instance.
232      */
233     public TranslationCheck() {
234         setFileExtensions("properties");
235         baseName = CommonUtil.createPattern("^messages.*$");
236         log = LogFactory.getLog(TranslationCheck.class);
237     }
238 
239     /**
240      * Setter to specify
241      * <a href="https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/ResourceBundle.html">
242      * Base name</a> of resource bundles which contain message resources.
243      * It helps the check to distinguish config and localization resources.
244      *
245      * @param baseName base name regexp.
246      * @since 6.17
247      */
248     public void setBaseName(Pattern baseName) {
249         this.baseName = baseName;
250     }
251 
252     /**
253      * Setter to specify language codes of required translations which must exist in project.
254      *
255      * @param translationCodes language codes.
256      * @since 6.11
257      */
258     public void setRequiredTranslations(String... translationCodes) {
259         requiredTranslations = Arrays.stream(translationCodes)
260             .collect(Collectors.toUnmodifiableSet());
261         validateUserSpecifiedLanguageCodes(requiredTranslations);
262     }
263 
264     /**
265      * Validates the correctness of user specified language codes for the check.
266      *
267      * @param languageCodes user specified language codes for the check.
268      * @throws IllegalArgumentException when any item of languageCodes is not valid language code
269      */
270     private void validateUserSpecifiedLanguageCodes(Set<String> languageCodes) {
271         for (String code : languageCodes) {
272             if (!isValidLanguageCode(code)) {
273                 final LocalizedMessage msg = new LocalizedMessage(TRANSLATION_BUNDLE,
274                         getClass(), WRONG_LANGUAGE_CODE_KEY, code);
275                 throw new IllegalArgumentException(msg.getMessage());
276             }
277         }
278     }
279 
280     /**
281      * Checks whether user specified language code is correct (is contained in available locales).
282      *
283      * @param userSpecifiedLanguageCode user specified language code.
284      * @return true if user specified language code is correct.
285      */
286     private static boolean isValidLanguageCode(final String userSpecifiedLanguageCode) {
287         boolean valid = false;
288         final Locale[] locales = Locale.getAvailableLocales();
289         for (Locale locale : locales) {
290             if (userSpecifiedLanguageCode.equals(locale.toString())) {
291                 valid = true;
292                 break;
293             }
294         }
295         return valid;
296     }
297 
298     @Override
299     public void beginProcessing(String charset) {
300         filesToProcess.clear();
301     }
302 
303     @Override
304     protected void processFiltered(File file, FileText fileText) {
305         // We are just collecting files for processing at finishProcessing()
306         filesToProcess.add(file);
307     }
308 
309     @Override
310     public void finishProcessing() {
311         final Set<ResourceBundle> bundles = groupFilesIntoBundles(filesToProcess, baseName);
312         for (ResourceBundle currentBundle : bundles) {
313             checkExistenceOfDefaultTranslation(currentBundle);
314             checkExistenceOfRequiredTranslations(currentBundle);
315             checkTranslationKeys(currentBundle);
316         }
317     }
318 
319     /**
320      * Checks an existence of default translation file in the resource bundle.
321      *
322      * @param bundle resource bundle.
323      */
324     private void checkExistenceOfDefaultTranslation(ResourceBundle bundle) {
325         getMissingFileName(bundle, null)
326             .ifPresent(fileName -> logMissingTranslation(bundle.getPath(), fileName));
327     }
328 
329     /**
330      * Checks an existence of translation files in the resource bundle.
331      * The name of translation file begins with the base name of resource bundle which is followed
332      * by '_' and a language code (country and variant are optional), it ends with the extension
333      * suffix.
334      *
335      * @param bundle resource bundle.
336      */
337     private void checkExistenceOfRequiredTranslations(ResourceBundle bundle) {
338         for (String languageCode : requiredTranslations) {
339             getMissingFileName(bundle, languageCode)
340                 .ifPresent(fileName -> logMissingTranslation(bundle.getPath(), fileName));
341         }
342     }
343 
344     /**
345      * Returns the name of translation file which is absent in resource bundle or Guava's Optional,
346      * if there is not missing translation.
347      *
348      * @param bundle resource bundle.
349      * @param languageCode language code.
350      * @return the name of translation file which is absent in resource bundle or Guava's Optional,
351      *         if there is not missing translation.
352      */
353     private static Optional<String> getMissingFileName(ResourceBundle bundle, String languageCode) {
354         final String fileNameRegexp;
355         final boolean searchForDefaultTranslation;
356         final String extension = bundle.getExtension();
357         final String baseName = bundle.getBaseName();
358         if (languageCode == null) {
359             searchForDefaultTranslation = true;
360             fileNameRegexp = String.format(Locale.ROOT, REGEXP_FORMAT_TO_CHECK_DEFAULT_TRANSLATIONS,
361                     baseName, extension);
362         }
363         else {
364             searchForDefaultTranslation = false;
365             fileNameRegexp = String.format(Locale.ROOT,
366                 REGEXP_FORMAT_TO_CHECK_REQUIRED_TRANSLATIONS, baseName, languageCode, extension);
367         }
368         Optional<String> missingFileName = Optional.empty();
369         if (!bundle.containsFile(fileNameRegexp)) {
370             if (searchForDefaultTranslation) {
371                 missingFileName = Optional.of(String.format(Locale.ROOT,
372                         DEFAULT_TRANSLATION_FILE_NAME_FORMATTER, baseName, extension));
373             }
374             else {
375                 missingFileName = Optional.of(String.format(Locale.ROOT,
376                         FILE_NAME_WITH_LANGUAGE_CODE_FORMATTER, baseName, languageCode, extension));
377             }
378         }
379         return missingFileName;
380     }
381 
382     /**
383      * Logs that translation file is missing.
384      *
385      * @param filePath file path.
386      * @param fileName file name.
387      */
388     private void logMissingTranslation(String filePath, String fileName) {
389         final MessageDispatcher dispatcher = getMessageDispatcher();
390         dispatcher.fireFileStarted(filePath);
391         log(1, MSG_KEY_MISSING_TRANSLATION_FILE, fileName);
392         fireErrors(filePath);
393         dispatcher.fireFileFinished(filePath);
394     }
395 
396     /**
397      * Groups a set of files into bundles.
398      * Only files, which names match base name regexp pattern will be grouped.
399      *
400      * @param files set of files.
401      * @param baseNameRegexp base name regexp pattern.
402      * @return set of ResourceBundles.
403      */
404     private static Set<ResourceBundle> groupFilesIntoBundles(Set<File> files,
405                                                              Pattern baseNameRegexp) {
406         final Set<ResourceBundle> resourceBundles = new HashSet<>();
407         for (File currentFile : files) {
408             final String fileName = currentFile.getName();
409             final String baseName = extractBaseName(fileName);
410             final Matcher baseNameMatcher = baseNameRegexp.matcher(baseName);
411             if (baseNameMatcher.matches()) {
412                 final String extension = CommonUtil.getFileExtension(fileName);
413                 final String path = getPath(currentFile.getAbsolutePath());
414                 final ResourceBundle newBundle = new ResourceBundle(baseName, path, extension);
415                 final Optional<ResourceBundle> bundle = findBundle(resourceBundles, newBundle);
416                 if (bundle.isPresent()) {
417                     bundle.orElseThrow().addFile(currentFile);
418                 }
419                 else {
420                     newBundle.addFile(currentFile);
421                     resourceBundles.add(newBundle);
422                 }
423             }
424         }
425         return resourceBundles;
426     }
427 
428     /**
429      * Searches for specific resource bundle in a set of resource bundles.
430      *
431      * @param bundles set of resource bundles.
432      * @param targetBundle target bundle to search for.
433      * @return Guava's Optional of resource bundle (present if target bundle is found).
434      */
435     private static Optional<ResourceBundle> findBundle(Set<ResourceBundle> bundles,
436                                                        ResourceBundle targetBundle) {
437         Optional<ResourceBundle> result = Optional.empty();
438         for (ResourceBundle currentBundle : bundles) {
439             if (targetBundle.getBaseName().equals(currentBundle.getBaseName())
440                     && targetBundle.getExtension().equals(currentBundle.getExtension())
441                     && targetBundle.getPath().equals(currentBundle.getPath())) {
442                 result = Optional.of(currentBundle);
443                 break;
444             }
445         }
446         return result;
447     }
448 
449     /**
450      * Extracts the base name (the unique prefix) of resource bundle from translation file name.
451      * For example "messages" is the base name of "messages.properties",
452      * "messages_de_AT.properties", "messages_en.properties", etc.
453      *
454      * @param fileName the fully qualified name of the translation file.
455      * @return the extracted base name.
456      */
457     private static String extractBaseName(String fileName) {
458         final String regexp;
459         final Matcher languageCountryVariantMatcher =
460             LANGUAGE_COUNTRY_VARIANT_PATTERN.matcher(fileName);
461         final Matcher languageCountryMatcher = LANGUAGE_COUNTRY_PATTERN.matcher(fileName);
462         final Matcher languageMatcher = LANGUAGE_PATTERN.matcher(fileName);
463         if (languageCountryVariantMatcher.matches()) {
464             regexp = LANGUAGE_COUNTRY_VARIANT_PATTERN.pattern();
465         }
466         else if (languageCountryMatcher.matches()) {
467             regexp = LANGUAGE_COUNTRY_PATTERN.pattern();
468         }
469         else if (languageMatcher.matches()) {
470             regexp = LANGUAGE_PATTERN.pattern();
471         }
472         else {
473             regexp = DEFAULT_TRANSLATION_REGEXP;
474         }
475         // We use substring(...) instead of replace(...), so that the regular expression does
476         // not have to be compiled each time it is used inside 'replace' method.
477         final String removePattern = regexp.substring("^.+".length());
478         return fileName.replaceAll(removePattern, "");
479     }
480 
481     /**
482      * Extracts path from a file name which contains the path.
483      * For example, if the file name is /xyz/messages.properties,
484      * then the method will return /xyz/.
485      *
486      * @param fileNameWithPath file name which contains the path.
487      * @return file path.
488      */
489     private static String getPath(String fileNameWithPath) {
490         return fileNameWithPath
491             .substring(0, fileNameWithPath.lastIndexOf(File.separator));
492     }
493 
494     /**
495      * Checks resource files in bundle for consistency regarding their keys.
496      * All files in bundle must have the same key set. If this is not the case
497      * an audit event message is posted giving information which key misses in which file.
498      *
499      * @param bundle resource bundle.
500      */
501     private void checkTranslationKeys(ResourceBundle bundle) {
502         final Set<File> filesInBundle = bundle.getFiles();
503         // build a map from files to the keys they contain
504         final Set<String> allTranslationKeys = new HashSet<>();
505         final Map<File, Set<String>> filesAssociatedWithKeys = new TreeMap<>();
506         for (File currentFile : filesInBundle) {
507             final Set<String> keysInCurrentFile = getTranslationKeys(currentFile);
508             allTranslationKeys.addAll(keysInCurrentFile);
509             filesAssociatedWithKeys.put(currentFile, keysInCurrentFile);
510         }
511         checkFilesForConsistencyRegardingTheirKeys(filesAssociatedWithKeys, allTranslationKeys);
512     }
513 
514     /**
515      * Compares th the specified key set with the key sets of the given translation files (arranged
516      * in a map). All missing keys are reported.
517      *
518      * @param fileKeys a Map from translation files to their key sets.
519      * @param keysThatMustExist the set of keys to compare with.
520      */
521     private void checkFilesForConsistencyRegardingTheirKeys(Map<File, Set<String>> fileKeys,
522                                                             Set<String> keysThatMustExist) {
523         for (Entry<File, Set<String>> fileKey : fileKeys.entrySet()) {
524             final Set<String> currentFileKeys = fileKey.getValue();
525             final Set<String> missingKeys = keysThatMustExist.stream()
526                 .filter(key -> !currentFileKeys.contains(key))
527                 .collect(Collectors.toUnmodifiableSet());
528             if (!missingKeys.isEmpty()) {
529                 final MessageDispatcher dispatcher = getMessageDispatcher();
530                 final String path = fileKey.getKey().getAbsolutePath();
531                 dispatcher.fireFileStarted(path);
532                 for (Object key : missingKeys) {
533                     log(1, MSG_KEY, key);
534                 }
535                 fireErrors(path);
536                 dispatcher.fireFileFinished(path);
537             }
538         }
539     }
540 
541     /**
542      * Loads the keys from the specified translation file into a set.
543      *
544      * @param file translation file.
545      * @return a Set object which holds the loaded keys.
546      */
547     private Set<String> getTranslationKeys(File file) {
548         Set<String> keys = new HashSet<>();
549         try (InputStream inStream = Files.newInputStream(file.toPath())) {
550             final Properties translations = new Properties();
551             translations.load(inStream);
552             keys = translations.stringPropertyNames();
553         }
554         // -@cs[IllegalCatch] It is better to catch all exceptions since it can throw
555         // a runtime exception.
556         catch (final Exception ex) {
557             logException(ex, file);
558         }
559         return keys;
560     }
561 
562     /**
563      * Helper method to log an exception.
564      *
565      * @param exception the exception that occurred
566      * @param file the file that could not be processed
567      */
568     private void logException(Exception exception, File file) {
569         final String[] args;
570         final String key;
571         if (exception instanceof NoSuchFileException) {
572             args = null;
573             key = "general.fileNotFound";
574         }
575         else {
576             args = new String[] {exception.getMessage()};
577             key = "general.exception";
578         }
579         final Violation message =
580             new Violation(
581                 0,
582                 Definitions.CHECKSTYLE_BUNDLE,
583                 key,
584                 args,
585                 getId(),
586                 getClass(), null);
587         final SortedSet<Violation> messages = new TreeSet<>();
588         messages.add(message);
589         getMessageDispatcher().fireErrors(file.getPath(), messages);
590         log.debug("Exception occurred.", exception);
591     }
592 
593     /** Class which represents a resource bundle. */
594     private static final class ResourceBundle {
595 
596         /** Bundle base name. */
597         private final String baseName;
598         /** Common extension of files which are included in the resource bundle. */
599         private final String extension;
600         /** Common path of files which are included in the resource bundle. */
601         private final String path;
602         /** Set of files which are included in the resource bundle. */
603         private final Set<File> files;
604 
605         /**
606          * Creates a ResourceBundle object with specific base name, common files extension.
607          *
608          * @param baseName bundle base name.
609          * @param path common path of files which are included in the resource bundle.
610          * @param extension common extension of files which are included in the resource bundle.
611          */
612         private ResourceBundle(String baseName, String path, String extension) {
613             this.baseName = baseName;
614             this.path = path;
615             this.extension = extension;
616             files = new HashSet<>();
617         }
618 
619         /**
620          * Returns the bundle base name.
621          *
622          * @return the bundle base name
623          */
624         public String getBaseName() {
625             return baseName;
626         }
627 
628         /**
629          * Returns the common path of files which are included in the resource bundle.
630          *
631          * @return the common path of files
632          */
633         public String getPath() {
634             return path;
635         }
636 
637         /**
638          * Returns the common extension of files which are included in the resource bundle.
639          *
640          * @return the common extension of files
641          */
642         public String getExtension() {
643             return extension;
644         }
645 
646         /**
647          * Returns the set of files which are included in the resource bundle.
648          *
649          * @return the set of files
650          */
651         public Set<File> getFiles() {
652             return Collections.unmodifiableSet(files);
653         }
654 
655         /**
656          * Adds a file into resource bundle.
657          *
658          * @param file file which should be added into resource bundle.
659          */
660         public void addFile(File file) {
661             files.add(file);
662         }
663 
664         /**
665          * Checks whether a resource bundle contains a file which name matches file name regexp.
666          *
667          * @param fileNameRegexp file name regexp.
668          * @return true if a resource bundle contains a file which name matches file name regexp.
669          */
670         public boolean containsFile(String fileNameRegexp) {
671             boolean containsFile = false;
672             for (File currentFile : files) {
673                 if (Pattern.matches(fileNameRegexp, currentFile.getName())) {
674                     containsFile = true;
675                     break;
676                 }
677             }
678             return containsFile;
679         }
680 
681     }
682 
683 }