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