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