JAL-1424 utility to check resource bundle labels used in code (and some
[jalview.git] / utils / MessageBundleChecker.java
diff --git a/utils/MessageBundleChecker.java b/utils/MessageBundleChecker.java
new file mode 100644 (file)
index 0000000..441e474
--- /dev/null
@@ -0,0 +1,308 @@
+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);
+    }
+
+  }
+
+}