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