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.Properties;
29 import java.util.TreeSet;
30 import java.util.regex.Pattern;
33 * This class scans Java source files for calls to MessageManager and reports
35 * <li>calls using keys not found in Messages.properties</li>
36 * <li>any unused keys in Messages.properties</li>
38 * It does not handle dynamically constructed keys, these are reported as
39 * possible errors for manual inspection. <br>
40 * For comparing translated bundles with Messages.properties, see i18nAnt.xml
45 public class MessageBundleChecker implements BufferedLineReader.LineCleaner
49 * opening quote, closing quote, no quotes in between
51 static Pattern STRING_PATTERN = Pattern.compile("^\"[^\"]*\"$");
54 * number of text lines to read at a time in order to parse
55 * code that is split over several lines
57 static int bufferSize = 3;
60 * resource bundle key is arg0 for these methods
62 static final String METHOD1 = "MessageManager.getString(";
64 static final String METHOD2 = "MessageManager.formatMessage(";
66 static final String METHOD3 = "MessageManager.getStringOrReturn(";
69 * resource bundle key is arg1 for this method
71 static final String JVINIT = "JvSwingUtils.jvInitComponent(";
73 static final String[] METHODS = { METHOD1, METHOD2, METHOD3, JVINIT };
76 * root of the Java source folders we want to scan
81 * contents of Messages.properties
83 private Properties messages;
86 * keys from Messages.properties
87 * we remove entries from here as they are found to be used
88 * any left over are unused entries
90 private TreeSet<String> messageKeys;
92 private int javaCount;
94 private Set<String> invalidKeys;
96 private Set<String> dynamicKeys;
99 * Runs the scan given the path to the root of Java source directories
102 * [0] path to the source folder to scan
104 * [1] (optional) read buffer size (default is 3); increasing this
105 * may detect more results but will give higher error counts due to
106 * double counting of the same code
107 * @throws IOException
109 public static void main(String[] args) throws IOException
111 if (args.length != 1 && args.length != 2)
113 System.out.println("Usage: <pathToSourceFolder> [readBufferSize]");
116 if (args.length == 2)
118 bufferSize = Integer.valueOf(args[1]);
120 new MessageBundleChecker().doMain(args[0]);
124 * Main method to perform the work
127 * @throws IOException
129 private void doMain(String srcPath) throws IOException
132 "Scanning " + srcPath + " for calls to MessageManager\n");
133 System.out.println("Please note this is not a perfect check:");
135 "- message keys constructed dynamically can't be checked");
137 .println("- message keys passed as variables can't be checked");
139 "- references in commented out code are not filtered out");
141 "Verify 'missing' keys manually by a source code search\n");
143 sourcePath = srcPath;
145 File dir = new File(srcPath);
148 System.out.println(srcPath + " not found");
152 invalidKeys = new HashSet<>();
153 dynamicKeys = new HashSet<>();
155 if (dir.isDirectory())
167 * Prints out counts to sysout
169 private void reportResults()
171 System.out.println("\nScanned " + javaCount + " source files");
173 "Messages.properties has " + messages.size() + " keys");
174 if (!invalidKeys.isEmpty())
176 System.out.println("Found " + invalidKeys.size()
177 + " possibly invalid unmatched keys in source code"
178 + (invalidKeys.size() > 1 ? "s" : ""));
182 "Keys not found in source code, assumed constructed dynamically:");
183 int dynamicCount = 0;
184 for (String key : messageKeys)
188 System.out.println(" " + key);
193 if (dynamicCount < messageKeys.size())
195 System.out.println((messageKeys.size() - dynamicCount)
196 + " keys not found, possibly unused, or used indirectly (check code manually!)");
197 for (String key : messageKeys)
201 System.out.println(" " + key);
206 .println("(Run i18nAnt.xml to compare other message bundles)");
210 * Answers true if the key starts with one of the recorded dynamic key stubs,
216 private boolean isDynamic(String key)
218 for (String dynamic : dynamicKeys)
220 if (key.startsWith(dynamic))
229 * Scan all files within a directory
232 * @throws IOException
234 private void scanDirectory(File dir) throws IOException
236 File[] files = dir.listFiles();
258 private void scanFile(File f) throws IOException
260 String path = f.getPath();
261 if (!path.endsWith(".java"))
268 * skip class with designed dynamic lookup call
270 if (path.endsWith("gui/JvSwingUtils.java"))
275 BufferedReader br = new BufferedReader(new FileReader(f));
276 BufferedLineReader blr = new BufferedLineReader(br, bufferSize, this);
279 String line = blr.read();
283 inspectSourceLines(path, lineNo, line);
291 * Look for calls to MessageManager methods, possibly split over two or more
292 * lines that have been concatenated while parsing the file
298 private void inspectSourceLines(String path, int lineNo, String line)
300 String lineNos = String.format("%d-%d", lineNo,
301 lineNo + bufferSize - 1);
302 for (String method : METHODS)
304 int pos = line.indexOf(method);
311 * extract what follows the opening bracket of the method call
313 String methodArgs = line.substring(pos + method.length()).trim();
314 if ("".equals(methodArgs))
317 * arguments are on next line - catch in the next read loop iteration
321 if (methodArgs.indexOf(",") == -1 && methodArgs.indexOf(")") == -1)
324 * arguments continue on next line - catch in the next read loop iteration
329 if (JVINIT == method && methodArgs.indexOf(",") == -1)
332 * not interested in 1-arg calls to jvInitComponent
337 String messageKey = getMessageKey(method, methodArgs);
339 if (METHOD3 == method)
341 String key = messageKey.substring(1, messageKey.length() - 1);
342 if (!dynamicKeys.contains(key))
344 System.out.println(String.format(
345 "Dynamic key \"" + key + "\" at %s line %s %s",
346 path.substring(sourcePath.length()), lineNos, line));
347 dynamicKeys.add(key);
352 if (messageKey == null)
354 System.out.println(String.format("Trouble parsing %s line %s %s",
355 path.substring(sourcePath.length()), lineNos, line));
359 if (!(STRING_PATTERN.matcher(messageKey).matches()))
361 System.out.println(String.format("Dynamic key at %s line %s %s",
362 path.substring(sourcePath.length()), lineNos, line));
367 * strip leading and trailing quote
369 messageKey = messageKey.substring(1, messageKey.length() - 1);
371 if (!this.messages.containsKey(messageKey))
373 if (!invalidKeys.contains(messageKey))
375 // report each key the first time found only
376 System.out.println(String.format(
377 "Unmatched key '%s' at line %s of %s", messageKey,
378 lineNos, path.substring(sourcePath.length())));
379 invalidKeys.add(messageKey);
382 messageKeys.remove(messageKey);
387 * Helper method to parse out the resource bundle key parameter of a method
392 * the rest of the source line starting with arguments to method
395 private String getMessageKey(String method, String methodArgs)
397 String key = methodArgs;
400 * locate second argument if calling jvInitComponent()
402 if (method == JVINIT)
404 int commaLoc = methodArgs.indexOf(",");
409 key = key.substring(commaLoc + 1).trim();
413 * take up to next comma or ) or end of line
415 int commaPos = key.indexOf(",");
416 int bracePos = key.indexOf(")");
417 int endPos = commaPos == -1 ? bracePos
418 : (bracePos == -1 ? commaPos : Math.min(commaPos, bracePos));
419 if (endPos == -1 && key.length() > 1 && key.endsWith("\""))
421 endPos = key.length();
424 return endPos == -1 ? null : key.substring(0, endPos);
428 * Loads properties from Message.properties
430 * @throws IOException
432 void loadMessages() throws IOException
434 messages = new Properties();
435 FileReader reader = new FileReader(
436 new File(sourcePath, "../resources/lang/Messages.properties"));
437 messages.load(reader);
440 messageKeys = new TreeSet<>();
441 for (Object key : messages.keySet())
443 messageKeys.add((String) key);
449 * Remove any trailing comments, change tabs to space, and trim
452 public String cleanLine(String l)
456 int pos = l.indexOf("//");
459 l = l.substring(0, pos);
461 l = l.replace("\t", " ").trim();