com.puppycrawl.tools.checkstyle.checks.TranslationCheck Maven / Gradle / Ivy
Show all versions of checkstyle Show documentation
////////////////////////////////////////////////////////////////////////////////
// checkstyle: Checks Java source code for adherence to a set of rules.
// Copyright (C) 2001-2016 the original author or authors.
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
////////////////////////////////////////////////////////////////////////////////
package com.puppycrawl.tools.checkstyle.checks;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.Locale;
import java.util.Properties;
import java.util.Set;
import java.util.SortedSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import com.google.common.base.Splitter;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.Lists;
import com.google.common.collect.SetMultimap;
import com.google.common.collect.Sets;
import com.google.common.io.Closeables;
import com.puppycrawl.tools.checkstyle.Definitions;
import com.puppycrawl.tools.checkstyle.api.AbstractFileSetCheck;
import com.puppycrawl.tools.checkstyle.api.LocalizedMessage;
import com.puppycrawl.tools.checkstyle.api.MessageDispatcher;
/**
*
* The TranslationCheck class helps to ensure the correct translation of code by
* checking property files for consistency regarding their keys.
* Two property files describing one and the same context are consistent if they
* contain the same keys.
*
*
* An example of how to configure the check is:
*
*
* <module name="Translation"/>
*
* Check has the following properties:
*
* basenameSeparator which allows setting separator in file names,
* default value is '_'.
*
* E.g.:
*
*
* messages_test.properties //separator is '_'
*
*
* app-dev.properties //separator is '-'
*
*
* requiredTranslations which allows to specify language codes of
* required translations which must exist in project. The check looks only for
* messages bundles which names contain the word 'messages'.
* Language code is composed of the lowercase, two-letter codes as defined by
* ISO 639-1.
* Default value is empty String Set which means that only the existence of
* default translation is checked.
* Note, if you specify language codes (or just one language code) of required translations
* the check will also check for existence of default translation files in project.
*
* @author Alexandra Bunge
* @author lkuehne
* @author Andrei Selkin
*/
public class TranslationCheck
extends AbstractFileSetCheck {
/**
* A key is pointing to the warning message text for missing key
* in "messages.properties" file.
*/
public static final String MSG_KEY = "translation.missingKey";
/**
* A key is pointing to the warning message text for missing translation file
* in "messages.properties" file.
*/
public static final String MSG_KEY_MISSING_TRANSLATION_FILE =
"translation.missingTranslationFile";
/** Logger for TranslationCheck. */
private static final Log LOG = LogFactory.getLog(TranslationCheck.class);
/** The property files to process. */
private final List propertyFiles = Lists.newArrayList();
/** The separator string used to separate translation files. */
private String basenameSeparator;
/**
* Language codes of required translations for the check (de, pt, ja, etc).
*/
private SortedSet requiredTranslations = ImmutableSortedSet.of();
/**
* Creates a new {@code TranslationCheck} instance.
*/
public TranslationCheck() {
setFileExtensions("properties");
basenameSeparator = "_";
}
/**
* Sets language codes of required translations for the check.
* @param translationCodes a comma separated list of language codes.
*/
public void setRequiredTranslations(String translationCodes) {
requiredTranslations = Sets.newTreeSet(Splitter.on(',')
.trimResults().omitEmptyStrings().split(translationCodes));
}
@Override
public void beginProcessing(String charset) {
super.beginProcessing(charset);
propertyFiles.clear();
}
@Override
protected void processFiltered(File file, List lines) {
propertyFiles.add(file);
}
@Override
public void finishProcessing() {
super.finishProcessing();
final SetMultimap propFilesMap =
arrangePropertyFiles(propertyFiles, basenameSeparator);
checkExistenceOfTranslations(propFilesMap);
checkPropertyFileSets(propFilesMap);
}
/**
* Checks existence of translation files (arranged in a map)
* for each resource bundle in project.
* @param translations the translation files bundles organized as Map.
*/
private void checkExistenceOfTranslations(SetMultimap translations) {
for (String fullyQualifiedBundleName : translations.keySet()) {
final String bundleBaseName = extractName(fullyQualifiedBundleName);
if (bundleBaseName.contains("messages")) {
final Set filesInBundle = translations.get(fullyQualifiedBundleName);
checkExistenceOfDefaultTranslation(filesInBundle);
checkExistenceOfRequiredTranslations(filesInBundle);
}
}
}
/**
* Checks an existence of default translation file in
* a set of files in resource bundle. The name of this file
* begins with the full name of the resource bundle and ends
* with the extension suffix.
* @param filesInResourceBundle a set of files in resource bundle.
*/
private void checkExistenceOfDefaultTranslation(Set filesInResourceBundle) {
final String fullBundleName = getFullBundleName(filesInResourceBundle);
final String extension = getFileExtensions()[0];
final String defaultTranslationFileName = fullBundleName + extension;
final boolean missing = isMissing(defaultTranslationFileName, filesInResourceBundle);
if (missing) {
logMissingTranslation(defaultTranslationFileName);
}
}
/**
* Checks existence of translation files in a set of files
* in resource bundle. If there is no translation file
* with required language code, there will be a violation.
* The name of translation file begins with the full name
* of resource bundle which is followed by '_' and language code,
* it ends with the extension suffix.
* @param filesInResourceBundle a set of files in resource bundle.
*/
private void checkExistenceOfRequiredTranslations(Set filesInResourceBundle) {
final String fullBundleName = getFullBundleName(filesInResourceBundle);
for (String languageCode : requiredTranslations) {
final String translationFileName = fullBundleName + '_' + languageCode;
final boolean missing = isMissing(translationFileName, filesInResourceBundle);
if (missing) {
final String missingTranslationFileName =
formMissingTranslationName(fullBundleName, languageCode);
logMissingTranslation(missingTranslationFileName);
}
}
}
/**
* Gets full name of resource bundle.
* Full name of resource bundle consists of bundle path and
* full base name.
* @param filesInResourceBundle a set of files in resource bundle.
* @return full name of resource bundle.
*/
private String getFullBundleName(Set filesInResourceBundle) {
final String fullBundleName;
final File firstTranslationFile = Collections.min(filesInResourceBundle);
final String translationPath = firstTranslationFile.getPath();
final String extension = getFileExtensions()[0];
final Pattern pattern = Pattern.compile("^.+_[a-z]{2}"
+ extension + "$");
final Matcher matcher = pattern.matcher(translationPath);
if (matcher.matches()) {
fullBundleName = translationPath
.substring(0, translationPath.lastIndexOf('_'));
}
else {
fullBundleName = translationPath
.substring(0, translationPath.lastIndexOf('.'));
}
return fullBundleName;
}
/**
* Checks whether file is missing in resource bundle.
* @param fileName file name.
* @param filesInResourceBundle a set of files in resource bundle.
* @return true if file is missing.
*/
private static boolean isMissing(String fileName, Set filesInResourceBundle) {
boolean missing = false;
for (File file : filesInResourceBundle) {
final String currentFileName = file.getPath();
missing = !currentFileName.contains(fileName);
if (!missing) {
break;
}
}
return missing;
}
/**
* Forms a name of translation file which is missing.
* @param fullBundleName full bundle name.
* @param languageCode language code.
* @return name of translation file which is missing.
*/
private String formMissingTranslationName(String fullBundleName, String languageCode) {
final String extension = getFileExtensions()[0];
return String.format(Locale.ROOT, "%s_%s%s", fullBundleName, languageCode, extension);
}
/**
* Logs that translation file is missing.
* @param fullyQualifiedFileName fully qualified file name.
*/
private void logMissingTranslation(String fullyQualifiedFileName) {
final String filePath = extractPath(fullyQualifiedFileName);
final MessageDispatcher dispatcher = getMessageDispatcher();
dispatcher.fireFileStarted(filePath);
log(0, MSG_KEY_MISSING_TRANSLATION_FILE, extractName(fullyQualifiedFileName));
fireErrors(filePath);
dispatcher.fireFileFinished(filePath);
}
/**
* Extracts path from fully qualified file name.
* @param fullyQualifiedFileName fully qualified file name.
* @return file path.
*/
private static String extractPath(String fullyQualifiedFileName) {
return fullyQualifiedFileName
.substring(0, fullyQualifiedFileName.lastIndexOf(File.separator));
}
/**
* Extracts short file name from fully qualified file name.
* @param fullyQualifiedFileName fully qualified file name.
* @return short file name.
*/
private static String extractName(String fullyQualifiedFileName) {
return fullyQualifiedFileName
.substring(fullyQualifiedFileName.lastIndexOf(File.separator) + 1);
}
/**
* Gets the basename (the unique prefix) of a property file. For example
* "xyz/messages" is the basename of "xyz/messages.properties",
* "xyz/messages_de_AT.properties", "xyz/messages_en.properties", etc.
*
* @param file the file
* @param basenameSeparator the basename separator
* @return the extracted basename
*/
private static String extractPropertyIdentifier(File file, String basenameSeparator) {
final String filePath = file.getPath();
final int dirNameEnd = filePath.lastIndexOf(File.separatorChar);
final int baseNameStart = dirNameEnd + 1;
final int underscoreIdx = filePath.indexOf(basenameSeparator,
baseNameStart);
final int dotIdx = filePath.indexOf('.', baseNameStart);
final int cutoffIdx;
if (underscoreIdx == -1) {
cutoffIdx = dotIdx;
}
else {
cutoffIdx = underscoreIdx;
}
return filePath.substring(0, cutoffIdx);
}
/**
* Sets the separator used to determine the basename of a property file.
* This defaults to "_"
*
* @param basenameSeparator the basename separator
*/
public final void setBasenameSeparator(String basenameSeparator) {
this.basenameSeparator = basenameSeparator;
}
/**
* Arranges a set of property files by their prefix.
* The method returns a Map object. The filename prefixes
* work as keys each mapped to a set of files.
* @param propFiles the set of property files
* @param basenameSeparator the basename separator
* @return a Map object which holds the arranged property file sets
*/
private static SetMultimap arrangePropertyFiles(
List propFiles, String basenameSeparator) {
final SetMultimap propFileMap = HashMultimap.create();
for (final File file : propFiles) {
final String identifier = extractPropertyIdentifier(file,
basenameSeparator);
final Set fileSet = propFileMap.get(identifier);
fileSet.add(file);
}
return propFileMap;
}
/**
* Loads the keys of the specified property file into a set.
* @param file the property file
* @return a Set object which holds the loaded keys
*/
private Set