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