001//////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code for adherence to a set of rules. 003// Copyright (C) 2001-2014 Oliver Burn 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//////////////////////////////////////////////////////////////////////////////// 019package com.puppycrawl.tools.checkstyle.checks; 020 021import com.google.common.collect.Lists; 022import com.google.common.collect.Maps; 023import com.google.common.collect.Sets; 024import com.puppycrawl.tools.checkstyle.Defn; 025import com.puppycrawl.tools.checkstyle.api.AbstractFileSetCheck; 026import com.puppycrawl.tools.checkstyle.api.LocalizedMessage; 027import com.puppycrawl.tools.checkstyle.api.MessageDispatcher; 028import com.puppycrawl.tools.checkstyle.api.Utils; 029import java.io.File; 030import java.io.FileInputStream; 031import java.io.FileNotFoundException; 032import java.io.IOException; 033import java.io.InputStream; 034import java.util.Enumeration; 035import java.util.List; 036import java.util.Map; 037import java.util.Properties; 038import java.util.Set; 039import java.util.TreeSet; 040import java.util.Map.Entry; 041 042/** 043 * <p> 044 * The TranslationCheck class helps to ensure the correct translation of code by 045 * checking property files for consistency regarding their keys. 046 * Two property files describing one and the same context are consistent if they 047 * contain the same keys. 048 * </p> 049 * <p> 050 * An example of how to configure the check is: 051 * </p> 052 * <pre> 053 * <module name="Translation"/> 054 * </pre> 055 * Check has a property <b>basenameSeparator</b> which allows setting separator in file names, 056 * default value is '_'. 057 * <p> 058 * E.g.: 059 * </p> 060 * <p> 061 * messages_test.properties //separator is '_' 062 * </p> 063 * <p> 064 * app-dev.properties //separator is '-' 065 * </p> 066 * <br> 067 * @author Alexandra Bunge 068 * @author lkuehne 069 */ 070public class TranslationCheck 071 extends AbstractFileSetCheck 072{ 073 /** The property files to process. */ 074 private final List<File> propertyFiles = Lists.newArrayList(); 075 076 /** The separator string used to separate translation files */ 077 private String basenameSeparator; 078 079 /** 080 * Creates a new <code>TranslationCheck</code> instance. 081 */ 082 public TranslationCheck() 083 { 084 setFileExtensions(new String[]{"properties"}); 085 setBasenameSeparator("_"); 086 } 087 088 @Override 089 public void beginProcessing(String charset) 090 { 091 super.beginProcessing(charset); 092 propertyFiles.clear(); 093 } 094 095 @Override 096 protected void processFiltered(File file, List<String> lines) 097 { 098 propertyFiles.add(file); 099 } 100 101 @Override 102 public void finishProcessing() 103 { 104 super.finishProcessing(); 105 final Map<String, Set<File>> propFilesMap = 106 arrangePropertyFiles(propertyFiles, basenameSeparator); 107 checkPropertyFileSets(propFilesMap); 108 } 109 110 /** 111 * Gets the basename (the unique prefix) of a property file. For example 112 * "xyz/messages" is the basename of "xyz/messages.properties", 113 * "xyz/messages_de_AT.properties", "xyz/messages_en.properties", etc. 114 * 115 * @param file the file 116 * @param basenameSeparator the basename separator 117 * @return the extracted basename 118 */ 119 private static String extractPropertyIdentifier(final File file, 120 final String basenameSeparator) 121 { 122 final String filePath = file.getPath(); 123 final int dirNameEnd = filePath.lastIndexOf(File.separatorChar); 124 final int baseNameStart = dirNameEnd + 1; 125 final int underscoreIdx = filePath.indexOf(basenameSeparator, 126 baseNameStart); 127 final int dotIdx = filePath.indexOf('.', baseNameStart); 128 final int cutoffIdx = (underscoreIdx != -1) ? underscoreIdx : dotIdx; 129 return filePath.substring(0, cutoffIdx); 130 } 131 132 /** 133 * Sets the separator used to determine the basename of a property file. 134 * This defaults to "_" 135 * 136 * @param basenameSeparator the basename separator 137 */ 138 public void setBasenameSeparator(String basenameSeparator) 139 { 140 this.basenameSeparator = basenameSeparator; 141 } 142 143 /** 144 * Arranges a set of property files by their prefix. 145 * The method returns a Map object. The filename prefixes 146 * work as keys each mapped to a set of files. 147 * @param propFiles the set of property files 148 * @param basenameSeparator the basename separator 149 * @return a Map object which holds the arranged property file sets 150 */ 151 private static Map<String, Set<File>> arrangePropertyFiles( 152 List<File> propFiles, String basenameSeparator) 153 { 154 final Map<String, Set<File>> propFileMap = Maps.newHashMap(); 155 156 for (final File f : propFiles) { 157 final String identifier = extractPropertyIdentifier(f, 158 basenameSeparator); 159 160 Set<File> fileSet = propFileMap.get(identifier); 161 if (fileSet == null) { 162 fileSet = Sets.newHashSet(); 163 propFileMap.put(identifier, fileSet); 164 } 165 fileSet.add(f); 166 } 167 return propFileMap; 168 } 169 170 /** 171 * Loads the keys of the specified property file into a set. 172 * @param file the property file 173 * @return a Set object which holds the loaded keys 174 */ 175 private Set<Object> loadKeys(File file) 176 { 177 final Set<Object> keys = Sets.newHashSet(); 178 InputStream inStream = null; 179 180 try { 181 // Load file and properties. 182 inStream = new FileInputStream(file); 183 final Properties props = new Properties(); 184 props.load(inStream); 185 186 // Gather the keys and put them into a set 187 final Enumeration<?> e = props.propertyNames(); 188 while (e.hasMoreElements()) { 189 keys.add(e.nextElement()); 190 } 191 } 192 catch (final IOException e) { 193 logIOException(e, file); 194 } 195 finally { 196 Utils.closeQuietly(inStream); 197 } 198 return keys; 199 } 200 201 /** 202 * helper method to log an io exception. 203 * @param ex the exception that occured 204 * @param file the file that could not be processed 205 */ 206 private void logIOException(IOException ex, File file) 207 { 208 String[] args = null; 209 String key = "general.fileNotFound"; 210 if (!(ex instanceof FileNotFoundException)) { 211 args = new String[] {ex.getMessage()}; 212 key = "general.exception"; 213 } 214 final LocalizedMessage message = 215 new LocalizedMessage( 216 0, 217 Defn.CHECKSTYLE_BUNDLE, 218 key, 219 args, 220 getId(), 221 this.getClass(), null); 222 final TreeSet<LocalizedMessage> messages = Sets.newTreeSet(); 223 messages.add(message); 224 getMessageDispatcher().fireErrors(file.getPath(), messages); 225 Utils.getExceptionLogger().debug("IOException occured.", ex); 226 } 227 228 229 /** 230 * Compares the key sets of the given property files (arranged in a map) 231 * with the specified key set. All missing keys are reported. 232 * @param keys the set of keys to compare with 233 * @param fileMap a Map from property files to their key sets 234 */ 235 private void compareKeySets(Set<Object> keys, 236 Map<File, Set<Object>> fileMap) 237 { 238 final Set<Entry<File, Set<Object>>> fls = fileMap.entrySet(); 239 240 for (Entry<File, Set<Object>> entry : fls) { 241 final File currentFile = entry.getKey(); 242 final MessageDispatcher dispatcher = getMessageDispatcher(); 243 final String path = currentFile.getPath(); 244 dispatcher.fireFileStarted(path); 245 final Set<Object> currentKeys = entry.getValue(); 246 247 // Clone the keys so that they are not lost 248 final Set<Object> keysClone = Sets.newHashSet(keys); 249 keysClone.removeAll(currentKeys); 250 251 // Remaining elements in the key set are missing in the current file 252 if (!keysClone.isEmpty()) { 253 for (Object key : keysClone) { 254 log(0, "translation.missingKey", key); 255 } 256 } 257 fireErrors(path); 258 dispatcher.fireFileFinished(path); 259 } 260 } 261 262 263 /** 264 * Tests whether the given property files (arranged by their prefixes 265 * in a Map) contain the proper keys. 266 * 267 * Each group of files must have the same keys. If this is not the case 268 * an error message is posted giving information which key misses in 269 * which file. 270 * 271 * @param propFiles the property files organized as Map 272 */ 273 private void checkPropertyFileSets(Map<String, Set<File>> propFiles) 274 { 275 final Set<Entry<String, Set<File>>> entrySet = propFiles.entrySet(); 276 277 for (Entry<String, Set<File>> entry : entrySet) { 278 final Set<File> files = entry.getValue(); 279 280 if (files.size() >= 2) { 281 // build a map from files to the keys they contain 282 final Set<Object> keys = Sets.newHashSet(); 283 final Map<File, Set<Object>> fileMap = Maps.newHashMap(); 284 285 for (File file : files) { 286 final Set<Object> fileKeys = loadKeys(file); 287 keys.addAll(fileKeys); 288 fileMap.put(file, fileKeys); 289 } 290 291 // check the map for consistency 292 compareKeySets(keys, fileMap); 293 } 294 } 295 } 296}