JAL-1424 slight tidying of reporting
[jalview.git] / utils / MessageBundleChecker.java
1
2 /*
3  * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
4  * Copyright (C) $$Year-Rel$$ The Jalview Authors
5  * 
6  * This file is part of Jalview.
7  * 
8  * Jalview is free software: you can redistribute it and/or
9  * modify it under the terms of the GNU General Public License 
10  * as published by the Free Software Foundation, either version 3
11  * of the License, or (at your option) any later version.
12  *  
13  * Jalview is distributed in the hope that it will be useful, but 
14  * WITHOUT ANY WARRANTY; without even the implied warranty 
15  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
16  * PURPOSE.  See the GNU General Public License for more details.
17  * 
18  * You should have received a copy of the GNU General Public License
19  * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
20  * The Jalview Authors are detailed in the 'AUTHORS' file.
21  */
22 import java.io.BufferedReader;
23 import java.io.File;
24 import java.io.FileReader;
25 import java.io.IOException;
26 import java.util.HashSet;
27 import java.util.Properties;
28 import java.util.Set;
29 import java.util.TreeSet;
30 import java.util.regex.Pattern;
31
32 /**
33  * This class scans Java source files for calls to MessageManager and reports
34  * <ul>
35  * <li>calls using keys not found in Messages.properties</li>
36  * <li>any unused keys in Messages.properties</li>
37  * </ul>
38  * It does not handle dynamically constructed keys, these are reported as
39  * possible errors for manual inspection. <br>
40  * For comparing translated bundles with Messages.properties, see i18nAnt.xml
41  * 
42  * @author gmcarstairs
43  *
44  */
45 public class MessageBundleChecker implements BufferedLineReader.LineCleaner
46 {
47   /*
48    * regex ^"[^"]*"$
49    * opening quote, closing quote, no quotes in between
50    */
51   static Pattern STRING_PATTERN = Pattern.compile("^\"[^\"]*\"$");
52
53   /*
54    * number of text lines to read at a time in order to parse
55    * code that is split over several lines
56    */
57   static int bufferSize = 3;
58
59   /*
60    * resource bundle key is arg0 for these methods
61    */
62   static final String METHOD1 = "MessageManager.getString(";
63
64   static final String METHOD2 = "MessageManager.formatMessage(";
65
66   static final String METHOD3 = "MessageManager.getStringOrReturn(";
67
68   /*
69    * resource bundle key is arg1 for this method
70    */
71   static final String JVINIT = "JvSwingUtils.jvInitComponent(";
72
73   static final String[] METHODS = { METHOD1, METHOD2, METHOD3, JVINIT };
74
75   /*
76    * root of the Java source folders we want to scan
77    */
78   String sourcePath;
79
80   /*
81    * contents of Messages.properties
82    */
83   private Properties messages;
84
85   /*
86    * keys from Messages.properties
87    * we remove entries from here as they are found to be used
88    * any left over are unused entries
89    */
90   private TreeSet<String> messageKeys;
91
92   private int javaCount;
93
94   private Set<String> invalidKeys;
95
96   private Set<String> dynamicKeys;
97
98   /**
99    * Runs the scan given the path to the root of Java source directories
100    * 
101    * @param args
102    *          [0] path to the source folder to scan
103    * @param args
104    *          [1] (optional) read buffer size (default is 3); increasing this
105    *          may detect more results but will give higher error counts due to
106    *          double counting of the same code
107    * @throws IOException
108    */
109   public static void main(String[] args) throws IOException
110   {
111     if (args.length != 1 && args.length != 2)
112     {
113       System.out.println("Usage: <pathToSourceFolder> [readBufferSize]");
114       return;
115     }
116     if (args.length == 2)
117     {
118       bufferSize = Integer.valueOf(args[1]);
119     }
120     new MessageBundleChecker().doMain(args[0]);
121   }
122
123   /**
124    * Main method to perform the work
125    * 
126    * @param srcPath
127    * @throws IOException
128    */
129   private void doMain(String srcPath) throws IOException
130   {
131     System.out.println(
132             "Scanning " + srcPath + " for calls to MessageManager\n");
133     System.out.println("Please note this is not a perfect check:");
134     System.out.println(
135             "- message keys constructed dynamically can't be checked");
136     System.out
137             .println("- message keys passed as variables can't be checked");
138     System.out.println(
139             "- references in commented out code are not filtered out");
140     System.out.println(
141             "Verify 'missing' keys manually by a source code search\n");
142
143     sourcePath = srcPath;
144     loadMessages();
145     File dir = new File(srcPath);
146     if (!dir.exists())
147     {
148       System.out.println(srcPath + " not found");
149       return;
150     }
151
152     invalidKeys = new HashSet<>();
153     dynamicKeys = new HashSet<>();
154
155     if (dir.isDirectory())
156     {
157       scanDirectory(dir);
158     }
159     else
160     {
161       scanFile(dir);
162     }
163     reportResults();
164   }
165
166   /**
167    * Prints out counts to sysout
168    */
169   private void reportResults()
170   {
171     System.out.println("\nScanned " + javaCount + " source files");
172     System.out.println(
173             "Messages.properties has " + messages.size() + " keys");
174     if (!invalidKeys.isEmpty())
175     {
176       System.out.println("Found " + invalidKeys.size()
177               + " possibly invalid unmatched keys in source code"
178               + (invalidKeys.size() > 1 ? "s" : ""));
179     }
180
181     System.out.println(
182             "Keys not found in source code, assumed constructed dynamically:");
183     int dynamicCount = 0;
184     for (String key : messageKeys)
185     {
186       if (isDynamic(key))
187       {
188         System.out.println("    " + key);
189         dynamicCount++;
190       }
191     }
192
193     if (dynamicCount < messageKeys.size())
194     {
195       System.out.println((messageKeys.size() - dynamicCount)
196               + " keys not found, possibly unused, or used indirectly (check code manually!)");
197       for (String key : messageKeys)
198       {
199         if (!isDynamic(key))
200         {
201           System.out.println("    " + key);
202         }
203       }
204     }
205     System.out
206             .println("(Run i18nAnt.xml to compare other message bundles)");
207   }
208
209   /**
210    * Answers true if the key starts with one of the recorded dynamic key stubs,
211    * else false
212    * 
213    * @param key
214    * @return
215    */
216   private boolean isDynamic(String key)
217   {
218     for (String dynamic : dynamicKeys)
219     {
220       if (key.startsWith(dynamic))
221       {
222         return true;
223       }
224     }
225     return false;
226   }
227
228   /**
229    * Scan all files within a directory
230    * 
231    * @param dir
232    * @throws IOException
233    */
234   private void scanDirectory(File dir) throws IOException
235   {
236     File[] files = dir.listFiles();
237     if (files != null)
238     {
239       for (File f : files)
240       {
241         if (f.isDirectory())
242         {
243           scanDirectory(f);
244         }
245         else
246         {
247           scanFile(f);
248         }
249       }
250     }
251   }
252
253   /**
254    * Scan a Java file
255    * 
256    * @param f
257    */
258   private void scanFile(File f) throws IOException
259   {
260     String path = f.getPath();
261     if (!path.endsWith(".java"))
262     {
263       return;
264     }
265     javaCount++;
266
267     /*
268      * skip class with designed dynamic lookup call
269      */
270     if (path.endsWith("gui/JvSwingUtils.java"))
271     {
272       return;
273     }
274
275     BufferedReader br = new BufferedReader(new FileReader(f));
276     BufferedLineReader blr = new BufferedLineReader(br, bufferSize, this);
277
278     int lineNo = 0;
279     String line = blr.read();
280     while (line != null)
281     {
282       lineNo++;
283       inspectSourceLines(path, lineNo, line);
284       line = blr.read();
285     }
286     br.close();
287
288   }
289
290   /**
291    * Look for calls to MessageManager methods, possibly split over two or more
292    * lines that have been concatenated while parsing the file
293    * 
294    * @param path
295    * @param lineNo
296    * @param line
297    */
298   private void inspectSourceLines(String path, int lineNo, String line)
299   {
300     String lineNos = String.format("%d-%d", lineNo,
301             lineNo + bufferSize - 1);
302     for (String method : METHODS)
303     {
304       int pos = line.indexOf(method);
305       if (pos == -1)
306       {
307         continue;
308       }
309
310       /*
311        * extract what follows the opening bracket of the method call
312        */
313       String methodArgs = line.substring(pos + method.length()).trim();
314       if ("".equals(methodArgs))
315       {
316         /*
317          * arguments are on next line - catch in the next read loop iteration
318          */
319         continue;
320       }
321       if (methodArgs.indexOf(",") == -1 && methodArgs.indexOf(")") == -1)
322       {
323         /*
324          * arguments continue on next line - catch in the next read loop iteration
325          */
326         continue;
327       }
328
329       if (JVINIT == method && methodArgs.indexOf(",") == -1)
330       {
331         /*
332          * not interested in 1-arg calls to jvInitComponent
333          */
334         continue;
335       }
336
337       String messageKey = getMessageKey(method, methodArgs);
338
339       if (METHOD3 == method)
340       {
341         String key = messageKey.substring(1, messageKey.length() - 1);
342         if (!dynamicKeys.contains(key))
343         {
344           System.out.println(String.format(
345                   "Dynamic key \"" + key + "\" at %s line %s %s",
346                   path.substring(sourcePath.length()), lineNos, line));
347           dynamicKeys.add(key);
348         }
349         continue;
350       }
351
352       if (messageKey == null)
353       {
354         System.out.println(String.format("Trouble parsing %s line %s %s",
355                 path.substring(sourcePath.length()), lineNos, line));
356         continue;
357       }
358
359       if (!(STRING_PATTERN.matcher(messageKey).matches()))
360       {
361         System.out.println(String.format("Dynamic key at %s line %s %s",
362                 path.substring(sourcePath.length()), lineNos, line));
363         continue;
364       }
365
366       /*
367        * strip leading and trailing quote
368        */
369       messageKey = messageKey.substring(1, messageKey.length() - 1);
370
371       if (!this.messages.containsKey(messageKey))
372       {
373         if (!invalidKeys.contains(messageKey))
374         {
375           // report each key the first time found only
376           System.out.println(String.format(
377                   "Unmatched key '%s' at line %s of %s", messageKey,
378                   lineNos, path.substring(sourcePath.length())));
379           invalidKeys.add(messageKey);
380         }
381       }
382       messageKeys.remove(messageKey);
383     }
384   }
385
386   /**
387    * Helper method to parse out the resource bundle key parameter of a method
388    * call
389    * 
390    * @param method
391    * @param methodArgs
392    *          the rest of the source line starting with arguments to method
393    * @return
394    */
395   private String getMessageKey(String method, String methodArgs)
396   {
397     String key = methodArgs;
398
399     /*
400      * locate second argument if calling jvInitComponent()
401      */
402     if (method == JVINIT)
403     {
404       int commaLoc = methodArgs.indexOf(",");
405       if (commaLoc == -1)
406       {
407         return null;
408       }
409       key = key.substring(commaLoc + 1).trim();
410     }
411
412     /*
413      * take up to next comma or ) or end of line
414      */
415     int commaPos = key.indexOf(",");
416     int bracePos = key.indexOf(")");
417     int endPos = commaPos == -1 ? bracePos
418             : (bracePos == -1 ? commaPos : Math.min(commaPos, bracePos));
419     if (endPos == -1 && key.length() > 1 && key.endsWith("\""))
420     {
421       endPos = key.length();
422     }
423
424     return endPos == -1 ? null : key.substring(0, endPos);
425   }
426
427   /**
428    * Loads properties from Message.properties
429    * 
430    * @throws IOException
431    */
432   void loadMessages() throws IOException
433   {
434     messages = new Properties();
435     FileReader reader = new FileReader(
436             new File(sourcePath, "../resources/lang/Messages.properties"));
437     messages.load(reader);
438     reader.close();
439
440     messageKeys = new TreeSet<>();
441     for (Object key : messages.keySet())
442     {
443       messageKeys.add((String) key);
444     }
445
446   }
447
448   /**
449    * Remove any trailing comments, change tabs to space, and trim
450    */
451   @Override
452   public String cleanLine(String l)
453   {
454     if (l != null)
455     {
456       int pos = l.indexOf("//");
457       if (pos != -1)
458       {
459         l = l.substring(0, pos);
460       }
461       l = l.replace("\t", " ").trim();
462     }
463     return l;
464   }
465
466 }