1 import java.io.BufferedReader;
3 import java.io.FileReader;
4 import java.io.IOException;
5 import java.util.HashSet;
6 import java.util.Properties;
7 import java.util.TreeSet;
8 import java.util.regex.Pattern;
11 * This class scans Java source files for calls to MessageManager and reports
13 * <li>calls using keys not found in Messages.properties</li>
14 * <li>any unused keys in Messages.properties</li>
16 * It does not handle dynamically constructed keys, these are reported as
17 * possible errors for manual inspection. <br>
18 * For comparing translated bundles with Messages.properties, see i18nAnt.xml
23 public class MessageBundleChecker
27 * opening quote, closing quote, no quotes in between
29 static Pattern STRING_PATTERN = Pattern.compile("^\"[^\"]*\"$");
32 * number of text lines to read at a time in order to parse
33 * code that is split over several lines
35 static int bufferSize = 3;
38 * resource bundle key is arg0 for these methods
40 static final String METHOD1 = "MessageManager.getString(";
42 static final String METHOD2 = "MessageManager.formatMessage(";
44 static final String METHOD3 = "MessageManager.getStringOrReturn(";
47 * resource bundle key is arg1 for this method
49 static final String JVINIT = "JvSwingUtils.jvInitComponent(";
51 static final String[] METHODS = { METHOD1, METHOD2, METHOD3, JVINIT };
54 * root of the Java source folders we want to scan
59 * contents of Messages.properties
61 private Properties messages;
64 * keys from Messages.properties
65 * we remove entries from here as they are found to be used
66 * any left over are unused entries
68 private TreeSet<String> messageKeys;
70 private int javaCount;
72 private HashSet<String> invalidKeys;
75 * Runs the scan given the path to the root of Java source directories
78 * [0] path to the source folder to scan
80 * [1] (optional) read buffer size (default is 3); increasing this
81 * may detect more results but will give higher error counts due to
82 * double counting of the same code
85 public static void main(String[] args) throws IOException
87 if (args.length != 1 && args.length != 2)
89 System.out.println("Usage: <pathToSourceFolder> [readBufferSize]");
94 bufferSize = Integer.valueOf(args[1]);
96 new MessageBundleChecker().doMain(args[0]);
100 * Main method to perform the work
103 * @throws IOException
105 private void doMain(String srcPath) throws IOException
107 System.out.println("Scanning " + srcPath
108 + " for calls to MessageManager");
109 sourcePath = srcPath;
111 File dir = new File(srcPath);
114 System.out.println(srcPath + " not found");
117 invalidKeys = new HashSet<String>();
118 if (dir.isDirectory())
130 * Prints out counts to sysout
132 private void reportResults()
134 System.out.println("\nScanned " + javaCount + " source files");
135 System.out.println("Message.properties has " + messages.size()
137 System.out.println("Found " + invalidKeys.size()
138 + " possibly invalid parameter calls");
140 System.out.println(messageKeys.size()
141 + " keys not found, either unused or constructed dynamically");
142 for (String key : messageKeys)
144 System.out.println(" " + key);
149 * Scan all files within a directory
152 * @throws IOException
154 private void scanDirectory(File dir) throws IOException
156 File[] files = dir.listFiles();
178 private void scanFile(File f) throws IOException
180 String path = f.getPath();
181 if (!path.endsWith(".java"))
188 * skip class with designed dynamic lookup call
190 if (path.endsWith("gui/JvSwingUtils.java"))
195 String[] lines = new String[bufferSize];
196 BufferedReader br = new BufferedReader(new FileReader(f));
197 for (int i = 0; i < bufferSize; i++)
199 String readLine = br.readLine();
200 lines[i] = stripCommentsAndTrim(readLine);
205 while (lines[bufferSize - 1] != null)
208 inspectSourceLines(path, lineNo, lines);
210 for (int i = 0; i < bufferSize - 1; i++)
212 lines[i] = lines[i + 1];
214 lines[bufferSize - 1] = stripCommentsAndTrim(br.readLine());
221 * removes anything after (and including) '//'
223 private String stripCommentsAndTrim(String line)
227 int pos = line.indexOf("//");
230 line = line.substring(0, pos);
232 line = line.replace("\t", " ").trim();
238 * Look for calls to MessageManager methods, possibly split over two or more
245 private void inspectSourceLines(String path, int lineNo, String[] lines)
247 String lineNos = String.format("%d-%d", lineNo, lineNo + lines.length
249 String combined = combineLines(lines);
250 for (String method : METHODS)
252 int pos = combined.indexOf(method);
259 * extract what follows the opening bracket of the method call
261 String methodArgs = combined.substring(pos + method.length()).trim();
262 if ("".equals(methodArgs))
265 * arguments are on next line - catch in the next read loop iteration
269 if (methodArgs.indexOf(",") == -1 && methodArgs.indexOf(")") == -1)
272 * arguments continue on next line - catch in the next read loop iteration
277 if (JVINIT == method && methodArgs.indexOf(",") == -1)
280 * not interested in 1-arg calls to jvInitComponent
285 if (METHOD3 == method)
287 System.out.println(String.format("Dynamic key at %s line %s %s",
288 path.substring(sourcePath.length()), lineNos, combined));
292 String messageKey = getMessageKey(method, methodArgs);
293 if (messageKey == null)
295 System.out.println(String.format("Trouble parsing %s line %s %s",
296 path.substring(sourcePath.length()), lineNos, combined));
300 if (!(STRING_PATTERN.matcher(messageKey).matches()))
302 System.out.println(String.format("Dynamic key at %s line %s %s",
303 path.substring(sourcePath.length()), lineNos, combined));
308 * strip leading and trailing quote
310 messageKey = messageKey.substring(1, messageKey.length() - 1);
312 if (!this.messages.containsKey(messageKey))
314 System.out.println(String.format(
315 "Unmatched key '%s' at line %s of %s", messageKey, lineNos,
316 path.substring(sourcePath.length())));
317 if (!invalidKeys.contains(messageKey))
319 invalidKeys.add(messageKey);
322 messageKeys.remove(messageKey);
327 * Helper method to parse out the resource bundle key parameter of a method
332 * the rest of the source line starting with arguments to method
335 private String getMessageKey(String method, String methodArgs)
337 String key = methodArgs;
340 * locate second argument if calling jvInitComponent()
342 if (method == JVINIT)
344 int commaLoc = methodArgs.indexOf(",");
349 key = key.substring(commaLoc + 1).trim();
353 * take up to next comma or ) or end of line
355 int commaPos = key.indexOf(",");
356 int bracePos = key.indexOf(")");
357 int endPos = commaPos == -1 ? bracePos : (bracePos == -1 ? commaPos
358 : Math.min(commaPos, bracePos));
359 if (endPos == -1 && key.length() > 1 && key.endsWith("\""))
361 endPos = key.length();
364 return endPos == -1 ? null : key.substring(0, endPos);
367 private String combineLines(String[] lines)
369 String combined = "";
372 for (String line : lines)
384 * Loads properties from Message.properties
386 * @throws IOException
388 void loadMessages() throws IOException
390 messages = new Properties();
391 FileReader reader = new FileReader(new File(sourcePath,
392 "../resources/lang/Messages.properties"));
393 messages.load(reader);
396 messageKeys = new TreeSet<String>();
397 for (Object key : messages.keySet())
399 messageKeys.add((String) key);