ExceptionMessagesDoclet.java
package multex.tool;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.StringReader;
import multex.Exc;
import com.sun.javadoc.ClassDoc;
import com.sun.javadoc.RootDoc;
/**This doclet collects the exception message text pattern of each concrete Throwable-subclass.
* Scans all Java sources, takes the javadoc main comment text of each concrete subclass of Throwable, and appends all of them to the
* file given by the Doclet option <code>-out</code>. This file is lateron usable as a resource bundle for internationalizing
* exception message texts.
* Each Throwable message text pattern line will be in the format
* <br>
* <i>fully qualified internal class name</i>=<i>message text pattern</i>
* <br>
* for example
* <br>
* <code>net.sourceforge.banking.lg.Account$CreditLimitExc=The amount of {0} euros is not available on this account.</code>
* <p><b>Multiple line text</b>: If the main javadoc comment consists of more than one line, a backslash is appended to each line,
* except the last one. So the convention of the .properties files continuation lines is achieved.
* As Javadoc removes leading white space on continuation lines, and java.util.Properties.load removes trailing white space,
* two subsequent lines in the Javadoc comment will be collapsed into one logical line without even a space between them.
* <br>
* So in order to make a long exception message legible, you should break inside of a word or append \u0020 or \t to each physical line.
* </p>
* <p><b>Character Encoding</b>: The encoding used to interpret characters in the source files
* can be set by the Javadoc option <code>-encoding</code>.
* In ANT use the attribute <code>Encoding="..."</code>.<br>
* The encoding used to encode characters when writing them to the -out file, is always ISO-8859-1,
* as this is the only encoding usable for .properties files, see documentation of java.util.Properties.load(InputStream).
* <br>
* So in order to make a long exception message legible, you should break inside of a word or append <code>\u0020</code> or <code>\t</code> to each physical line.
* </P>
**/
public class ExceptionMessagesDoclet {
private static final String OUT_OPTION = "-out";
/**Qualified class name of java.lang.Object*/
private static final String JAVA_LANG_OBJECT = Object.class.getName();
/**Qualified class name of java.lang.Throwable*/
private static final String JAVA_LANG_THROWABLE = Throwable.class.getName();
/**The destination, where to write the exception message texts.*/
private java.io.PrintWriter out;
/**Check for doclet-added options.
* Allowed options:
* <ul>
* <li><b>-out filename</b> : Indicates where to append the message lines.
* </ul>
* @param option the name of the option including the minus character, can be "-out" or "-outencoding".
* @return number of arguments on the command line for an option including the option name itself. Zero return means option not known. Negative value means error occurred.
*/
public static int optionLength(final String option){
if(OUT_OPTION.equals(option)){return 2;}
return 0;
}
/**Entry point to execute this Doclet. It will be invoked by the Javadoc tool.
* @param i_rootDoc Comprising all classes to visit.
* @return true
* @throws Exception An error occured inside of this doclet.
*/
public static boolean start(final RootDoc i_rootDoc) throws Exception {
new ExceptionMessagesDoclet()._execute(i_rootDoc);
return true;
}
/**Appends all exception message text patterns to the file named by option -out in encoding ISO-8859-1,
* as this is the standard encoding for Java .properties files,
* see java.util.Properties.load(InputStream inStream).
* @param i_rootDoc Comprising all classes to visit.
* @throws Exception An error occured inside of this doclet.
*/
private void _execute(final RootDoc i_rootDoc) throws Exception {
final String outFileName = _getOutputFilename(i_rootDoc.options());
final File outFile = new File(outFileName).getAbsoluteFile();
//final FileWriter fileWriter = new FileWriter(outFile, /*append*/true);
if(outFile.isDirectory()){
throw new Exception("Output file " + outFile + " is a directory");
}
if(!outFile.canWrite()){
throw new Exception("Cannot write to output file " + outFile);
}
final FileOutputStream fileOutputStream = new FileOutputStream(outFile, /*append*/true);
final OutputStreamWriter outputStreamWriter = new OutputStreamWriter(fileOutputStream, "ISO-8859-1");
//this.out = new PrintWriter(fileWriter);
this.out = new PrintWriter(outputStreamWriter);
_visitAll(i_rootDoc);
this.out.close();
System.out.println();
System.out.println(getClass().getName() + ": All exception message text patterns have been appended to file " + outFile);
}
/**Gets the value of the doclet option -out.
* This will be used as the name of the file to append the messages to.
* @param options Each element of it is an array with it's first element being the option's name, the following elements being option values.
* @return the filename where to add exception text property definitions
*/
private String _getOutputFilename(final String[][] options) {
for(int i=0; i<options.length; i++){
final String[] option = options[i];
if(OUT_OPTION.equals(option[0])){
return option[1];
}
}
throw new OutputFilenameMissingExc();
}
public static class OutputFilenameMissingExc extends Exc {
private static final long serialVersionUID = -4465866427805576812L;
public OutputFilenameMissingExc() {
super("Missing name of output file, where to append the extracted exception text property definitions.\nUse Javadoc doclet option " + OUT_OPTION + " to indicate it!");
}
}
/**Visits all classes contained in rootDoc. Visits the nested classes, too.*/
private void _visitAll(final RootDoc rootDoc) throws Exception {
final ClassDoc[] classDocs = rootDoc.classes();
for (int i = 0; i < classDocs.length; ++i) {
final ClassDoc classDoc = classDocs[i];
//System.out.println("Visiting class " + classDoc);
_visitClass(classDoc);
}
}
/**Vists the class in classDoc.
* @throws TooNestedThrowableException
* @throws IOException */
private void _visitClass(final ClassDoc classDoc) throws TooNestedThrowableException, IOException{
_printMessageTextPatternOfThrowable(classDoc);
/*It seems that recursion is not necessary!
final ClassDoc[] innerClasses = classDoc.innerClasses();
for(int i = 0; i < innerClasses.length; ++i){
final ClassDoc innerClassDoc = innerClasses[i];
_visitClass(innerClassDoc);
}
*/
}
/**Prints the message text pattern for the class, if it is a concrete subclass of Throwable.
* The main commentText (without tags) of the class is written in the Java property file format to {@link #out}.
*
* @param classDoc representing the class to be checked.
* @throws TooNestedThrowableException
* @throws IOException
*/
private void _printMessageTextPatternOfThrowable(final ClassDoc classDoc) throws TooNestedThrowableException, IOException {
if(classDoc.isAbstract() || classDoc.isOrdinaryClass()){return;}
//Now classdoc is a concrete interface, annotation type, enum, exception, or error
//out.println("class: " + classDoc);
if(!_isSubclassOfThrowable(classDoc)){return;}
final String commentText = classDoc.commentText();
if(commentText.length()<10){
System.err.println("Too short (<10ch) message text \"" + commentText + "\" for Throwable " + classDoc);
return;
}
final String key = _internalClassName(classDoc);
this.out.print(key);
this.out.print(" = ");
_printMultilineTextForPropertiesFile(commentText);
}
/**Prints the multi-line text suitable for usage as value in a .properties file to this.out.
* Each new-line in the text is printed as the sequence backslash, new-line.
* At the end a simple new-line is printed.
* @param multilineText The text to be printed in .properties multi-line format.
* @throws IOException
*/
private void _printMultilineTextForPropertiesFile(final String multilineText) throws IOException {
final BufferedReader br = new BufferedReader(new StringReader(multilineText));
for(boolean isContinuationLine=false;;){
final String line = br.readLine();
if(null==line){break;}
if(isContinuationLine){this.out.println('\\');}
isContinuationLine = true;
this.out.print(line);
}
this.out.println();
}
/**Throwable class {canonicalName} has more than one nesting level. Handling not yet implemented.*/
public static final class TooNestedThrowableException extends Exception {
private static final long serialVersionUID = 2388470231622088395L;
public TooNestedThrowableException(final String canonicalName){
super("Throwable class " + canonicalName + " has more than one nesting level. Handling not yet implemented.");
}
}
/**Returns the internal class name of the class.
* @param classDoc the class documentation, from which to get the internal name.
* @return the class name in the format of java.lang.Class.getName(),
* that means nested class identifiers are separated by a dollar sign ($),
* rather than by a period (.) from the identifier of the containing class.
* @throws TooNestedThrowableException The internal class name would have more than one dollar character.
*/
private String _internalClassName(final ClassDoc classDoc) throws TooNestedThrowableException {
final String canonicalName = classDoc.toString();
final ClassDoc containingClass = classDoc.containingClass();
if(null==containingClass){
//canonicalName does not contain a dollar ($) character.
return canonicalName;
}
if(null!=containingClass.containingClass()){
throw new TooNestedThrowableException(canonicalName);
}
final int indexOfLastDollar = canonicalName.lastIndexOf('.');
final char[] characters = canonicalName.toCharArray();
characters[indexOfLastDollar] = '$';
final String result = new String(characters);
return result;
}
private boolean _isSubclassOfThrowable(final ClassDoc classDoc){
final ClassDoc superClass = classDoc.superclass();
final String superClassName = superClass.toString();
//out.println("super: " + superClass);
if(JAVA_LANG_THROWABLE.equals(superClassName)){return true;}
if(JAVA_LANG_OBJECT.equals(superClassName)){return false;}
return _isSubclassOfThrowable(superClass);
}
}