1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149 @GlobalStatefulCheck
150 public class TranslationCheck extends AbstractFileSetCheck {
151
152
153
154
155
156 public static final String MSG_KEY = "translation.missingKey";
157
158
159
160
161
162 public static final String MSG_KEY_MISSING_TRANSLATION_FILE =
163 "translation.missingTranslationFile";
164
165
166 private static final String TRANSLATION_BUNDLE =
167 "com.puppycrawl.tools.checkstyle.checks.messages";
168
169
170
171
172
173 private static final String WRONG_LANGUAGE_CODE_KEY = "translation.wrongLanguageCode";
174
175
176
177
178
179 private static final String DEFAULT_TRANSLATION_REGEXP = "^.+\\..+$";
180
181
182
183
184
185 private static final Pattern LANGUAGE_COUNTRY_VARIANT_PATTERN =
186 CommonUtil.createPattern("^.+\\_[a-z]{2}\\_[A-Z]{2}\\_[A-Za-z]+\\..+$");
187
188
189
190
191 private static final Pattern LANGUAGE_COUNTRY_PATTERN =
192 CommonUtil.createPattern("^.+\\_[a-z]{2}\\_[A-Z]{2}\\..+$");
193
194
195
196
197 private static final Pattern LANGUAGE_PATTERN =
198 CommonUtil.createPattern("^.+\\_[a-z]{2}\\..+$");
199
200
201 private static final String DEFAULT_TRANSLATION_FILE_NAME_FORMATTER = "%s.%s";
202
203 private static final String FILE_NAME_WITH_LANGUAGE_CODE_FORMATTER = "%s_%s.%s";
204
205
206 private static final String REGEXP_FORMAT_TO_CHECK_REQUIRED_TRANSLATIONS =
207 "^%1$s\\_%2$s(\\_[A-Z]{2})?\\.%3$s$|^%1$s\\_%2$s\\_[A-Z]{2}\\_[A-Za-z]+\\.%3$s$";
208
209 private static final String REGEXP_FORMAT_TO_CHECK_DEFAULT_TRANSLATIONS = "^%s\\.%s$";
210
211
212 private final Log log;
213
214
215 private final Set<File> filesToProcess = ConcurrentHashMap.newKeySet();
216
217
218
219
220
221
222
223 private Pattern baseName;
224
225
226
227
228 private Set<String> requiredTranslations = new HashSet<>();
229
230
231
232
233 public TranslationCheck() {
234 setFileExtensions("properties");
235 baseName = CommonUtil.createPattern("^messages.*$");
236 log = LogFactory.getLog(TranslationCheck.class);
237 }
238
239
240
241
242
243
244
245
246
247
248 public void setBaseName(Pattern baseName) {
249 this.baseName = baseName;
250 }
251
252
253
254
255
256
257
258 public void setRequiredTranslations(String... translationCodes) {
259 requiredTranslations = Arrays.stream(translationCodes)
260 .collect(Collectors.toUnmodifiableSet());
261 validateUserSpecifiedLanguageCodes(requiredTranslations);
262 }
263
264
265
266
267
268
269
270 private void validateUserSpecifiedLanguageCodes(Set<String> languageCodes) {
271 for (String code : languageCodes) {
272 if (!isValidLanguageCode(code)) {
273 final LocalizedMessage msg = new LocalizedMessage(TRANSLATION_BUNDLE,
274 getClass(), WRONG_LANGUAGE_CODE_KEY, code);
275 throw new IllegalArgumentException(msg.getMessage());
276 }
277 }
278 }
279
280
281
282
283
284
285
286 private static boolean isValidLanguageCode(final String userSpecifiedLanguageCode) {
287 boolean valid = false;
288 final Locale[] locales = Locale.getAvailableLocales();
289 for (Locale locale : locales) {
290 if (userSpecifiedLanguageCode.equals(locale.toString())) {
291 valid = true;
292 break;
293 }
294 }
295 return valid;
296 }
297
298 @Override
299 public void beginProcessing(String charset) {
300 filesToProcess.clear();
301 }
302
303 @Override
304 protected void processFiltered(File file, FileText fileText) {
305
306 filesToProcess.add(file);
307 }
308
309 @Override
310 public void finishProcessing() {
311 final Set<ResourceBundle> bundles = groupFilesIntoBundles(filesToProcess, baseName);
312 for (ResourceBundle currentBundle : bundles) {
313 checkExistenceOfDefaultTranslation(currentBundle);
314 checkExistenceOfRequiredTranslations(currentBundle);
315 checkTranslationKeys(currentBundle);
316 }
317 }
318
319
320
321
322
323
324 private void checkExistenceOfDefaultTranslation(ResourceBundle bundle) {
325 getMissingFileName(bundle, null)
326 .ifPresent(fileName -> logMissingTranslation(bundle.getPath(), fileName));
327 }
328
329
330
331
332
333
334
335
336
337 private void checkExistenceOfRequiredTranslations(ResourceBundle bundle) {
338 for (String languageCode : requiredTranslations) {
339 getMissingFileName(bundle, languageCode)
340 .ifPresent(fileName -> logMissingTranslation(bundle.getPath(), fileName));
341 }
342 }
343
344
345
346
347
348
349
350
351
352
353 private static Optional<String> getMissingFileName(ResourceBundle bundle, String languageCode) {
354 final String fileNameRegexp;
355 final boolean searchForDefaultTranslation;
356 final String extension = bundle.getExtension();
357 final String baseName = bundle.getBaseName();
358 if (languageCode == null) {
359 searchForDefaultTranslation = true;
360 fileNameRegexp = String.format(Locale.ROOT, REGEXP_FORMAT_TO_CHECK_DEFAULT_TRANSLATIONS,
361 baseName, extension);
362 }
363 else {
364 searchForDefaultTranslation = false;
365 fileNameRegexp = String.format(Locale.ROOT,
366 REGEXP_FORMAT_TO_CHECK_REQUIRED_TRANSLATIONS, baseName, languageCode, extension);
367 }
368 Optional<String> missingFileName = Optional.empty();
369 if (!bundle.containsFile(fileNameRegexp)) {
370 if (searchForDefaultTranslation) {
371 missingFileName = Optional.of(String.format(Locale.ROOT,
372 DEFAULT_TRANSLATION_FILE_NAME_FORMATTER, baseName, extension));
373 }
374 else {
375 missingFileName = Optional.of(String.format(Locale.ROOT,
376 FILE_NAME_WITH_LANGUAGE_CODE_FORMATTER, baseName, languageCode, extension));
377 }
378 }
379 return missingFileName;
380 }
381
382
383
384
385
386
387
388 private void logMissingTranslation(String filePath, String fileName) {
389 final MessageDispatcher dispatcher = getMessageDispatcher();
390 dispatcher.fireFileStarted(filePath);
391 log(1, MSG_KEY_MISSING_TRANSLATION_FILE, fileName);
392 fireErrors(filePath);
393 dispatcher.fireFileFinished(filePath);
394 }
395
396
397
398
399
400
401
402
403
404 private static Set<ResourceBundle> groupFilesIntoBundles(Set<File> files,
405 Pattern baseNameRegexp) {
406 final Set<ResourceBundle> resourceBundles = new HashSet<>();
407 for (File currentFile : files) {
408 final String fileName = currentFile.getName();
409 final String baseName = extractBaseName(fileName);
410 final Matcher baseNameMatcher = baseNameRegexp.matcher(baseName);
411 if (baseNameMatcher.matches()) {
412 final String extension = CommonUtil.getFileExtension(fileName);
413 final String path = getPath(currentFile.getAbsolutePath());
414 final ResourceBundle newBundle = new ResourceBundle(baseName, path, extension);
415 final Optional<ResourceBundle> bundle = findBundle(resourceBundles, newBundle);
416 if (bundle.isPresent()) {
417 bundle.orElseThrow().addFile(currentFile);
418 }
419 else {
420 newBundle.addFile(currentFile);
421 resourceBundles.add(newBundle);
422 }
423 }
424 }
425 return resourceBundles;
426 }
427
428
429
430
431
432
433
434
435 private static Optional<ResourceBundle> findBundle(Set<ResourceBundle> bundles,
436 ResourceBundle targetBundle) {
437 Optional<ResourceBundle> result = Optional.empty();
438 for (ResourceBundle currentBundle : bundles) {
439 if (targetBundle.getBaseName().equals(currentBundle.getBaseName())
440 && targetBundle.getExtension().equals(currentBundle.getExtension())
441 && targetBundle.getPath().equals(currentBundle.getPath())) {
442 result = Optional.of(currentBundle);
443 break;
444 }
445 }
446 return result;
447 }
448
449
450
451
452
453
454
455
456
457 private static String extractBaseName(String fileName) {
458 final String regexp;
459 final Matcher languageCountryVariantMatcher =
460 LANGUAGE_COUNTRY_VARIANT_PATTERN.matcher(fileName);
461 final Matcher languageCountryMatcher = LANGUAGE_COUNTRY_PATTERN.matcher(fileName);
462 final Matcher languageMatcher = LANGUAGE_PATTERN.matcher(fileName);
463 if (languageCountryVariantMatcher.matches()) {
464 regexp = LANGUAGE_COUNTRY_VARIANT_PATTERN.pattern();
465 }
466 else if (languageCountryMatcher.matches()) {
467 regexp = LANGUAGE_COUNTRY_PATTERN.pattern();
468 }
469 else if (languageMatcher.matches()) {
470 regexp = LANGUAGE_PATTERN.pattern();
471 }
472 else {
473 regexp = DEFAULT_TRANSLATION_REGEXP;
474 }
475
476
477 final String removePattern = regexp.substring("^.+".length());
478 return fileName.replaceAll(removePattern, "");
479 }
480
481
482
483
484
485
486
487
488
489 private static String getPath(String fileNameWithPath) {
490 return fileNameWithPath
491 .substring(0, fileNameWithPath.lastIndexOf(File.separator));
492 }
493
494
495
496
497
498
499
500
501 private void checkTranslationKeys(ResourceBundle bundle) {
502 final Set<File> filesInBundle = bundle.getFiles();
503
504 final Set<String> allTranslationKeys = new HashSet<>();
505 final Map<File, Set<String>> filesAssociatedWithKeys = new TreeMap<>();
506 for (File currentFile : filesInBundle) {
507 final Set<String> keysInCurrentFile = getTranslationKeys(currentFile);
508 allTranslationKeys.addAll(keysInCurrentFile);
509 filesAssociatedWithKeys.put(currentFile, keysInCurrentFile);
510 }
511 checkFilesForConsistencyRegardingTheirKeys(filesAssociatedWithKeys, allTranslationKeys);
512 }
513
514
515
516
517
518
519
520
521 private void checkFilesForConsistencyRegardingTheirKeys(Map<File, Set<String>> fileKeys,
522 Set<String> keysThatMustExist) {
523 for (Entry<File, Set<String>> fileKey : fileKeys.entrySet()) {
524 final Set<String> currentFileKeys = fileKey.getValue();
525 final Set<String> missingKeys = keysThatMustExist.stream()
526 .filter(key -> !currentFileKeys.contains(key))
527 .collect(Collectors.toUnmodifiableSet());
528 if (!missingKeys.isEmpty()) {
529 final MessageDispatcher dispatcher = getMessageDispatcher();
530 final String path = fileKey.getKey().getAbsolutePath();
531 dispatcher.fireFileStarted(path);
532 for (Object key : missingKeys) {
533 log(1, MSG_KEY, key);
534 }
535 fireErrors(path);
536 dispatcher.fireFileFinished(path);
537 }
538 }
539 }
540
541
542
543
544
545
546
547 private Set<String> getTranslationKeys(File file) {
548 Set<String> keys = new HashSet<>();
549 try (InputStream inStream = Files.newInputStream(file.toPath())) {
550 final Properties translations = new Properties();
551 translations.load(inStream);
552 keys = translations.stringPropertyNames();
553 }
554
555
556 catch (final Exception ex) {
557 logException(ex, file);
558 }
559 return keys;
560 }
561
562
563
564
565
566
567
568 private void logException(Exception exception, File file) {
569 final String[] args;
570 final String key;
571 if (exception instanceof NoSuchFileException) {
572 args = null;
573 key = "general.fileNotFound";
574 }
575 else {
576 args = new String[] {exception.getMessage()};
577 key = "general.exception";
578 }
579 final Violation message =
580 new Violation(
581 0,
582 Definitions.CHECKSTYLE_BUNDLE,
583 key,
584 args,
585 getId(),
586 getClass(), null);
587 final SortedSet<Violation> messages = new TreeSet<>();
588 messages.add(message);
589 getMessageDispatcher().fireErrors(file.getPath(), messages);
590 log.debug("Exception occurred.", exception);
591 }
592
593
594 private static final class ResourceBundle {
595
596
597 private final String baseName;
598
599 private final String extension;
600
601 private final String path;
602
603 private final Set<File> files;
604
605
606
607
608
609
610
611
612 private ResourceBundle(String baseName, String path, String extension) {
613 this.baseName = baseName;
614 this.path = path;
615 this.extension = extension;
616 files = new HashSet<>();
617 }
618
619
620
621
622
623
624 public String getBaseName() {
625 return baseName;
626 }
627
628
629
630
631
632
633 public String getPath() {
634 return path;
635 }
636
637
638
639
640
641
642 public String getExtension() {
643 return extension;
644 }
645
646
647
648
649
650
651 public Set<File> getFiles() {
652 return Collections.unmodifiableSet(files);
653 }
654
655
656
657
658
659
660 public void addFile(File file) {
661 files.add(file);
662 }
663
664
665
666
667
668
669
670 public boolean containsFile(String fileNameRegexp) {
671 boolean containsFile = false;
672 for (File currentFile : files) {
673 if (Pattern.matches(fileNameRegexp, currentFile.getName())) {
674 containsFile = true;
675 break;
676 }
677 }
678 return containsFile;
679 }
680
681 }
682
683 }