3 * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
4 * Copyright (C) $$Year-Rel$$ The Jalview Authors
6 * This file is part of Jalview.
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.
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.
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.
22 import java.io.BufferedReader;
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;
30 import java.util.TreeSet;
31 import java.util.regex.Pattern;
34 * This class scans Java source files for calls to MessageManager and reports
36 * <li>calls using keys not found in Messages.properties</li>
37 * <li>any unused keys in Messages.properties</li>
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
46 public class MessageBundleChecker implements BufferedLineReader.LineCleaner
50 * opening quote, closing quote, no quotes in between
52 static Pattern STRING_PATTERN = Pattern.compile("^\"[^\"]*\"$");
55 * number of text lines to read at a time in order to parse
56 * code that is split over several lines
58 static int bufferSize = 3;
61 * resource bundle key is arg0 for these methods
63 static final String METHOD1 = "MessageManager.getString(";
65 static final String METHOD2 = "MessageManager.formatMessage(";
67 static final String METHOD3 = "MessageManager.getStringOrReturn(";
70 * resource bundle key is arg1 for this method
72 static final String JVINIT = "JvSwingUtils.jvInitComponent(";
74 static final String[] METHODS = { METHOD1, METHOD2, METHOD3, JVINIT };
77 * root of the Java source folders we want to scan
82 * contents of Messages.properties
84 private Properties messages;
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
91 private TreeSet<String> messageKeys;
93 private int javaCount;
95 private Set<String> invalidKeys;
97 private Set<String> dynamicKeys;
100 * Runs the scan given the path to the root of Java source directories
103 * [0] path to the source folder to scan
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
110 public static void main(String[] args) throws IOException
112 if (args.length != 1 && args.length != 2)
114 System.out.println("Usage: <pathToSourceFolder> [readBufferSize]");
117 if (args.length == 2)
119 bufferSize = Integer.valueOf(args[1]);
121 new MessageBundleChecker().doMain(args[0]);
125 * Main method to perform the work
128 * @throws IOException
130 private void doMain(String srcPath) throws IOException
133 "Scanning " + srcPath + " for calls to MessageManager\n");
135 sourcePath = srcPath;
137 File dir = new File(srcPath);
140 System.out.println(srcPath + " not found");
144 invalidKeys = new HashSet<>();
145 dynamicKeys = new HashSet<>();
147 if (dir.isDirectory())
159 * Prints out counts to sysout
161 private void reportResults()
164 "\nMessages.properties has " + messages.size() + " keys");
165 System.out.println("Scanned " + javaCount + " source files\n");
167 if (!invalidKeys.isEmpty())
169 System.out.println("Found " + invalidKeys.size()
170 + " possibly invalid unmatched key(s) in source code"
171 + (invalidKeys.size() > 1 ? "s" : ""));
175 "Keys not found in source code, assumed constructed dynamically:");
176 int dynamicCount = 0;
177 for (String key : messageKeys)
181 System.out.println(" " + key);
186 if (dynamicCount < messageKeys.size())
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)
194 System.out.println(" " + key);
199 .println("\nRun i18nAnt.xml to compare other message bundles");
203 * Answers true if the key starts with one of the recorded dynamic key stubs,
209 private boolean isDynamic(String key)
211 for (String dynamic : dynamicKeys)
213 if (key.startsWith(dynamic))
222 * Scan all files within a directory
225 * @throws IOException
227 private void scanDirectory(File dir) throws IOException
229 File[] files = dir.listFiles();
251 private void scanFile(File f) throws IOException
253 String path = f.getPath();
254 if (!path.endsWith(".java"))
261 * skip class with designed dynamic lookup call
263 if (path.endsWith("gui/JvSwingUtils.java"))
268 BufferedReader br = new BufferedReader(new FileReader(f));
269 BufferedLineReader blr = new BufferedLineReader(br, bufferSize, this);
272 String line = blr.read();
276 inspectSourceLines(path, lineNo, line);
284 * Look for calls to MessageManager methods, possibly split over two or more
285 * lines that have been concatenated while parsing the file
291 private void inspectSourceLines(String path, int lineNo,
294 String lineNos = String.format("%d-%d", lineNo,
295 lineNo + bufferSize - 1);
296 for (String method : METHODS)
298 int pos = line.indexOf(method);
305 * extract what follows the opening bracket of the method call
307 String methodArgs = line.substring(pos + method.length()).trim();
308 if ("".equals(methodArgs))
311 * arguments are on next line - catch in the next read loop iteration
315 if (methodArgs.indexOf(",") == -1 && methodArgs.indexOf(")") == -1)
318 * arguments continue on next line - catch in the next read loop iteration
323 if (JVINIT == method && methodArgs.indexOf(",") == -1)
326 * not interested in 1-arg calls to jvInitComponent
331 String messageKey = getMessageKey(method, methodArgs);
333 if (METHOD3 == method)
335 String key = messageKey.substring(1, messageKey.length() - 1);
336 if (!dynamicKeys.contains(key))
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);
346 if (messageKey == null)
348 System.out.println(String.format("Trouble parsing %s line %s %s",
349 path.substring(sourcePath.length()), lineNos, line));
353 if (!(STRING_PATTERN.matcher(messageKey).matches()))
355 // System.out.println(String.format("Dynamic key at %s line %s %s",
356 // path.substring(sourcePath.length()), lineNos, line));
361 * strip leading and trailing quote
363 messageKey = messageKey.substring(1, messageKey.length() - 1);
365 if (!this.messages.containsKey(messageKey))
367 if (!invalidKeys.contains(messageKey))
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);
376 messageKeys.remove(messageKey);
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
383 if (line.contains("\""))
385 Iterator<String> it = messageKeys.iterator();
388 if (line.contains(it.next()))
390 it.remove(); // remove as 'seen'
397 * Helper method to parse out the resource bundle key parameter of a method
402 * the rest of the source line starting with arguments to method
405 private String getMessageKey(String method, String methodArgs)
407 String key = methodArgs;
410 * locate second argument if calling jvInitComponent()
412 if (method == JVINIT)
414 int commaLoc = methodArgs.indexOf(",");
419 key = key.substring(commaLoc + 1).trim();
423 * take up to next comma or ) or end of line
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("\""))
431 endPos = key.length();
434 return endPos == -1 ? null : key.substring(0, endPos);
438 * Loads properties from Message.properties
440 * @throws IOException
442 void loadMessages() throws IOException
444 messages = new Properties();
445 FileReader reader = new FileReader(
446 new File(sourcePath, "../resources/lang/Messages.properties"));
447 messages.load(reader);
450 messageKeys = new TreeSet<>();
451 for (Object key : messages.keySet())
453 messageKeys.add((String) key);
459 * Remove any trailing comments, change tabs to space, and trim
462 public String cleanLine(String l)
466 int pos = l.indexOf("//");
469 l = l.substring(0, pos);
471 l = l.replace("\t", " ").trim();