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