2 * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3 * Copyright (C) $$Year-Rel$$ The Jalview Authors
5 * This file is part of Jalview.
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.
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.
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.
21 import java.io.BufferedReader;
23 import java.io.FileReader;
24 import java.io.IOException;
25 import java.util.HashSet;
26 import java.util.Properties;
28 import java.util.TreeSet;
29 import java.util.regex.Pattern;
32 * This class scans Java source files for calls to MessageManager and reports
34 * <li>calls using keys not found in Messages.properties</li>
35 * <li>any unused keys in Messages.properties</li>
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
44 public class MessageBundleChecker implements BufferedLineReader.LineCleaner
48 * opening quote, closing quote, no quotes in between
50 static Pattern STRING_PATTERN = Pattern.compile("^\"[^\"]*\"$");
53 * number of text lines to read at a time in order to parse
54 * code that is split over several lines
56 static int bufferSize = 3;
59 * resource bundle key is arg0 for these methods
61 static final String METHOD1 = "MessageManager.getString(";
63 static final String METHOD2 = "MessageManager.formatMessage(";
65 static final String METHOD3 = "MessageManager.getStringOrReturn(";
68 * resource bundle key is arg1 for this method
70 static final String JVINIT = "JvSwingUtils.jvInitComponent(";
72 static final String[] METHODS = { METHOD1, METHOD2, METHOD3, JVINIT };
75 * root of the Java source folders we want to scan
80 * contents of Messages.properties
82 private Properties messages;
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
89 private TreeSet<String> messageKeys;
91 private int javaCount;
93 private Set<String> invalidKeys;
95 private Set<String> dynamicKeys;
98 * Runs the scan given the path to the root of Java source directories
101 * [0] path to the source folder to scan
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
108 public static void main(String[] args) throws IOException
110 if (args.length != 1 && args.length != 2)
112 System.out.println("Usage: <pathToSourceFolder> [readBufferSize]");
115 if (args.length == 2)
117 bufferSize = Integer.valueOf(args[1]);
119 new MessageBundleChecker().doMain(args[0]);
123 * Main method to perform the work
126 * @throws IOException
128 private void doMain(String srcPath) throws IOException
130 System.out.println("Scanning " + srcPath
131 + " for calls to MessageManager\n");
132 sourcePath = srcPath;
134 File dir = new File(srcPath);
137 System.out.println(srcPath + " not found");
141 invalidKeys = new HashSet<>();
142 dynamicKeys = new HashSet<>();
144 if (dir.isDirectory())
156 * Prints out counts to sysout
158 private void reportResults()
160 System.out.println("\nScanned " + javaCount + " source files");
162 "Messages.properties has " + messages.size()
164 if (!invalidKeys.isEmpty())
166 System.out.println("Found " + invalidKeys.size()
167 + " possibly invalid parameter call"
168 + (invalidKeys.size() > 1 ? "s" : ""));
171 System.out.println("Keys not found, assumed constructed dynamically:");
172 int dynamicCount = 0;
173 for (String key : messageKeys)
177 System.out.println(" " + key);
182 if (dynamicCount < messageKeys.size())
184 System.out.println((messageKeys.size() - dynamicCount)
185 + " keys not found, possibly unused");
186 for (String key : messageKeys)
190 System.out.println(" " + key);
195 .println("(Run i18nAnt.xml to compare other message bundles)");
199 * Answers true if the key starts with one of the recorded dynamic key stubs,
205 private boolean isDynamic(String key)
207 for (String dynamic : dynamicKeys)
209 if (key.startsWith(dynamic))
218 * Scan all files within a directory
221 * @throws IOException
223 private void scanDirectory(File dir) throws IOException
225 File[] files = dir.listFiles();
247 private void scanFile(File f) throws IOException
249 String path = f.getPath();
250 if (!path.endsWith(".java"))
257 * skip class with designed dynamic lookup call
259 if (path.endsWith("gui/JvSwingUtils.java"))
264 BufferedReader br = new BufferedReader(new FileReader(f));
265 BufferedLineReader blr = new BufferedLineReader(br, bufferSize, this);
268 String line = blr.read();
272 inspectSourceLines(path, lineNo, line);
280 * Look for calls to MessageManager methods, possibly split over two or more
281 * lines that have been concatenated while parsing the file
287 private void inspectSourceLines(String path, int lineNo, String line)
289 String lineNos = String
290 .format("%d-%d", lineNo, lineNo + bufferSize
292 for (String method : METHODS)
294 int pos = line.indexOf(method);
301 * extract what follows the opening bracket of the method call
303 String methodArgs = line.substring(pos + method.length()).trim();
304 if ("".equals(methodArgs))
307 * arguments are on next line - catch in the next read loop iteration
311 if (methodArgs.indexOf(",") == -1 && methodArgs.indexOf(")") == -1)
314 * arguments continue on next line - catch in the next read loop iteration
319 if (JVINIT == method && methodArgs.indexOf(",") == -1)
322 * not interested in 1-arg calls to jvInitComponent
327 String messageKey = getMessageKey(method, methodArgs);
329 if (METHOD3 == method)
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);
338 if (messageKey == null)
340 System.out.println(String.format("Trouble parsing %s line %s %s",
341 path.substring(sourcePath.length()), lineNos, line));
345 if (!(STRING_PATTERN.matcher(messageKey).matches()))
347 System.out.println(String.format("Dynamic key at %s line %s %s",
348 path.substring(sourcePath.length()), lineNos, line));
353 * strip leading and trailing quote
355 messageKey = messageKey.substring(1, messageKey.length() - 1);
357 if (!this.messages.containsKey(messageKey))
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))
364 invalidKeys.add(messageKey);
367 messageKeys.remove(messageKey);
372 * Helper method to parse out the resource bundle key parameter of a method
377 * the rest of the source line starting with arguments to method
380 private String getMessageKey(String method, String methodArgs)
382 String key = methodArgs;
385 * locate second argument if calling jvInitComponent()
387 if (method == JVINIT)
389 int commaLoc = methodArgs.indexOf(",");
394 key = key.substring(commaLoc + 1).trim();
398 * take up to next comma or ) or end of line
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("\""))
406 endPos = key.length();
409 return endPos == -1 ? null : key.substring(0, endPos);
413 * Loads properties from Message.properties
415 * @throws IOException
417 void loadMessages() throws IOException
419 messages = new Properties();
420 FileReader reader = new FileReader(new File(sourcePath,
421 "../resources/lang/Messages.properties"));
422 messages.load(reader);
425 messageKeys = new TreeSet<>();
426 for (Object key : messages.keySet())
428 messageKeys.add((String) key);
434 * Remove any trailing comments, change tabs to space, and trim
437 public String cleanLine(String l)
441 int pos = l.indexOf("//");
444 l = l.substring(0, pos);
446 l = l.replace("\t", " ").trim();