import java.io.BufferedReader; import java.io.File; import java.io.FileReader; import java.io.IOException; import java.util.HashSet; import java.util.Properties; import java.util.TreeSet; /** * This class scans Java source files for calls to MessageManager and reports * * It does not handle dynamically constructed keys, these are reported as * possible errors for manual inspection.
* For comparing translated bundles with Messages.properties, see i18nAnt.xml * * @author gmcarstairs * */ public class MessageBundleChecker { /* * number of text lines to read at a time in order to parse * code that is split over several lines */ static int bufferSize = 3; static final String METHOD1 = "MessageManager.getString("; static final String METHOD2 = "MessageManager.getStringOrReturn("; static final String METHOD3 = "MessageManager.formatMessage("; static final String[] METHODS = { METHOD1, METHOD2, METHOD3 }; /* * root of the Java source folders we want to scan */ String sourcePath; /* * contents of Messages.properties */ private Properties messages; /* * keys from Messages.properties * we remove entries from here as they are found to be used * any left over are unused entries */ private TreeSet messageKeys; private int javaCount; private HashSet invalidKeys; /** * Runs the scan given the path to the root of Java source directories * * @param args * [0] path to the source folder to scan * @param args * [1] (optional) read buffer size (default is 3); increasing this * may detect more results but will give higher error counts due to * double counting of the same code * @throws IOException */ public static void main(String[] args) throws IOException { if (args.length != 1 && args.length != 2) { System.out.println("Usage: [readBufferSize]"); return; } if (args.length == 2) { bufferSize = Integer.valueOf(args[1]); } new MessageBundleChecker().doMain(args[0]); } /** * Main method to perform the work * * @param srcPath * @throws IOException */ private void doMain(String srcPath) throws IOException { System.out.println("Scanning " + srcPath + " for calls to MessageManager"); sourcePath = srcPath; loadMessages(); File dir = new File(srcPath); if (!dir.exists()) { System.out.println(srcPath + " not found"); return; } invalidKeys = new HashSet(); if (dir.isDirectory()) { scanDirectory(dir); } else { scanFile(dir); } reportResults(); } /** * Prints out counts to sysout */ private void reportResults() { System.out.println("\nScanned " + javaCount + " source files"); System.out.println("Message.properties has " + messages.size() + " keys"); System.out.println("Found " + invalidKeys.size() + " possibly invalid parameter calls"); System.out.println(messageKeys.size() + " keys not found, possibly unused"); for (String key : messageKeys) { System.out.println(" " + key); } } /** * Scan all files within a directory * * @param dir * @throws IOException */ private void scanDirectory(File dir) throws IOException { File[] files = dir.listFiles(); if (files != null) { for (File f : files) { if (f.isDirectory()) { scanDirectory(f); } else { scanFile(f); } } } } /** * Scan a Java file * * @param f */ private void scanFile(File f) throws IOException { String path = f.getPath(); if (!path.endsWith(".java")) { return; } javaCount++; String[] lines = new String[bufferSize]; BufferedReader br = new BufferedReader(new FileReader(f)); for (int i = 0; i < bufferSize; i++) { String readLine = br.readLine(); lines[i] = stripCommentsAndTrim(readLine); } int lineNo = 0; while (lines[bufferSize - 1] != null) { lineNo++; inspectSourceLines(path, lineNo, lines); for (int i = 0; i < bufferSize - 1; i++) { lines[i] = lines[i + 1]; } lines[bufferSize - 1] = stripCommentsAndTrim(br.readLine()); } br.close(); } /* * removes anything after (and including) '//' */ private String stripCommentsAndTrim(String line) { if (line != null) { int pos = line.indexOf("//"); if (pos != -1) { line = line.substring(0, pos); } line = line.replace("\t", " ").trim(); } return line; } /** * Look for calls to MessageManager methods, possibly split over two or more * lines * * @param path * @param lineNo * @param lines */ private void inspectSourceLines(String path, int lineNo, String[] lines) { String lineNos = String.format("%d-%d", lineNo, lineNo + lines.length - 1); String combined = combineLines(lines); for (String method : METHODS) { int pos = combined.indexOf(method); if (pos == -1) { continue; } String methodArgs = combined.substring(pos + method.length()); if ("".equals(methodArgs)) { /* * continues on next line - catch in the next read loop iteration */ continue; } if (!methodArgs.startsWith("\"")) { System.out.println(String.format( "Possible dynamic key at %s line %s %s", path.substring(sourcePath.length()), lineNos, combined)); continue; } methodArgs = methodArgs.substring(1); int quotePos = methodArgs.indexOf("\""); if (quotePos == -1) { System.out.println(String.format("Trouble parsing %s line %s %s", path.substring(sourcePath.length()), lineNos, combined)); continue; } String messageKey = methodArgs.substring(0, quotePos); if (!this.messages.containsKey(messageKey)) { System.out.println(String.format( "Unmatched key '%s' at line %s of %s", messageKey, lineNos, path.substring(sourcePath.length()))); if (!invalidKeys.contains(messageKey)) { invalidKeys.add(messageKey); } } messageKeys.remove(messageKey); } } private String combineLines(String[] lines) { String combined = ""; if (lines != null) { for (String line : lines) { if (line != null) { combined += line; } } } return combined; } /** * Loads properties from Message.properties * * @throws IOException */ void loadMessages() throws IOException { messages = new Properties(); FileReader reader = new FileReader(new File(sourcePath, "../resources/lang/Messages.properties")); messages.load(reader); reader.close(); messageKeys = new TreeSet(); for (Object key : messages.keySet()) { messageKeys.add((String) key); } } }