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.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
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 @GlobalStatefulCheck
132 public class TranslationCheck extends AbstractFileSetCheck {
133
134
135
136
137
138 public static final String MSG_KEY = "translation.missingKey";
139
140
141
142
143
144 public static final String MSG_KEY_MISSING_TRANSLATION_FILE =
145 "translation.missingTranslationFile";
146
147
148 private static final String TRANSLATION_BUNDLE =
149 "com.puppycrawl.tools.checkstyle.checks.messages";
150
151
152
153
154
155 private static final String WRONG_LANGUAGE_CODE_KEY = "translation.wrongLanguageCode";
156
157
158
159
160
161 private static final String DEFAULT_TRANSLATION_REGEXP = "^.+\\..+$";
162
163
164
165
166
167 private static final Pattern LANGUAGE_COUNTRY_VARIANT_PATTERN =
168 CommonUtil.createPattern("^.+\\_[a-z]{2}\\_[A-Z]{2}\\_[A-Za-z]+\\..+$");
169
170
171
172
173 private static final Pattern LANGUAGE_COUNTRY_PATTERN =
174 CommonUtil.createPattern("^.+\\_[a-z]{2}\\_[A-Z]{2}\\..+$");
175
176
177
178
179 private static final Pattern LANGUAGE_PATTERN =
180 CommonUtil.createPattern("^.+\\_[a-z]{2}\\..+$");
181
182
183 private static final String DEFAULT_TRANSLATION_FILE_NAME_FORMATTER = "%s.%s";
184
185 private static final String FILE_NAME_WITH_LANGUAGE_CODE_FORMATTER = "%s_%s.%s";
186
187
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
191 private static final String REGEXP_FORMAT_TO_CHECK_DEFAULT_TRANSLATIONS = "^%s\\.%s$";
192
193
194 private final Log log;
195
196
197 private final Set<File> filesToProcess = new HashSet<>();
198
199
200
201
202
203
204
205 private Pattern baseName;
206
207
208
209
210 private Set<String> requiredTranslations = new HashSet<>();
211
212
213
214
215 public TranslationCheck() {
216 setFileExtensions("properties");
217 baseName = CommonUtil.createPattern("^messages.*$");
218 log = LogFactory.getLog(TranslationCheck.class);
219 }
220
221
222
223
224
225
226
227
228
229
230 public void setBaseName(Pattern baseName) {
231 this.baseName = baseName;
232 }
233
234
235
236
237
238
239
240 public void setRequiredTranslations(String... translationCodes) {
241 requiredTranslations = Arrays.stream(translationCodes)
242 .collect(Collectors.toUnmodifiableSet());
243 validateUserSpecifiedLanguageCodes(requiredTranslations);
244 }
245
246
247
248
249
250
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
264
265
266
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
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
303
304
305
306 private void checkExistenceOfDefaultTranslation(ResourceBundle bundle) {
307 getMissingFileName(bundle, null)
308 .ifPresent(fileName -> logMissingTranslation(bundle.getPath(), fileName));
309 }
310
311
312
313
314
315
316
317
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
328
329
330
331
332
333
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
366
367
368
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
380
381
382
383
384
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
412
413
414
415
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
433
434
435
436
437
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
458
459 final String removePattern = regexp.substring("^.+".length());
460 return fileName.replaceAll(removePattern, "");
461 }
462
463
464
465
466
467
468
469
470
471 private static String getPath(String fileNameWithPath) {
472 return fileNameWithPath
473 .substring(0, fileNameWithPath.lastIndexOf(File.separator));
474 }
475
476
477
478
479
480
481
482
483 private void checkTranslationKeys(ResourceBundle bundle) {
484 final Set<File> filesInBundle = bundle.getFiles();
485
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
498
499
500
501
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
525
526
527
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
537
538 catch (final Exception ex) {
539 logException(ex, file);
540 }
541 return keys;
542 }
543
544
545
546
547
548
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
576 private static final class ResourceBundle {
577
578
579 private final String baseName;
580
581 private final String extension;
582
583 private final String path;
584
585 private final Set<File> files;
586
587
588
589
590
591
592
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
603
604
605
606 public String getBaseName() {
607 return baseName;
608 }
609
610
611
612
613
614
615 public String getPath() {
616 return path;
617 }
618
619
620
621
622
623
624 public String getExtension() {
625 return extension;
626 }
627
628
629
630
631
632
633 public Set<File> getFiles() {
634 return Collections.unmodifiableSet(files);
635 }
636
637
638
639
640
641
642 public void addFile(File file) {
643 files.add(file);
644 }
645
646
647
648
649
650
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 }