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