View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2025 the original author or authors.
4   //
5   // This library is free software; you can redistribute it and/or
6   // modify it under the terms of the GNU Lesser General Public
7   // License as published by the Free Software Foundation; either
8   // version 2.1 of the License, or (at your option) any later version.
9   //
10  // This library is distributed in the hope that it will be useful,
11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  // Lesser General Public License for more details.
14  //
15  // You should have received a copy of the GNU Lesser General Public
16  // License along with this library; if not, write to the Free Software
17  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18  ///////////////////////////////////////////////////////////////////////////////////////////////
19  
20  package com.puppycrawl.tools.checkstyle.checks;
21  
22  import java.io.File;
23  import java.io.InputStream;
24  import java.nio.file.Files;
25  import java.nio.file.NoSuchFileException;
26  import java.util.Arrays;
27  import java.util.Collections;
28  import java.util.HashSet;
29  import java.util.Locale;
30  import java.util.Map;
31  import java.util.Map.Entry;
32  import java.util.Optional;
33  import java.util.Properties;
34  import java.util.Set;
35  import java.util.SortedSet;
36  import java.util.TreeMap;
37  import java.util.TreeSet;
38  import java.util.concurrent.ConcurrentHashMap;
39  import java.util.regex.Matcher;
40  import java.util.regex.Pattern;
41  import java.util.stream.Collectors;
42  
43  import org.apache.commons.logging.Log;
44  import org.apache.commons.logging.LogFactory;
45  
46  import com.puppycrawl.tools.checkstyle.Definitions;
47  import com.puppycrawl.tools.checkstyle.GlobalStatefulCheck;
48  import com.puppycrawl.tools.checkstyle.LocalizedMessage;
49  import com.puppycrawl.tools.checkstyle.api.AbstractFileSetCheck;
50  import com.puppycrawl.tools.checkstyle.api.FileText;
51  import com.puppycrawl.tools.checkstyle.api.MessageDispatcher;
52  import com.puppycrawl.tools.checkstyle.api.Violation;
53  import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
54  
55  /**
56   * <div>
57   * Ensures the correct translation of code by checking property files for consistency
58   * regarding their keys. Two property files describing one and the same context
59   * are consistent if they contain the same keys. TranslationCheck also can check
60   * an existence of required translations which must exist in project, if
61   * {@code requiredTranslations} option is used.
62   * </div>
63   *
64   * <p>
65   * Language code for the property {@code requiredTranslations} is composed of
66   * the lowercase, two-letter codes as defined by
67   * <a href="https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes">ISO 639-1</a>.
68   * Default value is empty String Set which means that only the existence of default
69   * translation is checked. Note, if you specify language codes (or just one
70   * language code) of required translations the check will also check for existence
71   * of default translation files in project.
72   * </p>
73   *
74   * <p>
75   * Note: If your project uses preprocessed translation files and the original files do not have the
76   * {@code properties} extension, you can specify additional file extensions
77   * via the {@code fileExtensions} property.
78   * </p>
79   *
80   * <p>
81   * Attention: the check will perform the validation of ISO codes if the option
82   * is used. So, if you specify, for example, "mm" for language code,
83   * TranslationCheck will rise violation that the language code is incorrect.
84   * </p>
85   *
86   * <p>
87   * Attention: this Check could produce false-positives if it is used with
88   * <a href="https://checkstyle.org/config.html#Checker">Checker</a> that use cache
89   * (property "cacheFile") This is known design problem, will be addressed at
90   * <a href="https://github.com/checkstyle/checkstyle/issues/3539">issue</a>.
91   * </p>
92   * <ul>
93   * <li>
94   * Property {@code baseName} - Specify
95   * <a href="https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/ResourceBundle.html">
96   * Base name</a> of resource bundles which contain message resources.
97   * It helps the check to distinguish config and localization resources.
98   * Type is {@code java.util.regex.Pattern}.
99   * 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
133 public 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 = ConcurrentHashMap.newKeySet();
199 
200     /**
201      * Specify
202      * <a href="https://docs.oracle.com/en/java/javase/11/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
224      * <a href="https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/ResourceBundle.html">
225      * Base name</a> of resource bundles which contain message resources.
226      * It helps the check to distinguish config and localization resources.
227      *
228      * @param baseName base name regexp.
229      * @since 6.17
230      */
231     public void setBaseName(Pattern baseName) {
232         this.baseName = baseName;
233     }
234 
235     /**
236      * Setter to specify language codes of required translations which must exist in project.
237      *
238      * @param translationCodes language codes.
239      * @since 6.11
240      */
241     public void setRequiredTranslations(String... translationCodes) {
242         requiredTranslations = Arrays.stream(translationCodes)
243             .collect(Collectors.toUnmodifiableSet());
244         validateUserSpecifiedLanguageCodes(requiredTranslations);
245     }
246 
247     /**
248      * Validates the correctness of user specified language codes for the check.
249      *
250      * @param languageCodes user specified language codes for the check.
251      * @throws IllegalArgumentException when any item of languageCodes is not valid language code
252      */
253     private void validateUserSpecifiedLanguageCodes(Set<String> languageCodes) {
254         for (String code : languageCodes) {
255             if (!isValidLanguageCode(code)) {
256                 final LocalizedMessage msg = new LocalizedMessage(TRANSLATION_BUNDLE,
257                         getClass(), WRONG_LANGUAGE_CODE_KEY, code);
258                 throw new IllegalArgumentException(msg.getMessage());
259             }
260         }
261     }
262 
263     /**
264      * Checks whether user specified language code is correct (is contained in available locales).
265      *
266      * @param userSpecifiedLanguageCode user specified language code.
267      * @return true if user specified language code is correct.
268      */
269     private static boolean isValidLanguageCode(final String userSpecifiedLanguageCode) {
270         boolean valid = false;
271         final Locale[] locales = Locale.getAvailableLocales();
272         for (Locale locale : locales) {
273             if (userSpecifiedLanguageCode.equals(locale.toString())) {
274                 valid = true;
275                 break;
276             }
277         }
278         return valid;
279     }
280 
281     @Override
282     public void beginProcessing(String charset) {
283         filesToProcess.clear();
284     }
285 
286     @Override
287     protected void processFiltered(File file, FileText fileText) {
288         // We are just collecting files for processing at finishProcessing()
289         filesToProcess.add(file);
290     }
291 
292     @Override
293     public void finishProcessing() {
294         final Set<ResourceBundle> bundles = groupFilesIntoBundles(filesToProcess, baseName);
295         for (ResourceBundle currentBundle : bundles) {
296             checkExistenceOfDefaultTranslation(currentBundle);
297             checkExistenceOfRequiredTranslations(currentBundle);
298             checkTranslationKeys(currentBundle);
299         }
300     }
301 
302     /**
303      * Checks an existence of default translation file in the resource bundle.
304      *
305      * @param bundle resource bundle.
306      */
307     private void checkExistenceOfDefaultTranslation(ResourceBundle bundle) {
308         getMissingFileName(bundle, null)
309             .ifPresent(fileName -> logMissingTranslation(bundle.getPath(), fileName));
310     }
311 
312     /**
313      * Checks an existence of translation files in the resource bundle.
314      * The name of translation file begins with the base name of resource bundle which is followed
315      * by '_' and a language code (country and variant are optional), it ends with the extension
316      * suffix.
317      *
318      * @param bundle resource bundle.
319      */
320     private void checkExistenceOfRequiredTranslations(ResourceBundle bundle) {
321         for (String languageCode : requiredTranslations) {
322             getMissingFileName(bundle, languageCode)
323                 .ifPresent(fileName -> logMissingTranslation(bundle.getPath(), fileName));
324         }
325     }
326 
327     /**
328      * Returns the name of translation file which is absent in resource bundle or Guava's Optional,
329      * if there is not missing translation.
330      *
331      * @param bundle resource bundle.
332      * @param languageCode language code.
333      * @return the name of translation file which is absent in resource bundle or Guava's Optional,
334      *         if there is not missing translation.
335      */
336     private static Optional<String> getMissingFileName(ResourceBundle bundle, String languageCode) {
337         final String fileNameRegexp;
338         final boolean searchForDefaultTranslation;
339         final String extension = bundle.getExtension();
340         final String baseName = bundle.getBaseName();
341         if (languageCode == null) {
342             searchForDefaultTranslation = true;
343             fileNameRegexp = String.format(Locale.ROOT, REGEXP_FORMAT_TO_CHECK_DEFAULT_TRANSLATIONS,
344                     baseName, extension);
345         }
346         else {
347             searchForDefaultTranslation = false;
348             fileNameRegexp = String.format(Locale.ROOT,
349                 REGEXP_FORMAT_TO_CHECK_REQUIRED_TRANSLATIONS, baseName, languageCode, extension);
350         }
351         Optional<String> missingFileName = Optional.empty();
352         if (!bundle.containsFile(fileNameRegexp)) {
353             if (searchForDefaultTranslation) {
354                 missingFileName = Optional.of(String.format(Locale.ROOT,
355                         DEFAULT_TRANSLATION_FILE_NAME_FORMATTER, baseName, extension));
356             }
357             else {
358                 missingFileName = Optional.of(String.format(Locale.ROOT,
359                         FILE_NAME_WITH_LANGUAGE_CODE_FORMATTER, baseName, languageCode, extension));
360             }
361         }
362         return missingFileName;
363     }
364 
365     /**
366      * Logs that translation file is missing.
367      *
368      * @param filePath file path.
369      * @param fileName file name.
370      */
371     private void logMissingTranslation(String filePath, String fileName) {
372         final MessageDispatcher dispatcher = getMessageDispatcher();
373         dispatcher.fireFileStarted(filePath);
374         log(1, MSG_KEY_MISSING_TRANSLATION_FILE, fileName);
375         fireErrors(filePath);
376         dispatcher.fireFileFinished(filePath);
377     }
378 
379     /**
380      * Groups a set of files into bundles.
381      * Only files, which names match base name regexp pattern will be grouped.
382      *
383      * @param files set of files.
384      * @param baseNameRegexp base name regexp pattern.
385      * @return set of ResourceBundles.
386      */
387     private static Set<ResourceBundle> groupFilesIntoBundles(Set<File> files,
388                                                              Pattern baseNameRegexp) {
389         final Set<ResourceBundle> resourceBundles = new HashSet<>();
390         for (File currentFile : files) {
391             final String fileName = currentFile.getName();
392             final String baseName = extractBaseName(fileName);
393             final Matcher baseNameMatcher = baseNameRegexp.matcher(baseName);
394             if (baseNameMatcher.matches()) {
395                 final String extension = CommonUtil.getFileExtension(fileName);
396                 final String path = getPath(currentFile.getAbsolutePath());
397                 final ResourceBundle newBundle = new ResourceBundle(baseName, path, extension);
398                 final Optional<ResourceBundle> bundle = findBundle(resourceBundles, newBundle);
399                 if (bundle.isPresent()) {
400                     bundle.orElseThrow().addFile(currentFile);
401                 }
402                 else {
403                     newBundle.addFile(currentFile);
404                     resourceBundles.add(newBundle);
405                 }
406             }
407         }
408         return resourceBundles;
409     }
410 
411     /**
412      * Searches for specific resource bundle in a set of resource bundles.
413      *
414      * @param bundles set of resource bundles.
415      * @param targetBundle target bundle to search for.
416      * @return Guava's Optional of resource bundle (present if target bundle is found).
417      */
418     private static Optional<ResourceBundle> findBundle(Set<ResourceBundle> bundles,
419                                                        ResourceBundle targetBundle) {
420         Optional<ResourceBundle> result = Optional.empty();
421         for (ResourceBundle currentBundle : bundles) {
422             if (targetBundle.getBaseName().equals(currentBundle.getBaseName())
423                     && targetBundle.getExtension().equals(currentBundle.getExtension())
424                     && targetBundle.getPath().equals(currentBundle.getPath())) {
425                 result = Optional.of(currentBundle);
426                 break;
427             }
428         }
429         return result;
430     }
431 
432     /**
433      * Extracts the base name (the unique prefix) of resource bundle from translation file name.
434      * For example "messages" is the base name of "messages.properties",
435      * "messages_de_AT.properties", "messages_en.properties", etc.
436      *
437      * @param fileName the fully qualified name of the translation file.
438      * @return the extracted base name.
439      */
440     private static String extractBaseName(String fileName) {
441         final String regexp;
442         final Matcher languageCountryVariantMatcher =
443             LANGUAGE_COUNTRY_VARIANT_PATTERN.matcher(fileName);
444         final Matcher languageCountryMatcher = LANGUAGE_COUNTRY_PATTERN.matcher(fileName);
445         final Matcher languageMatcher = LANGUAGE_PATTERN.matcher(fileName);
446         if (languageCountryVariantMatcher.matches()) {
447             regexp = LANGUAGE_COUNTRY_VARIANT_PATTERN.pattern();
448         }
449         else if (languageCountryMatcher.matches()) {
450             regexp = LANGUAGE_COUNTRY_PATTERN.pattern();
451         }
452         else if (languageMatcher.matches()) {
453             regexp = LANGUAGE_PATTERN.pattern();
454         }
455         else {
456             regexp = DEFAULT_TRANSLATION_REGEXP;
457         }
458         // We use substring(...) instead of replace(...), so that the regular expression does
459         // not have to be compiled each time it is used inside 'replace' method.
460         final String removePattern = regexp.substring("^.+".length());
461         return fileName.replaceAll(removePattern, "");
462     }
463 
464     /**
465      * Extracts path from a file name which contains the path.
466      * For example, if the file name is /xyz/messages.properties,
467      * then the method will return /xyz/.
468      *
469      * @param fileNameWithPath file name which contains the path.
470      * @return file path.
471      */
472     private static String getPath(String fileNameWithPath) {
473         return fileNameWithPath
474             .substring(0, fileNameWithPath.lastIndexOf(File.separator));
475     }
476 
477     /**
478      * Checks resource files in bundle for consistency regarding their keys.
479      * All files in bundle must have the same key set. If this is not the case
480      * an audit event message is posted giving information which key misses in which file.
481      *
482      * @param bundle resource bundle.
483      */
484     private void checkTranslationKeys(ResourceBundle bundle) {
485         final Set<File> filesInBundle = bundle.getFiles();
486         // build a map from files to the keys they contain
487         final Set<String> allTranslationKeys = new HashSet<>();
488         final Map<File, Set<String>> filesAssociatedWithKeys = new TreeMap<>();
489         for (File currentFile : filesInBundle) {
490             final Set<String> keysInCurrentFile = getTranslationKeys(currentFile);
491             allTranslationKeys.addAll(keysInCurrentFile);
492             filesAssociatedWithKeys.put(currentFile, keysInCurrentFile);
493         }
494         checkFilesForConsistencyRegardingTheirKeys(filesAssociatedWithKeys, allTranslationKeys);
495     }
496 
497     /**
498      * Compares th the specified key set with the key sets of the given translation files (arranged
499      * in a map). All missing keys are reported.
500      *
501      * @param fileKeys a Map from translation files to their key sets.
502      * @param keysThatMustExist the set of keys to compare with.
503      */
504     private void checkFilesForConsistencyRegardingTheirKeys(Map<File, Set<String>> fileKeys,
505                                                             Set<String> keysThatMustExist) {
506         for (Entry<File, Set<String>> fileKey : fileKeys.entrySet()) {
507             final Set<String> currentFileKeys = fileKey.getValue();
508             final Set<String> missingKeys = keysThatMustExist.stream()
509                 .filter(key -> !currentFileKeys.contains(key))
510                 .collect(Collectors.toUnmodifiableSet());
511             if (!missingKeys.isEmpty()) {
512                 final MessageDispatcher dispatcher = getMessageDispatcher();
513                 final String path = fileKey.getKey().getAbsolutePath();
514                 dispatcher.fireFileStarted(path);
515                 for (Object key : missingKeys) {
516                     log(1, MSG_KEY, key);
517                 }
518                 fireErrors(path);
519                 dispatcher.fireFileFinished(path);
520             }
521         }
522     }
523 
524     /**
525      * Loads the keys from the specified translation file into a set.
526      *
527      * @param file translation file.
528      * @return a Set object which holds the loaded keys.
529      */
530     private Set<String> getTranslationKeys(File file) {
531         Set<String> keys = new HashSet<>();
532         try (InputStream inStream = Files.newInputStream(file.toPath())) {
533             final Properties translations = new Properties();
534             translations.load(inStream);
535             keys = translations.stringPropertyNames();
536         }
537         // -@cs[IllegalCatch] It is better to catch all exceptions since it can throw
538         // a runtime exception.
539         catch (final Exception ex) {
540             logException(ex, file);
541         }
542         return keys;
543     }
544 
545     /**
546      * Helper method to log an exception.
547      *
548      * @param exception the exception that occurred
549      * @param file the file that could not be processed
550      */
551     private void logException(Exception exception, File file) {
552         final String[] args;
553         final String key;
554         if (exception instanceof NoSuchFileException) {
555             args = null;
556             key = "general.fileNotFound";
557         }
558         else {
559             args = new String[] {exception.getMessage()};
560             key = "general.exception";
561         }
562         final Violation message =
563             new Violation(
564                 0,
565                 Definitions.CHECKSTYLE_BUNDLE,
566                 key,
567                 args,
568                 getId(),
569                 getClass(), null);
570         final SortedSet<Violation> messages = new TreeSet<>();
571         messages.add(message);
572         getMessageDispatcher().fireErrors(file.getPath(), messages);
573         log.debug("Exception occurred.", exception);
574     }
575 
576     /** Class which represents a resource bundle. */
577     private static final class ResourceBundle {
578 
579         /** Bundle base name. */
580         private final String baseName;
581         /** Common extension of files which are included in the resource bundle. */
582         private final String extension;
583         /** Common path of files which are included in the resource bundle. */
584         private final String path;
585         /** Set of files which are included in the resource bundle. */
586         private final Set<File> files;
587 
588         /**
589          * Creates a ResourceBundle object with specific base name, common files extension.
590          *
591          * @param baseName bundle base name.
592          * @param path common path of files which are included in the resource bundle.
593          * @param extension common extension of files which are included in the resource bundle.
594          */
595         private ResourceBundle(String baseName, String path, String extension) {
596             this.baseName = baseName;
597             this.path = path;
598             this.extension = extension;
599             files = new HashSet<>();
600         }
601 
602         /**
603          * Returns the bundle base name.
604          *
605          * @return the bundle base name
606          */
607         public String getBaseName() {
608             return baseName;
609         }
610 
611         /**
612          * Returns the common path of files which are included in the resource bundle.
613          *
614          * @return the common path of files
615          */
616         public String getPath() {
617             return path;
618         }
619 
620         /**
621          * Returns the common extension of files which are included in the resource bundle.
622          *
623          * @return the common extension of files
624          */
625         public String getExtension() {
626             return extension;
627         }
628 
629         /**
630          * Returns the set of files which are included in the resource bundle.
631          *
632          * @return the set of files
633          */
634         public Set<File> getFiles() {
635             return Collections.unmodifiableSet(files);
636         }
637 
638         /**
639          * Adds a file into resource bundle.
640          *
641          * @param file file which should be added into resource bundle.
642          */
643         public void addFile(File file) {
644             files.add(file);
645         }
646 
647         /**
648          * Checks whether a resource bundle contains a file which name matches file name regexp.
649          *
650          * @param fileNameRegexp file name regexp.
651          * @return true if a resource bundle contains a file which name matches file name regexp.
652          */
653         public boolean containsFile(String fileNameRegexp) {
654             boolean containsFile = false;
655             for (File currentFile : files) {
656                 if (Pattern.matches(fileNameRegexp, currentFile.getName())) {
657                     containsFile = true;
658                     break;
659                 }
660             }
661             return containsFile;
662         }
663 
664     }
665 
666 }