/*
* Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
* Copyright (C) $$Year-Rel$$ The Jalview Authors
*
* This file is part of Jalview.
*
* Jalview is free software: you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3
* of the License, or (at your option) any later version.
*
* Jalview is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR
* PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Jalview. If not, see .
* The Jalview Authors are detailed in the 'AUTHORS' file.
*/
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Properties;
import java.util.Set;
import java.util.TreeSet;
import java.util.regex.Pattern;
/**
* This class scans Java source files for calls to MessageManager and reports
*
* - calls using keys not found in Messages.properties
* - any unused keys in Messages.properties
*
* It does not handle dynamically constructed keys, these are reported as
* possible errors for manual inspection.
* For comparing translated bundles with Messages.properties, see i18nAnt.xml
*
* @author gmcarstairs
*
*/
public class MessageBundleChecker implements BufferedLineReader.LineCleaner
{
/*
* regex ^"[^"]*"$
* opening quote, closing quote, no quotes in between
*/
static Pattern STRING_PATTERN = Pattern.compile("^\"[^\"]*\"$");
/*
* number of text lines to read at a time in order to parse
* code that is split over several lines
*/
static int bufferSize = 3;
/*
* resource bundle key is arg0 for these methods
*/
static final String METHOD1 = "MessageManager.getString(";
static final String METHOD2 = "MessageManager.formatMessage(";
static final String METHOD3 = "MessageManager.getStringOrReturn(";
/*
* resource bundle key is arg1 for this method
*/
static final String JVINIT = "JvSwingUtils.jvInitComponent(";
static final String[] METHODS = { METHOD1, METHOD2, METHOD3, JVINIT };
/*
* root of the Java source folders we want to scan
*/
String sourcePath;
/*
* contents of Messages.properties
*/
private Properties messages;
/*
* keys from Messages.properties
* we remove entries from here as they are found to be used
* any left over are unused entries
*/
private TreeSet messageKeys;
private int javaCount;
private Set invalidKeys;
private Set dynamicKeys;
/**
* Runs the scan given the path to the root of Java source directories
*
* @param args
* [0] path to the source folder to scan
* @param args
* [1] (optional) read buffer size (default is 3); increasing this
* may detect more results but will give higher error counts due to
* double counting of the same code
* @throws IOException
*/
public static void main(String[] args) throws IOException
{
if (args.length != 1 && args.length != 2)
{
System.out.println("Usage: [readBufferSize]");
return;
}
if (args.length == 2)
{
bufferSize = Integer.valueOf(args[1]);
}
new MessageBundleChecker().doMain(args[0]);
}
/**
* Main method to perform the work
*
* @param srcPath
* @throws IOException
*/
private void doMain(String srcPath) throws IOException
{
System.out.println(
"Scanning " + srcPath + " for calls to MessageManager\n");
sourcePath = srcPath;
loadMessages();
File dir = new File(srcPath);
if (!dir.exists())
{
System.out.println(srcPath + " not found");
return;
}
invalidKeys = new HashSet<>();
dynamicKeys = new HashSet<>();
if (dir.isDirectory())
{
scanDirectory(dir);
}
else
{
scanFile(dir);
}
reportResults();
}
/**
* Prints out counts to sysout
*/
private void reportResults()
{
System.out.println(
"\nMessages.properties has " + messages.size() + " keys");
System.out.println("Scanned " + javaCount + " source files\n");
if (!invalidKeys.isEmpty())
{
System.out.println("Found " + invalidKeys.size()
+ " possibly invalid unmatched key(s) in source code"
+ (invalidKeys.size() > 1 ? "s" : ""));
}
System.out.println(
"Keys not found in source code, assumed constructed dynamically:");
int dynamicCount = 0;
for (String key : messageKeys)
{
if (isDynamic(key))
{
System.out.println(" " + key);
dynamicCount++;
}
}
if (dynamicCount < messageKeys.size())
{
System.out.println((messageKeys.size() - dynamicCount)
+ " key(s) not found, possibly unused, or used indirectly (check code manually!)");
for (String key : messageKeys)
{
if (!isDynamic(key))
{
System.out.println(" " + key);
}
}
}
System.out
.println("\nRun i18nAnt.xml to compare other message bundles");
}
/**
* Answers true if the key starts with one of the recorded dynamic key stubs,
* else false
*
* @param key
* @return
*/
private boolean isDynamic(String key)
{
for (String dynamic : dynamicKeys)
{
if (key.startsWith(dynamic))
{
return true;
}
}
return false;
}
/**
* Scan all files within a directory
*
* @param dir
* @throws IOException
*/
private void scanDirectory(File dir) throws IOException
{
File[] files = dir.listFiles();
if (files != null)
{
for (File f : files)
{
if (f.isDirectory())
{
scanDirectory(f);
}
else
{
scanFile(f);
}
}
}
}
/**
* Scan a Java file
*
* @param f
*/
private void scanFile(File f) throws IOException
{
String path = f.getPath();
if (!path.endsWith(".java"))
{
return;
}
javaCount++;
/*
* skip class with designed dynamic lookup call
*/
if (path.endsWith("gui/JvSwingUtils.java"))
{
return;
}
BufferedReader br = new BufferedReader(new FileReader(f));
BufferedLineReader blr = new BufferedLineReader(br, bufferSize, this);
int lineNo = 0;
String line = blr.read();
while (line != null)
{
lineNo++;
inspectSourceLines(path, lineNo, line);
line = blr.read();
}
br.close();
}
/**
* Look for calls to MessageManager methods, possibly split over two or more
* lines that have been concatenated while parsing the file
*
* @param path
* @param lineNo
* @param line
*/
private void inspectSourceLines(String path, int lineNo,
final String line)
{
String lineNos = String.format("%d-%d", lineNo,
lineNo + bufferSize - 1);
for (String method : METHODS)
{
int pos = line.indexOf(method);
if (pos == -1)
{
continue;
}
/*
* extract what follows the opening bracket of the method call
*/
String methodArgs = line.substring(pos + method.length()).trim();
if ("".equals(methodArgs))
{
/*
* arguments are on next line - catch in the next read loop iteration
*/
continue;
}
if (methodArgs.indexOf(",") == -1 && methodArgs.indexOf(")") == -1)
{
/*
* arguments continue on next line - catch in the next read loop iteration
*/
continue;
}
if (JVINIT == method && methodArgs.indexOf(",") == -1)
{
/*
* not interested in 1-arg calls to jvInitComponent
*/
continue;
}
String messageKey = getMessageKey(method, methodArgs);
if (METHOD3 == method)
{
String key = messageKey.substring(1, messageKey.length() - 1);
if (!dynamicKeys.contains(key))
{
// System.out.println(String.format(
// "Dynamic key \"" + key + "\" at %s line %s %s",
// path.substring(sourcePath.length()), lineNos, line));
dynamicKeys.add(key);
}
continue;
}
if (messageKey == null)
{
System.out.println(String.format("Trouble parsing %s line %s %s",
path.substring(sourcePath.length()), lineNos, line));
continue;
}
if (!(STRING_PATTERN.matcher(messageKey).matches()))
{
// System.out.println(String.format("Dynamic key at %s line %s %s",
// path.substring(sourcePath.length()), lineNos, line));
continue;
}
/*
* strip leading and trailing quote
*/
messageKey = messageKey.substring(1, messageKey.length() - 1);
if (!this.messages.containsKey(messageKey))
{
if (!invalidKeys.contains(messageKey))
{
// report each key the first time found only
System.out.println(String.format(
"Unmatched key '%s' at line %s of %s", messageKey,
lineNos, path.substring(sourcePath.length())));
invalidKeys.add(messageKey);
}
}
messageKeys.remove(messageKey);
}
/*
* and a brute force scan for _any_ as yet unseen message key, to catch the
* cases where it is in a variable or a condition
*/
if (line.contains("\""))
{
Iterator it = messageKeys.iterator();
while (it.hasNext())
{
if (line.contains(it.next()))
{
it.remove(); // remove as 'seen'
}
}
}
}
/**
* Helper method to parse out the resource bundle key parameter of a method
* call
*
* @param method
* @param methodArgs
* the rest of the source line starting with arguments to method
* @return
*/
private String getMessageKey(String method, String methodArgs)
{
String key = methodArgs;
/*
* locate second argument if calling jvInitComponent()
*/
if (method == JVINIT)
{
int commaLoc = methodArgs.indexOf(",");
if (commaLoc == -1)
{
return null;
}
key = key.substring(commaLoc + 1).trim();
}
/*
* take up to next comma or ) or end of line
*/
int commaPos = key.indexOf(",");
int bracePos = key.indexOf(")");
int endPos = commaPos == -1 ? bracePos
: (bracePos == -1 ? commaPos : Math.min(commaPos, bracePos));
if (endPos == -1 && key.length() > 1 && key.endsWith("\""))
{
endPos = key.length();
}
return endPos == -1 ? null : key.substring(0, endPos);
}
/**
* Loads properties from Message.properties
*
* @throws IOException
*/
void loadMessages() throws IOException
{
messages = new Properties();
FileReader reader = new FileReader(
new File(sourcePath, "../resources/lang/Messages.properties"));
messages.load(reader);
reader.close();
messageKeys = new TreeSet<>();
for (Object key : messages.keySet())
{
messageKeys.add((String) key);
}
}
/**
* Remove any trailing comments, change tabs to space, and trim
*/
@Override
public String cleanLine(String l)
{
if (l != null)
{
int pos = l.indexOf("//");
if (pos != -1)
{
l = l.substring(0, pos);
}
l = l.replace("\t", " ").trim();
}
return l;
}
}