import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.HashSet;
import java.util.Properties;
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
{
/*
* 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 HashSet invalidKeys;
/**
* 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");
sourcePath = srcPath;
loadMessages();
File dir = new File(srcPath);
if (!dir.exists())
{
System.out.println(srcPath + " not found");
return;
}
invalidKeys = new HashSet();
if (dir.isDirectory())
{
scanDirectory(dir);
}
else
{
scanFile(dir);
}
reportResults();
}
/**
* Prints out counts to sysout
*/
private void reportResults()
{
System.out.println("\nScanned " + javaCount + " source files");
System.out.println("Message.properties has " + messages.size()
+ " keys");
System.out.println("Found " + invalidKeys.size()
+ " possibly invalid parameter calls");
System.out.println(messageKeys.size()
+ " keys not found, either unused or constructed dynamically");
for (String key : messageKeys)
{
System.out.println(" " + key);
}
}
/**
* 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;
}
String[] lines = new String[bufferSize];
BufferedReader br = new BufferedReader(new FileReader(f));
for (int i = 0; i < bufferSize; i++)
{
String readLine = br.readLine();
lines[i] = stripCommentsAndTrim(readLine);
}
int lineNo = 0;
while (lines[bufferSize - 1] != null)
{
lineNo++;
inspectSourceLines(path, lineNo, lines);
for (int i = 0; i < bufferSize - 1; i++)
{
lines[i] = lines[i + 1];
}
lines[bufferSize - 1] = stripCommentsAndTrim(br.readLine());
}
br.close();
}
/*
* removes anything after (and including) '//'
*/
private String stripCommentsAndTrim(String line)
{
if (line != null)
{
int pos = line.indexOf("//");
if (pos != -1)
{
line = line.substring(0, pos);
}
line = line.replace("\t", " ").trim();
}
return line;
}
/**
* Look for calls to MessageManager methods, possibly split over two or more
* lines
*
* @param path
* @param lineNo
* @param lines
*/
private void inspectSourceLines(String path, int lineNo, String[] lines)
{
String lineNos = String.format("%d-%d", lineNo, lineNo + lines.length
- 1);
String combined = combineLines(lines);
for (String method : METHODS)
{
int pos = combined.indexOf(method);
if (pos == -1)
{
continue;
}
/*
* extract what follows the opening bracket of the method call
*/
String methodArgs = combined.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;
}
if (METHOD3 == method)
{
System.out.println(String.format("Dynamic key at %s line %s %s",
path.substring(sourcePath.length()), lineNos, combined));
continue;
}
String messageKey = getMessageKey(method, methodArgs);
if (messageKey == null)
{
System.out.println(String.format("Trouble parsing %s line %s %s",
path.substring(sourcePath.length()), lineNos, combined));
continue;
}
if (!(STRING_PATTERN.matcher(messageKey).matches()))
{
System.out.println(String.format("Dynamic key at %s line %s %s",
path.substring(sourcePath.length()), lineNos, combined));
continue;
}
/*
* strip leading and trailing quote
*/
messageKey = messageKey.substring(1, messageKey.length() - 1);
if (!this.messages.containsKey(messageKey))
{
System.out.println(String.format(
"Unmatched key '%s' at line %s of %s", messageKey, lineNos,
path.substring(sourcePath.length())));
if (!invalidKeys.contains(messageKey))
{
invalidKeys.add(messageKey);
}
}
messageKeys.remove(messageKey);
}
}
/**
* 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);
}
private String combineLines(String[] lines)
{
String combined = "";
if (lines != null)
{
for (String line : lines)
{
if (line != null)
{
combined += line;
}
}
}
return combined;
}
/**
* 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);
}
}
}