--- /dev/null
+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
+ * <ul>
+ * <li>calls using keys not found in Messages.properties</li>
+ * <li>any unused keys in Messages.properties</li>
+ * </ul>
+ * It does not handle dynamically constructed keys, these are reported as
+ * possible errors for manual inspection. <br>
+ * 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<String> messageKeys;
+
+ private int javaCount;
+
+ private HashSet<String> 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: <pathToSourceFolder> [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<String>();
+ 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("Trouble parsing %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<String>();
+ for (Object key : messages.keySet())
+ {
+ messageKeys.add((String) key);
+ }
+
+ }
+
+}