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