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 * @author Alexandra Bunge 056 * @author lkuehne 057 */ 058public class TranslationCheck 059 extends AbstractFileSetCheck 060{ 061 /** The property files to process. */ 062 private final List<File> mPropertyFiles = Lists.newArrayList(); 063 064 /** 065 * Creates a new <code>TranslationCheck</code> instance. 066 */ 067 public TranslationCheck() 068 { 069 setFileExtensions(new String[]{"properties"}); 070 } 071 072 @Override 073 public void beginProcessing(String aCharset) 074 { 075 super.beginProcessing(aCharset); 076 mPropertyFiles.clear(); 077 } 078 079 @Override 080 protected void processFiltered(File aFile, List<String> aLines) 081 { 082 mPropertyFiles.add(aFile); 083 } 084 085 @Override 086 public void finishProcessing() 087 { 088 super.finishProcessing(); 089 final Map<String, Set<File>> propFilesMap = 090 arrangePropertyFiles(mPropertyFiles); 091 checkPropertyFileSets(propFilesMap); 092 } 093 094 /** 095 * Gets the basename (the unique prefix) of a property file. For example 096 * "xyz/messages" is the basename of "xyz/messages.properties", 097 * "xyz/messages_de_AT.properties", "xyz/messages_en.properties", etc. 098 * 099 * @param aFile the file 100 * @return the extracted basename 101 */ 102 private static String extractPropertyIdentifier(final File aFile) 103 { 104 final String filePath = aFile.getPath(); 105 final int dirNameEnd = filePath.lastIndexOf(File.separatorChar); 106 final int baseNameStart = dirNameEnd + 1; 107 final int underscoreIdx = filePath.indexOf('_', baseNameStart); 108 final int dotIdx = filePath.indexOf('.', baseNameStart); 109 final int cutoffIdx = (underscoreIdx != -1) ? underscoreIdx : dotIdx; 110 return filePath.substring(0, cutoffIdx); 111 } 112 113 /** 114 * Arranges a set of property files by their prefix. 115 * The method returns a Map object. The filename prefixes 116 * work as keys each mapped to a set of files. 117 * @param aPropFiles the set of property files 118 * @return a Map object which holds the arranged property file sets 119 */ 120 private static Map<String, Set<File>> arrangePropertyFiles( 121 List<File> aPropFiles) 122 { 123 final Map<String, Set<File>> propFileMap = Maps.newHashMap(); 124 125 for (final File f : aPropFiles) { 126 final String identifier = extractPropertyIdentifier(f); 127 128 Set<File> fileSet = propFileMap.get(identifier); 129 if (fileSet == null) { 130 fileSet = Sets.newHashSet(); 131 propFileMap.put(identifier, fileSet); 132 } 133 fileSet.add(f); 134 } 135 return propFileMap; 136 } 137 138 /** 139 * Loads the keys of the specified property file into a set. 140 * @param aFile the property file 141 * @return a Set object which holds the loaded keys 142 */ 143 private Set<Object> loadKeys(File aFile) 144 { 145 final Set<Object> keys = Sets.newHashSet(); 146 InputStream inStream = null; 147 148 try { 149 // Load file and properties. 150 inStream = new FileInputStream(aFile); 151 final Properties props = new Properties(); 152 props.load(inStream); 153 154 // Gather the keys and put them into a set 155 final Enumeration<?> e = props.propertyNames(); 156 while (e.hasMoreElements()) { 157 keys.add(e.nextElement()); 158 } 159 } 160 catch (final IOException e) { 161 logIOException(e, aFile); 162 } 163 finally { 164 Utils.closeQuietly(inStream); 165 } 166 return keys; 167 } 168 169 /** 170 * helper method to log an io exception. 171 * @param aEx the exception that occured 172 * @param aFile the file that could not be processed 173 */ 174 private void logIOException(IOException aEx, File aFile) 175 { 176 String[] args = null; 177 String key = "general.fileNotFound"; 178 if (!(aEx instanceof FileNotFoundException)) { 179 args = new String[] {aEx.getMessage()}; 180 key = "general.exception"; 181 } 182 final LocalizedMessage message = 183 new LocalizedMessage( 184 0, 185 Defn.CHECKSTYLE_BUNDLE, 186 key, 187 args, 188 getId(), 189 this.getClass(), null); 190 final TreeSet<LocalizedMessage> messages = Sets.newTreeSet(); 191 messages.add(message); 192 getMessageDispatcher().fireErrors(aFile.getPath(), messages); 193 Utils.getExceptionLogger().debug("IOException occured.", aEx); 194 } 195 196 197 /** 198 * Compares the key sets of the given property files (arranged in a map) 199 * with the specified key set. All missing keys are reported. 200 * @param aKeys the set of keys to compare with 201 * @param aFileMap a Map from property files to their key sets 202 */ 203 private void compareKeySets(Set<Object> aKeys, 204 Map<File, Set<Object>> aFileMap) 205 { 206 final Set<Entry<File, Set<Object>>> fls = aFileMap.entrySet(); 207 208 for (Entry<File, Set<Object>> entry : fls) { 209 final File currentFile = entry.getKey(); 210 final MessageDispatcher dispatcher = getMessageDispatcher(); 211 final String path = currentFile.getPath(); 212 dispatcher.fireFileStarted(path); 213 final Set<Object> currentKeys = entry.getValue(); 214 215 // Clone the keys so that they are not lost 216 final Set<Object> keysClone = Sets.newHashSet(aKeys); 217 keysClone.removeAll(currentKeys); 218 219 // Remaining elements in the key set are missing in the current file 220 if (!keysClone.isEmpty()) { 221 for (Object key : keysClone) { 222 log(0, "translation.missingKey", key); 223 } 224 } 225 fireErrors(path); 226 dispatcher.fireFileFinished(path); 227 } 228 } 229 230 231 /** 232 * Tests whether the given property files (arranged by their prefixes 233 * in a Map) contain the proper keys. 234 * 235 * Each group of files must have the same keys. If this is not the case 236 * an error message is posted giving information which key misses in 237 * which file. 238 * 239 * @param aPropFiles the property files organized as Map 240 */ 241 private void checkPropertyFileSets(Map<String, Set<File>> aPropFiles) 242 { 243 final Set<Entry<String, Set<File>>> entrySet = aPropFiles.entrySet(); 244 245 for (Entry<String, Set<File>> entry : entrySet) { 246 final Set<File> files = entry.getValue(); 247 248 if (files.size() >= 2) { 249 // build a map from files to the keys they contain 250 final Set<Object> keys = Sets.newHashSet(); 251 final Map<File, Set<Object>> fileMap = Maps.newHashMap(); 252 253 for (File file : files) { 254 final Set<Object> fileKeys = loadKeys(file); 255 keys.addAll(fileKeys); 256 fileMap.put(file, fileKeys); 257 } 258 259 // check the map for consistency 260 compareKeySets(keys, fileMap); 261 } 262 } 263 } 264}