JAL-2189 formatting and GPL
[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
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     String[] lines = new String[bufferSize];
216     BufferedReader br = new BufferedReader(new FileReader(f));
217     for (int i = 0; i < bufferSize; i++)
218     {
219       String readLine = br.readLine();
220       lines[i] = stripCommentsAndTrim(readLine);
221     }
222
223     int lineNo = 0;
224
225     while (lines[bufferSize - 1] != null)
226     {
227       lineNo++;
228       inspectSourceLines(path, lineNo, lines);
229
230       for (int i = 0; i < bufferSize - 1; i++)
231       {
232         lines[i] = lines[i + 1];
233       }
234       lines[bufferSize - 1] = stripCommentsAndTrim(br.readLine());
235     }
236     br.close();
237
238   }
239
240   /*
241    * removes anything after (and including) '//'
242    */
243   private String stripCommentsAndTrim(String line)
244   {
245     if (line != null)
246     {
247       int pos = line.indexOf("//");
248       if (pos != -1)
249       {
250         line = line.substring(0, pos);
251       }
252       line = line.replace("\t", " ").trim();
253     }
254     return line;
255   }
256
257   /**
258    * Look for calls to MessageManager methods, possibly split over two or more
259    * lines
260    * 
261    * @param path
262    * @param lineNo
263    * @param lines
264    */
265   private void inspectSourceLines(String path, int lineNo, String[] lines)
266   {
267     String lineNos = String.format("%d-%d", lineNo, lineNo + lines.length
268             - 1);
269     String combined = combineLines(lines);
270     for (String method : METHODS)
271     {
272       int pos = combined.indexOf(method);
273       if (pos == -1)
274       {
275         continue;
276       }
277
278       /*
279        * extract what follows the opening bracket of the method call
280        */
281       String methodArgs = combined.substring(pos + method.length()).trim();
282       if ("".equals(methodArgs))
283       {
284         /*
285          * arguments are on next line - catch in the next read loop iteration
286          */
287         continue;
288       }
289       if (methodArgs.indexOf(",") == -1 && methodArgs.indexOf(")") == -1)
290       {
291         /*
292          * arguments continue on next line - catch in the next read loop iteration
293          */
294         continue;
295       }
296
297       if (JVINIT == method && methodArgs.indexOf(",") == -1)
298       {
299         /*
300          * not interested in 1-arg calls to jvInitComponent
301          */
302         continue;
303       }
304
305       if (METHOD3 == method)
306       {
307         System.out.println(String.format("Dynamic key at %s line %s %s",
308                 path.substring(sourcePath.length()), lineNos, combined));
309         continue;
310       }
311
312       String messageKey = getMessageKey(method, methodArgs);
313       if (messageKey == null)
314       {
315         System.out.println(String.format("Trouble parsing %s line %s %s",
316                 path.substring(sourcePath.length()), lineNos, combined));
317         continue;
318       }
319
320       if (!(STRING_PATTERN.matcher(messageKey).matches()))
321       {
322         System.out.println(String.format("Dynamic key at %s line %s %s",
323                 path.substring(sourcePath.length()), lineNos, combined));
324         continue;
325       }
326
327       /*
328        * strip leading and trailing quote
329        */
330       messageKey = messageKey.substring(1, messageKey.length() - 1);
331
332       if (!this.messages.containsKey(messageKey))
333       {
334         System.out.println(String.format(
335                 "Unmatched key '%s' at line %s of %s", messageKey, lineNos,
336                 path.substring(sourcePath.length())));
337         if (!invalidKeys.contains(messageKey))
338         {
339           invalidKeys.add(messageKey);
340         }
341       }
342       messageKeys.remove(messageKey);
343     }
344   }
345
346   /**
347    * Helper method to parse out the resource bundle key parameter of a method
348    * call
349    * 
350    * @param method
351    * @param methodArgs
352    *          the rest of the source line starting with arguments to method
353    * @return
354    */
355   private String getMessageKey(String method, String methodArgs)
356   {
357     String key = methodArgs;
358
359     /*
360      * locate second argument if calling jvInitComponent()
361      */
362     if (method == JVINIT)
363     {
364       int commaLoc = methodArgs.indexOf(",");
365       if (commaLoc == -1)
366       {
367         return null;
368       }
369       key = key.substring(commaLoc + 1).trim();
370     }
371
372     /*
373      * take up to next comma or ) or end of line
374      */
375     int commaPos = key.indexOf(",");
376     int bracePos = key.indexOf(")");
377     int endPos = commaPos == -1 ? bracePos : (bracePos == -1 ? commaPos
378             : Math.min(commaPos, bracePos));
379     if (endPos == -1 && key.length() > 1 && key.endsWith("\""))
380     {
381       endPos = key.length();
382     }
383
384     return endPos == -1 ? null : key.substring(0, endPos);
385   }
386
387   private String combineLines(String[] lines)
388   {
389     String combined = "";
390     if (lines != null)
391     {
392       for (String line : lines)
393       {
394         if (line != null)
395         {
396           combined += line;
397         }
398       }
399     }
400     return combined;
401   }
402
403   /**
404    * Loads properties from Message.properties
405    * 
406    * @throws IOException
407    */
408   void loadMessages() throws IOException
409   {
410     messages = new Properties();
411     FileReader reader = new FileReader(new File(sourcePath,
412             "../resources/lang/Messages.properties"));
413     messages.load(reader);
414     reader.close();
415
416     messageKeys = new TreeSet<String>();
417     for (Object key : messages.keySet())
418     {
419       messageKeys.add((String) key);
420     }
421
422   }
423
424 }