/*
* This file is part of the Weborganic XMLDoclet library.
*
* For licensing information please see the file license.txt included in the release.
* A copy of this licence can also be found at
* http://www.opensource.org/licenses/artistic-license-2.0.php
*/
package org.weborganic.xmldoclet;
import java.io.File;
import java.lang.reflect.Method;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.sun.javadoc.AnnotationDesc;
import com.sun.javadoc.ClassDoc;
import com.sun.javadoc.DocErrorReporter;
import com.sun.tools.doclets.Taglet;
/**
* Container for the options for the XML Doclet.
*
* @author Christophe Lauret
*
* @version 21 June 2010
*/
public final class Options {
/**
* An empty array constant for reuse.
*/
private static final String[] EMPTY_ARRAY = new String[]{};
/**
* The default encoding for the output
*/
private static final Charset DEFAULT_CHARSET = Charset.forName("utf-8");
/**
* The default filename for the output.
*/
private static final String DEFAULT_FILENAME = "xmldoclet.xml";
/**
* Determines whether the output is a single file or multiple files.
*
* Populated from the command line via the "-multiple" flag.
*/
private boolean multipleFiles = false;
/**
* Determines whether files are organised as subfolders or all in the same folder.
*
* Populated from the command line via the "-subfolders" flag.
*/
private boolean subFolders = false;
/**
* Determines the directory where output is placed.
*
* Populated from the command line via the "-d [directory]" flag.
*/
private File directory;
/**
* The output encoding of the XML files.
*/
private Charset encoding = DEFAULT_CHARSET;
/**
* Filter classes extending the specified class.
*/
private String extendsFilter = null;
/**
* Filter classes implementing the specified class.
*/
private String implementsFilter = null;
/**
* Filter classes with the specified annotation.
*/
private String annotationFilter = null;
/**
* The taglets loaded by this doclet.
*/
private Map<String, Taglet> taglets = new HashMap<String, Taglet>();
/**
* Name of the file - used for single output only.
*
* Populated from the command line via the "-filename [file]" flag.
*/
private String filename = DEFAULT_FILENAME;
/**
* Creates new options.
*/
public Options() {
// Load the standard taglets
for (BlockTag t : BlockTag.values()) {
this.taglets.put(t.getName(), t);
}
for (InlineTag t : InlineTag.values()) {
this.taglets.put("@"+t.getName(), t);
}
}
/**
* Indicates whether these options should use multiple files.
*/
public boolean useMultipleFiles() {
return this.multipleFiles;
}
/**
* Indicates whether to organise files as subfolders for packages.
*/
public boolean useSubFolders() {
return this.subFolders;
}
/**
* Returns the charset to use to encode the output.
*
* @return the charset to use to encode the output.
*/
public Charset getEncoding() {
return this.encoding;
}
/**
* Returns the directory where to store the files.
*
* @return where to store the files.
*/
public File getDirectory() {
return this.directory;
}
/**
* Returns the name of the file for single output.
*
* @return the name of the file for single output.
*/
public String getFilename() {
return this.filename;
}
/**
* Returns the taglet instance for the specified tag name.
*
* @param name The name of the tag.
* @return The corresponding <code>Taglet</code> or <code>null</code>.
*/
public Taglet getTagletForName(String name) {
for (String n : this.taglets.keySet()) {
if (n.equals(name)) return this.taglets.get(n);
}
return null;
}
/**
* Indicates whether these options specify a filter.
*
* @return <code>true</code> if the class must implement or extends or have a specific annotation.
* <code>false</code> otherwise.
*/
public boolean hasFilter() {
return this.extendsFilter != null || this.implementsFilter != null || this.annotationFilter != null;
}
/**
* Filters the included set of classes by checking whether the given class matches all of
* the specified '-extends', '-implements' and '-annotated' options.
*
* @param doc the class documentation.
* @return <code>true</code> if the class should be included; <code>false</code> otherwise.
*/
public boolean filter(ClassDoc doc) {
boolean included = true;
// Extends
if (this.extendsFilter != null) {
included = included && filterExtends(doc, this.extendsFilter);
}
// Implements
if (this.implementsFilter != null) {
included = included && filterImplements(doc, this.implementsFilter);
}
// Annotation
if (this.annotationFilter != null) {
included = included && filterAnnotated(doc, this.annotationFilter);
}
// No filtering
return included;
}
@Override
public String toString() {
return super.toString();
}
// static methods for use by Doclet =============================================================
/**
* A JavaDoc option parsing handler.
*
* <p>This one returns the number of arguments required for the given option.
*
* @see com.sun.javadoc.Doclet#optionLength(String)
*
* @param option The name of the option.
*
* @return The number of arguments for that option.
*/
public static int getLength(String option) {
// possibly specified by javadoc understood by this doclet
if ("-d".equals(option)) return 2;
if ("-docencoding".equals(option)) return 2;
// specific to this doclet
if ("-multiple".equals(option)) return 1;
if ("-filename".equals(option)) return 2;
if ("-implements".equals(option)) return 2;
if ("-extends".equals(option)) return 2;
if ("-annotated".equals(option)) return 2;
if ("-tag".equals(option)) return 2;
if ("-taglet".equals(option)) return 2;
if ("-subfolders".equals(option)) return 1;
return 0;
}
/**
* Retrieve the expected options from the given array of options.
*
* @param root The root object which contains the options to be retrieved.
*/
public static Options toOptions(String options[][], DocErrorReporter reporter) {
Options o = new Options();
// Flags
o.multipleFiles = has(options, "-multiple");
o.subFolders = has(options, "-subfolders");
// Output directory
if (has(options, "-d")) {
String directory = get(options, "-d");
if (directory == null) {
reporter.printError("Missing value for <directory>, usage:");
reporter.printError("-d <directory> Destination directory for output files");
return null;
} else {
o.directory = new File(directory);
// TODO check
reporter.printNotice("Output directory: "+directory);
}
} else {
reporter.printError("Output directory not specified; use -d <directory>");
return null;
}
// Output encoding
if (has(options, "-docencoding")) {
String encoding = get(options, "-docencoding");
if (encoding == null) {
reporter.printError("Missing value for <name>, usage:");
reporter.printError("-docencoding <name> \t Output encoding name");
return null;
} else {
o.encoding = Charset.forName(encoding);
reporter.printNotice("Output encoding: "+o.encoding);
}
}
// Extends
if (has(options, "-filename")) {
String name = get(options, "-filename");
if (name != null && !o.multipleFiles) {
o.filename = name;
reporter.printNotice("Using file name: "+name);
} else reporter.printWarning("'-filename' option ignored");
}
// Extends
if (has(options, "-extends")) {
String superclass = get(options, "-extends");
if (superclass != null) {
o.extendsFilter = superclass;
reporter.printNotice("Filtering classes extending: "+superclass);
} else reporter.printWarning("'-extends' option ignored - superclass not specified");
}
// Annotated
if (has(options, "-annotated")) {
String annotation = get(options, "-annotated");
if (annotation != null) {
o.annotationFilter = annotation;
reporter.printNotice("Filtering classes annotated: "+annotation);
} else reporter.printWarning("'-annotated' option ignored - annotation not specified");
}
// Implements
if (has(options, "-implements")) {
String iface = get(options, "-implements");
if (iface != null) {
o.implementsFilter = iface;
reporter.printNotice("Filtering classes implementing: "+iface);
} else reporter.printWarning("'-implements' option ignored - interface not specified");
}
// Custom Tags
if (has(options, "-tag")) {
List<String> tags = getAll(options, "-tag");
for (String def : tags) {
int colon = def.indexOf(':');
String name = colon < 0? def : def.substring(0, colon);
CustomTag tag = new CustomTag(name, false);
if (colon >= 0) {
// scope
String scope = def.substring(colon+1);
colon = scope.indexOf(':');
if (colon >= 0) {
String title = scope.substring(colon+1);
scope = scope.substring(0, colon);
tag.setTitle(title);
}
tag.setScope(scope);
}
o.taglets.put(name, new CustomTag(name, true));
reporter.printNotice("Using Tag "+name);
}
}
// Taglets
if (has(options, "-taglet")) {
String classes = get(options, "-taglet");
if (classes != null) {
for (String c : classes.split(":")) {
try {
Class<?> x = Class.forName(c);
Class<? extends Taglet> t = x.asSubclass(Taglet.class);
Method m = t.getMethod("register", Map.class);
m.invoke(null, o.taglets);
reporter.printNotice("Using Taglet "+t.getName());
} catch (Exception ex) {
reporter.printError("'-taglet' option reported error - :"+ex.getMessage());
}
}
} else reporter.printWarning("'-taglet' option ignored - classes not specified");
}
// If we reached this point everything is OK
return o;
}
/**
* Indicates whether the specified option is defined.
*
* @param options the matrix of command line options.
* @param name the name of the requested option.
*
* @return <code>true</code> if defined; <code>false</code> otherwise.
*/
private static boolean has(String[][] options, String name) {
for (String[] option : options) {
if (option[0].equals(name)) return true;
}
return false;
}
/**
* Returns the list of single values for the specified option if defined.
*
* @param options the matrix of command line options.
* @param name the name of the requested option.
*
* @return the array value if available or <code>null</code>.
*/
private static List<String> getAll(String[][] options, String name) {
List<String> values = new ArrayList<String>();
for (String[] option : options) {
if (option[0].equals(name)) {
if (option.length > 1) values.add(option[1]);
}
}
return values;
}
/**
* Returns the single value for the specified option if defined.
*
* @param options the matrix of command line options.
* @param name the name of the requested option.
*
* @return the value if available or <code>null</code>.
*/
private static String get(String[][] options, String name) {
String[] option = find(options, name);
return (option.length > 1)? option[1] : null;
}
/**
* Finds the options array for the specified option name.
*
* <p>The first element is <i>always</i> the name of the option.
*
* @param options the matrix of command line options.
* @param name the name of the requested option.
*
* @return the corresponding array or an empty array.
*/
private static String[] find(String[][] options, String name) {
for (String[] option : options) {
if (option[0].equals(name)) return option;
}
// Option not available
return EMPTY_ARRAY;
}
/**
* Filters the included set of classes by checking whether the given class matches the '-extends' option.
*
* @param doc the class documentation.
* @param base the class to extend.
* @return <code>true</code> if the class should be included; <code>false</code> otherwise.
*/
private static boolean filterExtends(ClassDoc doc, String base) {
ClassDoc superclass = doc.superclass();
return superclass != null && base.equals(superclass.toString());
}
/**
* Filters the included set of classes by checking whether the given class matches the '-implements' option.
*
* @param doc the class documentation.
* @param iface the interface to implement.
* @return <code>true</code> if the class should be included; <code>false</code> otherwise.
*/
private static boolean filterImplements(ClassDoc doc, String iface) {
ClassDoc[] interfaces = doc.interfaces();
for (ClassDoc i : interfaces) {
if (iface.equals(i.toString())) return true;
}
return false;
}
/**
* Filters the included set of classes by checking whether the given class matches the '-annotated' option.
*
* @param doc the class documentation.
* @param annotation the annotation to match.
* @return <code>true</code> if the class should be included; <code>false</code> otherwise.
*/
private static boolean filterAnnotated(ClassDoc doc, String annotation) {
AnnotationDesc[] annotations = doc.annotations();
for (AnnotationDesc i : annotations) {
if (annotation.equals(i.annotationType().qualifiedName())) return true;
}
return false;
}
}