JAL-1551 formatting
[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 import java.util.regex.Pattern;
9
10 /**
11  * This class scans Java source files for calls to MessageManager and reports
12  * <ul>
13  * <li>calls using keys not found in Messages.properties</li>
14  * <li>any unused keys in Messages.properties</li>
15  * </ul>
16  * It does not handle dynamically constructed keys, these are reported as
17  * possible errors for manual inspection. <br>
18  * For comparing translated bundles with Messages.properties, see i18nAnt.xml
19  * 
20  * @author gmcarstairs
21  *
22  */
23 public class MessageBundleChecker
24 {
25   /*
26    * regex ^"[^"]*"$
27    * opening quote, closing quote, no quotes in between
28    */
29   static Pattern STRING_PATTERN = Pattern.compile("^\"[^\"]*\"$");
30
31   /*
32    * number of text lines to read at a time in order to parse
33    * code that is split over several lines
34    */
35   static int bufferSize = 3;
36
37   /*
38    * resource bundle key is arg0 for these methods
39    */
40   static final String METHOD1 = "MessageManager.getString(";
41
42   static final String METHOD2 = "MessageManager.formatMessage(";
43
44   static final String METHOD3 = "MessageManager.getStringOrReturn(";
45
46   /*
47    * resource bundle key is arg1 for this method
48    */
49   static final String JVINIT = "JvSwingUtils.jvInitComponent(";
50
51   static final String[] METHODS = { METHOD1, METHOD2, METHOD3, JVINIT };
52
53   /*
54    * root of the Java source folders we want to scan
55    */
56   String sourcePath;
57
58   /*
59    * contents of Messages.properties
60    */
61   private Properties messages;
62
63   /*
64    * keys from Messages.properties
65    * we remove entries from here as they are found to be used
66    * any left over are unused entries
67    */
68   private TreeSet<String> messageKeys;
69
70   private int javaCount;
71
72   private HashSet<String> invalidKeys;
73
74   /**
75    * Runs the scan given the path to the root of Java source directories
76    * 
77    * @param args
78    *          [0] path to the source folder to scan
79    * @param args
80    *          [1] (optional) read buffer size (default is 3); increasing this
81    *          may detect more results but will give higher error counts due to
82    *          double counting of the same code
83    * @throws IOException
84    */
85   public static void main(String[] args) throws IOException
86   {
87     if (args.length != 1 && args.length != 2)
88     {
89       System.out.println("Usage: <pathToSourceFolder> [readBufferSize]");
90       return;
91     }
92     if (args.length == 2)
93     {
94       bufferSize = Integer.valueOf(args[1]);
95     }
96     new MessageBundleChecker().doMain(args[0]);
97   }
98
99   /**
100    * Main method to perform the work
101    * 
102    * @param srcPath
103    * @throws IOException
104    */
105   private void doMain(String srcPath) throws IOException
106   {
107     System.out.println("Scanning " + srcPath
108             + " for calls to MessageManager");
109     sourcePath = srcPath;
110     loadMessages();
111     File dir = new File(srcPath);
112     if (!dir.exists())
113     {
114       System.out.println(srcPath + " not found");
115       return;
116     }
117     invalidKeys = new HashSet<String>();
118     if (dir.isDirectory())
119     {
120       scanDirectory(dir);
121     }
122     else
123     {
124       scanFile(dir);
125     }
126     reportResults();
127   }
128
129   /**
130    * Prints out counts to sysout
131    */
132   private void reportResults()
133   {
134     System.out.println("\nScanned " + javaCount + " source files");
135     System.out.println("Message.properties has " + messages.size()
136             + " keys");
137     System.out.println("Found " + invalidKeys.size()
138             + " possibly invalid parameter calls");
139
140     System.out.println(messageKeys.size()
141             + " keys not found, either unused or constructed dynamically");
142     for (String key : messageKeys)
143     {
144       System.out.println("    " + key);
145     }
146   }
147
148   /**
149    * Scan all files within a directory
150    * 
151    * @param dir
152    * @throws IOException
153    */
154   private void scanDirectory(File dir) throws IOException
155   {
156     File[] files = dir.listFiles();
157     if (files != null)
158     {
159       for (File f : files)
160       {
161         if (f.isDirectory())
162         {
163           scanDirectory(f);
164         }
165         else
166         {
167           scanFile(f);
168         }
169       }
170     }
171   }
172
173   /**
174    * Scan a Java file
175    * 
176    * @param f
177    */
178   private void scanFile(File f) throws IOException
179   {
180     String path = f.getPath();
181     if (!path.endsWith(".java"))
182     {
183       return;
184     }
185     javaCount++;
186
187     /*
188      * skip class with designed dynamic lookup call
189      */
190     if (path.endsWith("gui/JvSwingUtils.java"))
191     {
192       return;
193     }
194
195     String[] lines = new String[bufferSize];
196     BufferedReader br = new BufferedReader(new FileReader(f));
197     for (int i = 0; i < bufferSize; i++)
198     {
199       String readLine = br.readLine();
200       lines[i] = stripCommentsAndTrim(readLine);
201     }
202
203     int lineNo = 0;
204
205     while (lines[bufferSize - 1] != null)
206     {
207       lineNo++;
208       inspectSourceLines(path, lineNo, lines);
209
210       for (int i = 0; i < bufferSize - 1; i++)
211       {
212         lines[i] = lines[i + 1];
213       }
214       lines[bufferSize - 1] = stripCommentsAndTrim(br.readLine());
215     }
216     br.close();
217
218   }
219
220   /*
221    * removes anything after (and including) '//'
222    */
223   private String stripCommentsAndTrim(String line)
224   {
225     if (line != null)
226     {
227       int pos = line.indexOf("//");
228       if (pos != -1)
229       {
230         line = line.substring(0, pos);
231       }
232       line = line.replace("\t", " ").trim();
233     }
234     return line;
235   }
236
237   /**
238    * Look for calls to MessageManager methods, possibly split over two or more
239    * lines
240    * 
241    * @param path
242    * @param lineNo
243    * @param lines
244    */
245   private void inspectSourceLines(String path, int lineNo, String[] lines)
246   {
247     String lineNos = String.format("%d-%d", lineNo, lineNo + lines.length
248             - 1);
249     String combined = combineLines(lines);
250     for (String method : METHODS)
251     {
252       int pos = combined.indexOf(method);
253       if (pos == -1)
254       {
255         continue;
256       }
257
258       /*
259        * extract what follows the opening bracket of the method call
260        */
261       String methodArgs = combined.substring(pos + method.length()).trim();
262       if ("".equals(methodArgs))
263       {
264         /*
265          * arguments are on next line - catch in the next read loop iteration
266          */
267         continue;
268       }
269       if (methodArgs.indexOf(",") == -1 && methodArgs.indexOf(")") == -1)
270       {
271         /*
272          * arguments continue on next line - catch in the next read loop iteration
273          */
274         continue;
275       }
276
277       if (JVINIT == method && methodArgs.indexOf(",") == -1)
278       {
279         /*
280          * not interested in 1-arg calls to jvInitComponent
281          */
282         continue;
283       }
284
285       if (METHOD3 == method)
286       {
287         System.out.println(String.format("Dynamic key at %s line %s %s",
288                 path.substring(sourcePath.length()), lineNos, combined));
289         continue;
290       }
291
292       String messageKey = getMessageKey(method, methodArgs);
293       if (messageKey == null)
294       {
295         System.out.println(String.format("Trouble parsing %s line %s %s",
296                 path.substring(sourcePath.length()), lineNos, combined));
297         continue;
298       }
299
300       if (!(STRING_PATTERN.matcher(messageKey).matches()))
301       {
302         System.out.println(String.format("Dynamic key at %s line %s %s",
303                 path.substring(sourcePath.length()), lineNos, combined));
304         continue;
305       }
306
307       /*
308        * strip leading and trailing quote
309        */
310       messageKey = messageKey.substring(1, messageKey.length() - 1);
311
312       if (!this.messages.containsKey(messageKey))
313       {
314         System.out.println(String.format(
315                 "Unmatched key '%s' at line %s of %s", messageKey, lineNos,
316                 path.substring(sourcePath.length())));
317         if (!invalidKeys.contains(messageKey))
318         {
319           invalidKeys.add(messageKey);
320         }
321       }
322       messageKeys.remove(messageKey);
323     }
324   }
325
326   /**
327    * Helper method to parse out the resource bundle key parameter of a method
328    * call
329    * 
330    * @param method
331    * @param methodArgs
332    *          the rest of the source line starting with arguments to method
333    * @return
334    */
335   private String getMessageKey(String method, String methodArgs)
336   {
337     String key = methodArgs;
338
339     /*
340      * locate second argument if calling jvInitComponent()
341      */
342     if (method == JVINIT)
343     {
344       int commaLoc = methodArgs.indexOf(",");
345       if (commaLoc == -1)
346       {
347         return null;
348       }
349       key = key.substring(commaLoc + 1).trim();
350     }
351
352     /*
353      * take up to next comma or ) or end of line
354      */
355     int commaPos = key.indexOf(",");
356     int bracePos = key.indexOf(")");
357     int endPos = commaPos == -1 ? bracePos : (bracePos == -1 ? commaPos
358             : Math.min(commaPos, bracePos));
359     if (endPos == -1 && key.length() > 1 && key.endsWith("\""))
360     {
361       endPos = key.length();
362     }
363
364     return endPos == -1 ? null : key.substring(0, endPos);
365   }
366
367   private String combineLines(String[] lines)
368   {
369     String combined = "";
370     if (lines != null)
371     {
372       for (String line : lines)
373       {
374         if (line != null)
375         {
376           combined += line;
377         }
378       }
379     }
380     return combined;
381   }
382
383   /**
384    * Loads properties from Message.properties
385    * 
386    * @throws IOException
387    */
388   void loadMessages() throws IOException
389   {
390     messages = new Properties();
391     FileReader reader = new FileReader(new File(sourcePath,
392             "../resources/lang/Messages.properties"));
393     messages.load(reader);
394     reader.close();
395
396     messageKeys = new TreeSet<String>();
397     for (Object key : messages.keySet())
398     {
399       messageKeys.add((String) key);
400     }
401
402   }
403
404 }