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