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