001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2024 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.HashSet;
029import java.util.Locale;
030import java.util.Map;
031import java.util.Map.Entry;
032import java.util.Optional;
033import java.util.Properties;
034import java.util.Set;
035import java.util.SortedSet;
036import java.util.TreeMap;
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.LocalizedMessage;
049import com.puppycrawl.tools.checkstyle.api.AbstractFileSetCheck;
050import com.puppycrawl.tools.checkstyle.api.FileText;
051import com.puppycrawl.tools.checkstyle.api.MessageDispatcher;
052import com.puppycrawl.tools.checkstyle.api.Violation;
053import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
054
055/**
056 * <p>
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 * Note: If your project uses preprocessed translation files and the original files do not have the
097 * {@code properties} extension, you can specify additional file extensions
098 * via the {@code fileExtensions} property.
099 * </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
150public 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}