JAL-3521 High quality file icons for jalview associated files, and associated mime...
[jalview.git] / utils / MessageBundleChecker.java
index 7850eb5..c870f6d 100644 (file)
@@ -1,10 +1,32 @@
+/*
+ * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
+ * Copyright (C) $$Year-Rel$$ The Jalview Authors
+ * 
+ * This file is part of Jalview.
+ * 
+ * Jalview is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License 
+ * as published by the Free Software Foundation, either version 3
+ * of the License, or (at your option) any later version.
+ *  
+ * Jalview is distributed in the hope that it will be useful, but 
+ * WITHOUT ANY WARRANTY; without even the implied warranty 
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
+ * PURPOSE.  See the GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
+ * The Jalview Authors are detailed in the 'AUTHORS' file.
+ */
 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.Set;
 import java.util.TreeSet;
+import java.util.regex.Pattern;
 
 /**
  * This class scans Java source files for calls to MessageManager and reports
@@ -19,21 +41,35 @@ import java.util.TreeSet;
  * @author gmcarstairs
  *
  */
-public class MessageBundleChecker
+public class MessageBundleChecker implements BufferedLineReader.LineCleaner
 {
   /*
+   * regex ^"[^"]*"$
+   * opening quote, closing quote, no quotes in between
+   */
+  static Pattern STRING_PATTERN = Pattern.compile("^\"[^\"]*\"$");
+
+  /*
    * number of text lines to read at a time in order to parse
    * code that is split over several lines
    */
   static int bufferSize = 3;
 
+  /*
+   * resource bundle key is arg0 for these methods
+   */
   static final String METHOD1 = "MessageManager.getString(";
 
-  static final String METHOD2 = "MessageManager.getStringOrReturn(";
+  static final String METHOD2 = "MessageManager.formatMessage(";
 
-  static final String METHOD3 = "MessageManager.formatMessage(";
+  static final String METHOD3 = "MessageManager.getStringOrReturn(";
+
+  /*
+   * resource bundle key is arg1 for this method
+   */
+  static final String JVINIT = "JvSwingUtils.jvInitComponent(";
 
-  static final String[] METHODS = { METHOD1, METHOD2, METHOD3 };
+  static final String[] METHODS = { METHOD1, METHOD2, METHOD3, JVINIT };
 
   /*
    * root of the Java source folders we want to scan
@@ -54,7 +90,9 @@ public class MessageBundleChecker
 
   private int javaCount;
 
-  private HashSet<String> invalidKeys;
+  private Set<String> invalidKeys;
+
+  private Set<String> dynamicKeys;
 
   /**
    * Runs the scan given the path to the root of Java source directories
@@ -90,7 +128,7 @@ public class MessageBundleChecker
   private void doMain(String srcPath) throws IOException
   {
     System.out.println("Scanning " + srcPath
-            + " for calls to MessageManager");
+            + " for calls to MessageManager\n");
     sourcePath = srcPath;
     loadMessages();
     File dir = new File(srcPath);
@@ -99,7 +137,10 @@ public class MessageBundleChecker
       System.out.println(srcPath + " not found");
       return;
     }
-    invalidKeys = new HashSet<String>();
+
+    invalidKeys = new HashSet<>();
+    dynamicKeys = new HashSet<>();
+
     if (dir.isDirectory())
     {
       scanDirectory(dir);
@@ -117,17 +158,60 @@ public class MessageBundleChecker
   private void reportResults()
   {
     System.out.println("\nScanned " + javaCount + " source files");
-    System.out.println("Message.properties has " + messages.size()
+    System.out.println(
+            "Messages.properties has " + messages.size()
             + " keys");
-    System.out.println("Found " + invalidKeys.size()
-            + " possibly invalid parameter calls");
+    if (!invalidKeys.isEmpty())
+    {
+      System.out.println("Found " + invalidKeys.size()
+              + " possibly invalid parameter call"
+              + (invalidKeys.size() > 1 ? "s" : ""));
+    }
 
-    System.out.println(messageKeys.size()
-            + " keys not found, possibly unused");
+    System.out.println("Keys not found, assumed constructed dynamically:");
+    int dynamicCount = 0;
     for (String key : messageKeys)
     {
-      System.out.println("    " + key);
+      if (isDynamic(key))
+      {
+        System.out.println("    " + key);
+        dynamicCount++;
+      }
+    }
+
+    if (dynamicCount < messageKeys.size())
+    {
+      System.out.println((messageKeys.size() - dynamicCount)
+              + " keys not found, possibly unused");
+      for (String key : messageKeys)
+      {
+        if (!isDynamic(key))
+        {
+          System.out.println("    " + key);
+        }
+      }
+    }
+    System.out
+            .println("(Run i18nAnt.xml to compare other message bundles)");
+  }
+
+  /**
+   * Answers true if the key starts with one of the recorded dynamic key stubs,
+   * else false
+   * 
+   * @param key
+   * @return
+   */
+  private boolean isDynamic(String key)
+  {
+    for (String dynamic : dynamicKeys)
+    {
+      if (key.startsWith(dynamic))
+      {
+        return true;
+      }
     }
+    return false;
   }
 
   /**
@@ -169,92 +253,107 @@ public class MessageBundleChecker
     }
     javaCount++;
 
-    String[] lines = new String[bufferSize];
-    BufferedReader br = new BufferedReader(new FileReader(f));
-    for (int i = 0; i < bufferSize; i++)
+    /*
+     * skip class with designed dynamic lookup call
+     */
+    if (path.endsWith("gui/JvSwingUtils.java"))
     {
-      String readLine = br.readLine();
-      lines[i] = stripCommentsAndTrim(readLine);
+      return;
     }
 
-    int lineNo = 0;
+    BufferedReader br = new BufferedReader(new FileReader(f));
+    BufferedLineReader blr = new BufferedLineReader(br, bufferSize, this);
 
-    while (lines[bufferSize - 1] != null)
+    int lineNo = 0;
+    String line = blr.read();
+    while (line != 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());
+      inspectSourceLines(path, lineNo, line);
+      line = blr.read();
     }
     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
