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