JAL-1424 utility to check resource bundle labels used in code (and some
[jalview.git] / utils / MessageBundleChecker.java
1 import java.io.BufferedReader;
2 import java.io.File;
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
9 /**
10  * This class scans Java source files for calls to MessageManager and reports
11  * <ul>
12  * <li>calls using keys not found in Messages.properties</li>
13  * <li>any unused keys in Messages.properties</li>
14  * </ul>
15  * It does not handle dynamically constructed keys, these are reported as
16  * possible errors for manual inspection. <br>
17  * For comparing translated bundles with Messages.properties, see i18nAnt.xml
18  * 
19  * @author gmcarstairs
20  *
21  */
22 public class MessageBundleChecker
23 {
24   /*
25    * number of text lines to read at a time in order to parse
26    * code that is split over several lines
27    */
28   static int bufferSize = 3;
29
30   static final String METHOD1 = "MessageManager.getString(";
31
32   static final String METHOD2 = "MessageManager.getStringOrReturn(";
33
34   static final String METHOD3 = "MessageManager.formatMessage(";
35
36   static final String[] METHODS = { METHOD1, METHOD2, METHOD3 };
37
38   /*
39    * root of the Java source folders we want to scan
40    */
41   String sourcePath;
42
43   /*
44    * contents of Messages.properties
45    */
46   private Properties messages;
47
48   /*
49    * keys from Messages.properties
50    * we remove entries from here as they are found to be used
51    * any left over are unused entries
52    */
53   private TreeSet<String> messageKeys;
54
55   private int javaCount;
56
57   private HashSet<String> invalidKeys;
58
59   /**
60    * Runs the scan given the path to the root of Java source directories
61    * 
62    * @param args
63    *          [0] path to the source folder to scan
64    * @param args
65    *          [1] (optional) read buffer size (default is 3); increasing this
66    *          may detect more results but will give higher error counts due to
67    *          double counting of the same code
68    * @throws IOException
69    */
70   public static void main(String[] args) throws IOException
71   {
72     if (args.length != 1 && args.length != 2)
73     {
74       System.out.println("Usage: <pathToSourceFolder> [readBufferSize]");
75       return;
76     }
77     if (args.length == 2)
78     {
79       bufferSize = Integer.valueOf(args[1]);
80     }
81     new MessageBundleChecker().doMain(args[0]);
82   }
83
84   /**
85    * Main method to perform the work
86    * 
87    * @param srcPath
88    * @throws IOException
89    */
90   private void doMain(String srcPath) throws IOException
91   {
92     System.out.println("Scanning " + srcPath
93             + " for calls to MessageManager");
94     sourcePath = srcPath;
95     loadMessages();
96     File dir = new File(srcPath);
97     if (!dir.exists())
98     {
99       System.out.println(srcPath + " not found");
100       return;
101     }
102     invalidKeys = new HashSet<String>();
103     if (dir.isDirectory())
104     {
105       scanDirectory(dir);
106     }
107     else
108     {
109       scanFile(dir);
110     }
111     reportResults();
112   }
113
114   /**
115    * Prints out counts to sysout
116    */
117   private void reportResults()
118   {
119     System.out.println("\nScanned " + javaCount + " source files");
120     System.out.println("Message.properties has " + messages.size()
121             + " keys");
122     System.out.println("Found " + invalidKeys.size()
123             + " possibly invalid parameter calls");
124
125     System.out.println(messageKeys.size()
126             + " keys not found, possibly unused");
127     for (String key : messageKeys)
128     {
129       System.out.println("    " + key);
130     }
131   }
132
133   /**
134    * Scan all files within a directory
135    * 
136    * @param dir
137    * @throws IOException
138    */
139   private void scanDirectory(File dir) throws IOException
140   {
141     File[] files = dir.listFiles();
142     if (files != null)
143     {
144       for (File f : files)
145       {
146         if (f.isDirectory())
147         {
148           scanDirectory(f);
149         }
150         else
151         {
152           scanFile(f);
153         }
154       }
155     }
156   }
157
158   /**
159    * Scan a Java file
160    * 
161    * @param f
162    */
163   private void scanFile(File f) throws IOException
164   {
165     String path = f.getPath();
166     if (!path.endsWith(".java"))
167     {
168       return;
169     }
170     javaCount++;
171
172     String[] lines = new String[bufferSize];
173     BufferedReader br = new BufferedReader(new FileReader(f));
174     for (int i = 0; i < bufferSize; i++)
175     {
176       String readLine = br.readLine();
177       lines[i] = stripCommentsAndTrim(readLine);
178     }
179
180     int lineNo = 0;
181
182     while (lines[bufferSize - 1] != null)
183     {
184       lineNo++;
185       inspectSourceLines(path, lineNo, lines);
186
187       for (int i = 0; i < bufferSize - 1; i++)
188       {
189         lines[i] = lines[i + 1];
190       }
191       lines[bufferSize - 1] = stripCommentsAndTrim(br.readLine());
192     }
193     br.close();
194
195   }
196
197   /*
198    * removes anything after (and including) '//'
199    */
200   private String stripCommentsAndTrim(String line)
201   {
202     if (line != null)
203     {
204       int pos = line.indexOf("//");
205       if (pos != -1)
206       {
207         line = line.substring(0, pos);
208       }
209       line = line.replace("\t", " ").trim();
210     }
211     return line;
212   }
213
214   /**
215    * Look for calls to MessageManager methods, possibly split over two or more
216    * lines
217    * 
218    * @param path
219    * @param lineNo
220    * @param lines
221    */
222   private void inspectSourceLines(String path, int lineNo, String[] lines)
223   {
224     String lineNos = String.format("%d-%d", lineNo, lineNo + lines.length
225             - 1);
226     String combined = combineLines(lines);
227     for (String method : METHODS)
228     {
229       int pos = combined.indexOf(method);
230       if (pos == -1)
231       {
232         continue;
233       }
234       String methodArgs = combined.substring(pos + method.length());
235       if ("".equals(methodArgs))
236       {
237         /*
238          * continues on next line - catch in the next read loop iteration
239          */
240         continue;
241       }
242       if (!methodArgs.startsWith("\""))
243       {
244         System.out.println(String.format("Trouble parsing %s line %s %s",
245                 path.substring(sourcePath.length()), lineNos, combined));
246         continue;
247       }
248       methodArgs = methodArgs.substring(1);
249       int quotePos = methodArgs.indexOf("\"");
250       if (quotePos == -1)
251       {
252         System.out.println(String.format("Trouble parsing %s line %s %s",
253                 path.substring(sourcePath.length()), lineNos, combined));
254         continue;
255       }
256       String messageKey = methodArgs.substring(0, quotePos);
257       if (!this.messages.containsKey(messageKey))
258       {
259         System.out.println(String.format(
260                 "Unmatched key '%s' at line %s of %s", messageKey, lineNos,
261                 path.substring(sourcePath.length())));
262         if (!invalidKeys.contains(messageKey))
263         {
264           invalidKeys.add(messageKey);
265         }
266       }
267       messageKeys.remove(messageKey);
268     }
269   }
270
271   private String combineLines(String[] lines)
272   {
273     String combined = "";
274     if (lines != null)
275     {
276       for (String line : lines)
277       {
278         if (line != null)
279         {
280           combined += line;
281         }
282       }
283     }
284     return combined;
285   }
286
287   /**
288    * Loads properties from Message.properties
289    * 
290    * @throws IOException
291    */
292   void loadMessages() throws IOException
293   {
294     messages = new Properties();
295     FileReader reader = new FileReader(new File(sourcePath,
296             "../resources/lang/Messages.properties"));
297     messages.load(reader);
298     reader.close();
299
300     messageKeys = new TreeSet<String>();
301     for (Object key : messages.keySet())
302     {
303       messageKeys.add((String) key);
304     }
305
306   }
307
308 }