+   * lines that have been concatenated while parsing the file
    * 
    * @param path
    * @param lineNo
-   * @param lines
+   * @param line
    */
-  private void inspectSourceLines(String path, int lineNo, String[] lines)
+  private void inspectSourceLines(String path, int lineNo, String line)
   {
-    String lineNos = String.format("%d-%d", lineNo, lineNo + lines.length
+    String lineNos = String
+            .format("%d-%d", lineNo, lineNo + bufferSize
             - 1);
-    String combined = combineLines(lines);
     for (String method : METHODS)
     {
-      int pos = combined.indexOf(method);
+      int pos = line.indexOf(method);
       if (pos == -1)
       {
         continue;
       }
-      String methodArgs = combined.substring(pos + method.length());
+
+      /*
+       * extract what follows the opening bracket of the method call
+       */
+      String methodArgs = line.substring(pos + method.length()).trim();
       if ("".equals(methodArgs))
       {
         /*
-         * continues on next line - catch in the next read loop iteration
+         * arguments are on next line - catch in the next read loop iteration
          */
         continue;
       }
-      if (!methodArgs.startsWith("\""))
+      if (methodArgs.indexOf(",") == -1 && methodArgs.indexOf(")") == -1)
       {
-        System.out.println(String.format(
-                "Possible dynamic key at %s line %s %s",
-                path.substring(sourcePath.length()), lineNos, combined));
+        /*
+         * arguments continue on next line - catch in the next read loop iteration
+         */
+        continue;
+      }
+
+      if (JVINIT == method && methodArgs.indexOf(",") == -1)
+      {
+        /*
+         * not interested in 1-arg calls to jvInitComponent
+         */
+        continue;
+      }
+
+      String messageKey = getMessageKey(method, methodArgs);
+
+      if (METHOD3 == method)
+      {
+        System.out.println(String.format("Dynamic key at %s line %s %s",
+                path.substring(sourcePath.length()), lineNos, line));
+        String key = messageKey.substring(1, messageKey.length() - 1);
+        dynamicKeys.add(key);
         continue;
       }
-      methodArgs = methodArgs.substring(1);
-      int quotePos = methodArgs.indexOf("\"");
-      if (quotePos == -1)
+
+      if (messageKey == null)
       {
         System.out.println(String.format("Trouble parsing %s line %s %s",
-                path.substring(sourcePath.length()), lineNos, combined));
+                path.substring(sourcePath.length()), lineNos, line));
+        continue;
+      }
+
+      if (!(STRING_PATTERN.matcher(messageKey).matches()))
+      {
+        System.out.println(String.format("Dynamic key at %s line %s %s",
+                path.substring(sourcePath.length()), lineNos, line));
         continue;
       }
-      String messageKey = methodArgs.substring(0, quotePos);
+
+      /*
+       * strip leading and trailing quote
+       */
+      messageKey = messageKey.substring(1, messageKey.length() - 1);
+
       if (!this.messages.containsKey(messageKey))
       {
         System.out.println(String.format(
@@ -269,20 +368,45 @@ public class MessageBundleChecker
     }
   }
 
-  private String combineLines(String[] lines)
+  /**
+   * Helper method to parse out the resource bundle key parameter of a method
+   * call
+   * 
+   * @param method
+   * @param methodArgs
+   *          the rest of the source line starting with arguments to method
+   * @return
+   */
+  private String getMessageKey(String method, String methodArgs)
   {
-    String combined = "";
-    if (lines != null)
+    String key = methodArgs;
+
+    /*
+     * locate second argument if calling jvInitComponent()
+     */
+    if (method == JVINIT)
     {
-      for (String line : lines)
+      int commaLoc = methodArgs.indexOf(",");
+      if (commaLoc == -1)
       {
-        if (line != null)
-        {
-          combined += line;
-        }
+        return null;
       }
+      key = key.substring(commaLoc + 1).trim();
+    }
+
+    /*
+     * take up to next comma or ) or end of line
+     */
+    int commaPos = key.indexOf(",");
+    int bracePos = key.indexOf(")");
+    int endPos = commaPos == -1 ? bracePos : (bracePos == -1 ? commaPos
+            : Math.min(commaPos, bracePos));
+    if (endPos == -1 && key.length() > 1 && key.endsWith("\""))
+    {
+      endPos = key.length();
     }
-    return combined;
+
+    return endPos == -1 ? null : key.substring(0, endPos);
   }
 
   /**
@@ -298,7 +422,7 @@ public class MessageBundleChecker
     messages.load(reader);
     reader.close();
 
-    messageKeys = new TreeSet<String>();
+    messageKeys = new TreeSet<>();
     for (Object key : messages.keySet())
     {
       messageKeys.add((String) key);
@@ -306,4 +430,22 @@ public class MessageBundleChecker
 
   }
 
+  /**
+   * Remove any trailing comments, change tabs to space, and trim
+   */
+  @Override
+  public String cleanLine(String l)
+  {
+    if (l != null)
+    {
+      int pos = l.indexOf("//");
+      if (pos != -1)
+      {
+        l = l.substring(0, pos);
+      }
+      l = l.replace("\t", " ").trim();
+    }
+    return l;
+  }
+
 }