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