001//////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code for adherence to a set of rules. 003// Copyright (C) 2001-2015 the original author or authors. 004// 005// This library is free software; you can redistribute it and/or 006// modify it under the terms of the GNU Lesser General Public 007// License as published by the Free Software Foundation; either 008// version 2.1 of the License, or (at your option) any later version. 009// 010// This library is distributed in the hope that it will be useful, 011// but WITHOUT ANY WARRANTY; without even the implied warranty of 012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 013// Lesser General Public License for more details. 014// 015// You should have received a copy of the GNU Lesser General Public 016// License along with this library; if not, write to the Free Software 017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 018//////////////////////////////////////////////////////////////////////////////// 019 020package com.puppycrawl.tools.checkstyle.checks; 021 022import java.io.File; 023import java.io.FileInputStream; 024import java.io.FileNotFoundException; 025import java.io.IOException; 026import java.io.InputStream; 027import java.util.Enumeration; 028import java.util.List; 029import java.util.Properties; 030import java.util.Set; 031import java.util.SortedSet; 032import java.util.regex.Matcher; 033import java.util.regex.Pattern; 034 035import org.apache.commons.logging.Log; 036import org.apache.commons.logging.LogFactory; 037 038import com.google.common.base.Splitter; 039import com.google.common.collect.HashMultimap; 040import com.google.common.collect.ImmutableSortedSet; 041import com.google.common.collect.Lists; 042import com.google.common.collect.SetMultimap; 043import com.google.common.collect.Sets; 044import com.google.common.io.Closeables; 045import com.puppycrawl.tools.checkstyle.Definitions; 046import com.puppycrawl.tools.checkstyle.api.AbstractFileSetCheck; 047import com.puppycrawl.tools.checkstyle.api.LocalizedMessage; 048import com.puppycrawl.tools.checkstyle.api.MessageDispatcher; 049 050/** 051 * <p> 052 * The TranslationCheck class helps to ensure the correct translation of code by 053 * checking property files for consistency regarding their keys. 054 * Two property files describing one and the same context are consistent if they 055 * contain the same keys. 056 * </p> 057 * <p> 058 * An example of how to configure the check is: 059 * </p> 060 * <pre> 061 * <module name="Translation"/> 062 * </pre> 063 * Check has the following properties: 064 * 065 * <p><b>basenameSeparator</b> which allows setting separator in file names, 066 * default value is '_'. 067 * <p> 068 * E.g.: 069 * </p> 070 * <p> 071 * messages_test.properties //separator is '_' 072 * </p> 073 * <p> 074 * app-dev.properties //separator is '-' 075 * </p> 076 * 077 * <p><b>requiredTranslations</b> which allows to specify language codes of 078 * required translations which must exist in project. The check looks only for 079 * messages bundles which names contain the word 'messages'. 080 * Language code is composed of the lowercase, two-letter codes as defined by 081 * <a href="http://www.fatbellyman.com/webstuff/language_codes_639-1/">ISO 639-1</a>. 082 * Default value is <b>empty String Set</b> which means that only the existence of 083 * default translation is checked. 084 * Note, if you specify language codes (or just one language code) of required translations 085 * the check will also check for existence of default translation files in project. 086 * <br> 087 * @author Alexandra Bunge 088 * @author lkuehne 089 * @author Andrei Selkin 090 */ 091public class TranslationCheck 092 extends AbstractFileSetCheck { 093 094 /** 095 * A key is pointing to the warning message text for missing key 096 * in "messages.properties" file. 097 */ 098 public static final String MSG_KEY = "translation.missingKey"; 099 100 /** 101 * A key is pointing to the warning message text for missing translation file 102 * in "messages.properties" file. 103 */ 104 public static final String MSG_KEY_MISSING_TRANSLATION_FILE = 105 "translation.missingTranslationFile"; 106 107 /** Logger for TranslationCheck. */ 108 private static final Log LOG = LogFactory.getLog(TranslationCheck.class); 109 110 /** The property files to process. */ 111 private final List<File> propertyFiles = Lists.newArrayList(); 112 113 /** The separator string used to separate translation files. */ 114 private String basenameSeparator; 115 116 /** 117 * Language codes of required translations for the check (de, pt, ja, etc). 118 */ 119 private SortedSet<String> requiredTranslations = ImmutableSortedSet.of(); 120 121 /** 122 * Creates a new {@code TranslationCheck} instance. 123 */ 124 public TranslationCheck() { 125 setFileExtensions("properties"); 126 basenameSeparator = "_"; 127 } 128 129 /** 130 * Sets language codes of required translations for the check. 131 * @param translationCodes a comma separated list of language codes. 132 */ 133 public void setRequiredTranslations(String translationCodes) { 134 requiredTranslations = Sets.newTreeSet(Splitter.on(',') 135 .trimResults().omitEmptyStrings().split(translationCodes)); 136 } 137 138 @Override 139 public void beginProcessing(String charset) { 140 super.beginProcessing(charset); 141 propertyFiles.clear(); 142 } 143 144 @Override 145 protected void processFiltered(File file, List<String> lines) { 146 propertyFiles.add(file); 147 } 148 149 @Override 150 public void finishProcessing() { 151 super.finishProcessing(); 152 final SetMultimap<String, File> propFilesMap = 153 arrangePropertyFiles(propertyFiles, basenameSeparator); 154 checkExistenceOfTranslations(propFilesMap); 155 checkPropertyFileSets(propFilesMap); 156 } 157 158 /** 159 * Checks existence of translation files (arranged in a map) 160 * for each resource bundle in project. 161 * @param translations the translation files bundles organized as Map. 162 */ 163 private void checkExistenceOfTranslations(SetMultimap<String, File> translations) { 164 for (String fullyQualifiedBundleName : translations.keySet()) { 165 final String bundleBaseName = extractName(fullyQualifiedBundleName); 166 if (bundleBaseName.contains("messages")) { 167 final Set<File> filesInBundle = translations.get(fullyQualifiedBundleName); 168 checkExistenceOfDefaultTranslation(filesInBundle); 169 checkExistenceOfRequiredTranslations(filesInBundle); 170 } 171 } 172 } 173 174 /** 175 * Checks an existence of default translation file in 176 * a set of files in resource bundle. The name of this file 177 * begins with the full name of the resource bundle and ends 178 * with the extension suffix. 179 * @param filesInResourceBundle a set of files in resource bundle. 180 */ 181 private void checkExistenceOfDefaultTranslation(Set<File> filesInResourceBundle) { 182 final String fullBundleName = getFullBundleName(filesInResourceBundle); 183 final String extension = getFileExtensions()[0]; 184 final String defaultTranslationFileName = fullBundleName + extension; 185 186 final boolean missing = isMissing(defaultTranslationFileName, filesInResourceBundle); 187 if (missing) { 188 logMissingTranslation(defaultTranslationFileName); 189 } 190 } 191 192 /** 193 * Checks existence of translation files in a set of files 194 * in resource bundle. If there is no translation file 195 * with required language code, there will be a violation. 196 * The name of translation file begins with the full name 197 * of resource bundle which is followed by '_' and language code, 198 * it ends with the extension suffix. 199 * @param filesInResourceBundle a set of files in resource bundle. 200 */ 201 private void checkExistenceOfRequiredTranslations(Set<File> filesInResourceBundle) { 202 final String fullBundleName = getFullBundleName(filesInResourceBundle); 203 final String extension = getFileExtensions()[0]; 204 205 for (String languageCode : requiredTranslations) { 206 final String translationFileName = 207 fullBundleName + '_' + languageCode + extension; 208 209 final boolean missing = isMissing(translationFileName, filesInResourceBundle); 210 if (missing) { 211 final String missingTranslationFileName = 212 formMissingTranslationName(fullBundleName, languageCode); 213 logMissingTranslation(missingTranslationFileName); 214 } 215 } 216 } 217 218 /** 219 * Gets full name of resource bundle. 220 * Full name of resource bundle consists of bundle path and 221 * full base name. 222 * @param filesInResourceBundle a set of files in resource bundle. 223 * @return full name of resource bundle. 224 */ 225 private String getFullBundleName(Set<File> filesInResourceBundle) { 226 final String fullBundleName; 227 228 final File translationFile = filesInResourceBundle.iterator().next(); 229 final String translationPath = translationFile.getPath(); 230 final String extension = getFileExtensions()[0]; 231 232 final Pattern pattern = Pattern.compile("^.+_[a-z]{2}" 233 + extension + "$"); 234 final Matcher matcher = pattern.matcher(translationPath); 235 if (matcher.matches()) { 236 fullBundleName = translationPath 237 .substring(0, translationPath.lastIndexOf('_')); 238 } 239 else { 240 fullBundleName = translationPath 241 .substring(0, translationPath.lastIndexOf('.')); 242 } 243 return fullBundleName; 244 } 245 246 /** 247 * Checks whether file is missing in resource bundle. 248 * @param fileName file name. 249 * @param filesInResourceBundle a set of files in resource bundle. 250 * @return true if file is missing. 251 */ 252 private static boolean isMissing(String fileName, Set<File> filesInResourceBundle) { 253 boolean missing = false; 254 for (File file : filesInResourceBundle) { 255 final String currentFileName = file.getPath(); 256 missing = !currentFileName.equals(fileName); 257 if (!missing) { 258 break; 259 } 260 } 261 return missing; 262 } 263 264 /** 265 * Forms a name of translation file which is missing. 266 * @param fullBundleName full bundle name. 267 * @param languageCode language code. 268 * @return name of translation file which is missing. 269 */ 270 private String formMissingTranslationName(String fullBundleName, String languageCode) { 271 final String extension = getFileExtensions()[0]; 272 return String.format("%s_%s%s", fullBundleName, languageCode, extension); 273 } 274 275 /** 276 * Logs that translation file is missing. 277 * @param fullyQualifiedFileName fully qualified file name. 278 */ 279 private void logMissingTranslation(String fullyQualifiedFileName) { 280 final String filePath = extractPath(fullyQualifiedFileName); 281 282 final MessageDispatcher dispatcher = getMessageDispatcher(); 283 dispatcher.fireFileStarted(filePath); 284 285 log(0, MSG_KEY_MISSING_TRANSLATION_FILE, extractName(fullyQualifiedFileName)); 286 287 fireErrors(filePath); 288 dispatcher.fireFileFinished(filePath); 289 } 290 291 /** 292 * Extracts path from fully qualified file name. 293 * @param fullyQualifiedFileName fully qualified file name. 294 * @return file path. 295 */ 296 private static String extractPath(String fullyQualifiedFileName) { 297 return fullyQualifiedFileName 298 .substring(0, fullyQualifiedFileName.lastIndexOf(File.separator)); 299 } 300 301 /** 302 * Extracts short file name from fully qualified file name. 303 * @param fullyQualifiedFileName fully qualified file name. 304 * @return short file name. 305 */ 306 private static String extractName(String fullyQualifiedFileName) { 307 return fullyQualifiedFileName 308 .substring(fullyQualifiedFileName.lastIndexOf(File.separator) + 1); 309 } 310 311 /** 312 * Gets the basename (the unique prefix) of a property file. For example 313 * "xyz/messages" is the basename of "xyz/messages.properties", 314 * "xyz/messages_de_AT.properties", "xyz/messages_en.properties", etc. 315 * 316 * @param file the file 317 * @param basenameSeparator the basename separator 318 * @return the extracted basename 319 */ 320 private static String extractPropertyIdentifier(File file, String basenameSeparator) { 321 final String filePath = file.getPath(); 322 final int dirNameEnd = filePath.lastIndexOf(File.separatorChar); 323 final int baseNameStart = dirNameEnd + 1; 324 final int underscoreIdx = filePath.indexOf(basenameSeparator, 325 baseNameStart); 326 final int dotIdx = filePath.indexOf('.', baseNameStart); 327 final int cutoffIdx; 328 329 if (underscoreIdx == -1) { 330 cutoffIdx = dotIdx; 331 } 332 else { 333 cutoffIdx = underscoreIdx; 334 } 335 return filePath.substring(0, cutoffIdx); 336 } 337 338 /** 339 * Sets the separator used to determine the basename of a property file. 340 * This defaults to "_" 341 * 342 * @param basenameSeparator the basename separator 343 */ 344 public final void setBasenameSeparator(String basenameSeparator) { 345 this.basenameSeparator = basenameSeparator; 346 } 347 348 /** 349 * Arranges a set of property files by their prefix. 350 * The method returns a Map object. The filename prefixes 351 * work as keys each mapped to a set of files. 352 * @param propFiles the set of property files 353 * @param basenameSeparator the basename separator 354 * @return a Map object which holds the arranged property file sets 355 */ 356 private static SetMultimap<String, File> arrangePropertyFiles( 357 List<File> propFiles, String basenameSeparator) { 358 final SetMultimap<String, File> propFileMap = HashMultimap.create(); 359 360 for (final File file : propFiles) { 361 final String identifier = extractPropertyIdentifier(file, 362 basenameSeparator); 363 364 final Set<File> fileSet = propFileMap.get(identifier); 365 fileSet.add(file); 366 } 367 return propFileMap; 368 } 369 370 /** 371 * Loads the keys of the specified property file into a set. 372 * @param file the property file 373 * @return a Set object which holds the loaded keys 374 */ 375 private Set<Object> loadKeys(File file) { 376 final Set<Object> keys = Sets.newHashSet(); 377 InputStream inStream = null; 378 379 try { 380 // Load file and properties. 381 inStream = new FileInputStream(file); 382 final Properties props = new Properties(); 383 props.load(inStream); 384 385 // Gather the keys and put them into a set 386 final Enumeration<?> e = props.propertyNames(); 387 while (e.hasMoreElements()) { 388 keys.add(e.nextElement()); 389 } 390 } 391 catch (final IOException e) { 392 logIOException(e, file); 393 } 394 finally { 395 Closeables.closeQuietly(inStream); 396 } 397 return keys; 398 } 399 400 /** 401 * Helper method to log an io exception. 402 * @param ex the exception that occurred 403 * @param file the file that could not be processed 404 */ 405 private void logIOException(IOException ex, File file) { 406 String[] args = null; 407 String key = "general.fileNotFound"; 408 if (!(ex instanceof FileNotFoundException)) { 409 args = new String[] {ex.getMessage()}; 410 key = "general.exception"; 411 } 412 final LocalizedMessage message = 413 new LocalizedMessage( 414 0, 415 Definitions.CHECKSTYLE_BUNDLE, 416 key, 417 args, 418 getId(), 419 getClass(), null); 420 final SortedSet<LocalizedMessage> messages = Sets.newTreeSet(); 421 messages.add(message); 422 getMessageDispatcher().fireErrors(file.getPath(), messages); 423 LOG.debug("IOException occurred.", ex); 424 } 425 426 /** 427 * Compares the key sets of the given property files (arranged in a map) 428 * with the specified key set. All missing keys are reported. 429 * @param keys the set of keys to compare with 430 * @param fileMap a Map from property files to their key sets 431 */ 432 private void compareKeySets(Set<Object> keys, 433 SetMultimap<File, Object> fileMap) { 434 435 for (File currentFile : fileMap.keySet()) { 436 final MessageDispatcher dispatcher = getMessageDispatcher(); 437 final String path = currentFile.getPath(); 438 dispatcher.fireFileStarted(path); 439 final Set<Object> currentKeys = fileMap.get(currentFile); 440 441 // Clone the keys so that they are not lost 442 final Set<Object> keysClone = Sets.newHashSet(keys); 443 keysClone.removeAll(currentKeys); 444 445 // Remaining elements in the key set are missing in the current file 446 if (!keysClone.isEmpty()) { 447 for (Object key : keysClone) { 448 log(0, MSG_KEY, key); 449 } 450 } 451 fireErrors(path); 452 dispatcher.fireFileFinished(path); 453 } 454 } 455 456 /** 457 * Tests whether the given property files (arranged by their prefixes 458 * in a Map) contain the proper keys. 459 * 460 * <p>Each group of files must have the same keys. If this is not the case 461 * an error message is posted giving information which key misses in 462 * which file. 463 * 464 * @param propFiles the property files organized as Map 465 */ 466 private void checkPropertyFileSets(SetMultimap<String, File> propFiles) { 467 468 for (String key : propFiles.keySet()) { 469 final Set<File> files = propFiles.get(key); 470 471 if (files.size() >= 2) { 472 // build a map from files to the keys they contain 473 final Set<Object> keys = Sets.newHashSet(); 474 final SetMultimap<File, Object> fileMap = HashMultimap.create(); 475 476 for (File file : files) { 477 final Set<Object> fileKeys = loadKeys(file); 478 keys.addAll(fileKeys); 479 fileMap.putAll(file, fileKeys); 480 } 481 482 // check the map for consistency 483 compareKeySets(keys, fileMap); 484 } 485 } 486 } 487}