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