/**
* Copyright 2011 meltmedia
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.xchain.framework.doclets;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.net.URLConnection;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.xml.transform.Templates;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import org.xchain.Catalog;
import org.xchain.annotations.Attribute;
import org.xchain.annotations.Element;
import org.xchain.annotations.Namespace;
import org.xchain.framework.util.HtmlUtil;
import com.sun.javadoc.AnnotationDesc;
import com.sun.javadoc.ClassDoc;
import com.sun.javadoc.MethodDoc;
import com.sun.javadoc.PackageDoc;
import com.sun.javadoc.RootDoc;
import com.sun.javadoc.AnnotationDesc.ElementValuePair;
/**
* Doclet for producing documentation on catalogs and commands available in namespaces.
*
* @author Devon Tackett
* @author Christian Trimble
*/
public class NamespaceDoclet {
private static Pattern localNamePattern;
private static ClassDoc catalogDoc;
private static final String XDOC_PATH = "./xdoc/namespaces/";
private static final String APT_PATH = "./apt/namespaces/";
public static boolean start(RootDoc root) {
StringBuffer namespaceData;
if (!makeDirectories(XDOC_PATH))
return false;
if (!makeDirectories(APT_PATH))
return false;
try {
copyAttributeTypeDoc();
} catch (Exception e) {
System.out.println("Could not load attribute type documentation.");
e.printStackTrace();
return false;
}
// create the templates.
Templates templates = null;
try {
templates = loadTemplates();
}
catch( Exception e ) {
System.out.println("Could not load stylesheet for xchain namespace documentation.");
e.printStackTrace();
return false;
}
// Compile the localName regex pattern.
localNamePattern = Pattern.compile(".*localName=\"([^\"]*)\".*");
// Find the Catalog classDoc
catalogDoc = root.classNamed(Catalog.class.getName());
// Process all the packages.
for (PackageDoc packageDoc : root.specifiedPackages()) {
if (isNamespacePackage(packageDoc)) {
try {
namespaceData = new StringBuffer();
// Hard coded hack to ignore the jsl namespace. It'll need to be built by hand.
if (getUnqualifiedName(packageDoc.name()).equalsIgnoreCase("jsl"))
continue;
File xdocFile = new File(XDOC_PATH + getUnqualifiedName(packageDoc.name())+ ".xml");
System.out.println("Generating " + xdocFile.getAbsolutePath() + "...");
// Build the xdoc
// Add the XML header
namespaceData.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
namespaceData.append("<document>\n");
namespaceData.append(createIndent(1)).append("<properties>\n");
// Set the title to the package name.
namespaceData.append(createIndent(2)).append("<title>").append(getUnqualifiedName(packageDoc.name())).append(" namespace</title>\n");
namespaceData.append(createIndent(1)).append("</properties>\n");
namespaceData.append(createIndent(1)).append("<body>\n");
namespaceData.append(createIndent(1)).append("<section name=\"").append(getUnqualifiedName(packageDoc.name())).append(" Namespace\">\n");
namespaceData.append(createIndent(1)).append("<p>Namespace URI: ").append(getNamespaceUri(packageDoc)).append("</p>");
// Namespace comments.
namespaceData.append(createIndent(2)).append(HtmlUtil.htmlFragmentToXmlFragment(packageDoc.commentText()));
Map<String, Entry> commandMap = new HashMap<String, Entry>();
Map<String, Entry> catalogMap = new HashMap<String, Entry>();
// Build the command map for all the catalogs and commands in the namespace.
for (ClassDoc classDoc : packageDoc.allClasses()) {
if (isElement(classDoc)) {
createEntries(commandMap, catalogMap, classDoc);
}
}
namespaceData.append(createIndent(1)).append("</section>\n");
// Catalogs.
if (!catalogMap.isEmpty()) {
namespaceData.append(createIndent(2)).append("<section name=\"Available Catalogs\">\n");
writeMap(catalogMap, namespaceData);
namespaceData.append(createIndent(2)).append("</section>\n");
}
// Commands.
if (!commandMap.isEmpty()) {
namespaceData.append(createIndent(2)).append("<section name=\"Available Commands\">\n");
writeMap(commandMap, namespaceData);
namespaceData.append(createIndent(2)).append("</section>\n");
}
namespaceData.append(createIndent(1)).append("</body>\n");
namespaceData.append("</document>\n");
xdocFile.createNewFile();
writeData( templates, xdocFile, namespaceData.toString() );
} catch (Exception ex) {
System.out.println("Exception found.");
ex.printStackTrace();
return false;
}
}
}
return true;
}
/**
* Get the namespace uri for the given packageDoc.
*
* @param packageDoc The packageDoc to check.
*
* @return The namespace uri for the packageDoc.
*/
private static String getNamespaceUri(PackageDoc packageDoc) {
AnnotationDesc annoDesc = getAnnotation(packageDoc.annotations(), Namespace.class);
return getNamespaceAttribute(annoDesc);
}
/**
* Append entry information to the given StringBuffer.
*
* @param map The map of entries to append.
* @param doc The StringBuffer to build upon.
*/
private static void writeMap(Map<String, Entry> map, StringBuffer doc) {
// Write out the entries in a sorted order.
TreeSet<String> nameSet = new TreeSet<String>(map.keySet());
for(String name : nameSet) {
appendEntity(doc, map.get(name), 3);
}
}
/**
* Append the given entry to the given StringBuffer.
*
* @param doc The StringBuffer to append to.
* @param command The entry to append.
* @param currentDepth The tab depth for the entry. Used for document cosmetic purposes.
*/
private static void appendEntity(StringBuffer doc, Entry command, int currentDepth) {
doc.append("<subsection name=\"").append(command.getName()).append("\">\n");
doc.append(command.getContent()).append("\n");
TreeSet<String> subCommands = new TreeSet<String>(command.getSubEntries().keySet());
for(String subCommandName : subCommands) {
appendEntity(doc, command.getSubEntries().get(subCommandName), currentDepth + 1);
}
doc.append("</subsection>");
}
/**
* Get an unqualified name from a fully qualified name.
*
* @param name The fully qualified name.
*
* @return The unqualified name.
*/
private static String getUnqualifiedName(String name) {
int lastIndex = name.lastIndexOf(".");
if (lastIndex != -1)
name = name.substring(lastIndex+1);
return name;
}
/**
* Add a catalog or command to the given maps.
*
* @param commandMap The currently known command map.
* @param catalogMap The currently known catalog map.
* @param entityDoc The entity to parse.
*/
private static void createEntries(Map<String, Entry> commandMap, Map<String, Entry> catalogMap, ClassDoc entityDoc)
throws Exception
{
// Create a new entity.
Entry entity = new Entry();
// Set the name.
entity.setName(getElementName(entityDoc));
// Create the content.
StringBuffer entityContent = new StringBuffer();
if (hasAttributes(entityDoc)) {
// Set the attributes.
entityContent.append(createIndent(4)).append("Attributes\n");
entityContent.append(createIndent(4)).append("<table>\n");
entityContent.append(createIndent(5)).append("<tr><th>Name</th><th>Description</th><th>Type</th><th>Default Value</th><th>Java Return Type</th></tr>\n");
appendAttributes(entityContent, entityDoc);
entityContent.append(createIndent(4)).append("</table>\n");
}
// Grab the javadoc comments.
entityContent.append(HtmlUtil.htmlFragmentToXmlFragment(entityDoc.commentText()));
// Set the content on the command.
entity.setContent(entityContent);
Map<String, Entry> entityMap;
if (isCatalog(entityDoc))
entityMap = catalogMap;
else
entityMap = commandMap;
String parentEntityName = getParentEntity(entityDoc);
// Check if the entity is a sub entity.
if (parentEntityName != null && parentEntityName.trim().length() != 0) {
Entry parentCommand = entityMap.get(parentEntityName);
parentCommand.addSubEntry(entity);
} else {
// Add the entity to the entity map.
entityMap.put(entity.getName(), entity);
}
}
/**
* Get the name of the parent element. Null if there is no parent element.
*/
private static String getParentEntity(ClassDoc commandDoc) {
AnnotationDesc annoDesc = getAnnotation(commandDoc.annotations(), Element.class);
for (ElementValuePair elementValue : annoDesc.elementValues()) {
if (elementValue.element().name().equalsIgnoreCase("parentElements")) {
Matcher matcher = localNamePattern.matcher(elementValue.value().toString());
if (matcher.matches())
return matcher.group(1);
}
}
return null;
}
/**
* Determine if the given ClassDoc is a Catalog.
*/
private static boolean isCatalog(ClassDoc entityDoc) {
return entityDoc.subclassOf(catalogDoc);
}
/**
* Add all the attributes for the given class to the given StringBuffer.
*
* @param doc The StringBuffer to append to.
* @param command The command to inspect.
*/
private static void appendAttributes(StringBuffer doc, ClassDoc command) {
// Get attributes from interfaces.
for (ClassDoc parentClassDoc : command.interfaces()) {
appendAttributes(doc, parentClassDoc);
}
// Get attributes from super class.
if (command.superclass() != null) {
appendAttributes(doc, command.superclass());
}
for(MethodDoc methodDoc : command.methods()) {
if (isAttribute(methodDoc)) {
AnnotationDesc annoDesc = getAnnotation(methodDoc.annotations(), Attribute.class);
doc.append(createIndent(5)).append("<tr>");
doc.append("<td>").append(getAttributeName(annoDesc)).append("</td>");
doc.append("<td>").append(methodDoc.commentText()).append("</td>");
String attributeType = getAttributeType(annoDesc);
doc.append("<td><a href=\"./attributetypes.html#").append(attributeType).append("\">").append(attributeType).append("</a></td>");
doc.append("<td>").append(getAttributeDefaultValue(annoDesc)).append("</td>");
doc.append("<td>").append(methodDoc.returnType().typeName()).append("</td>");
doc.append("</tr>\n");
}
}
}
/**
* Get the name of the Attribute annotation.
*/
private static String getAttributeName(AnnotationDesc annoDesc) {
for (ElementValuePair elementValue : annoDesc.elementValues()) {
if (elementValue.element().name().equalsIgnoreCase("localName"))
return elementValue.value().toString().replaceAll("\"", "");
}
return "unknown";
}
/**
* Get the Attribute annotation type.
*/
private static String getAttributeType(AnnotationDesc annoDesc) {
for (ElementValuePair elementValue : annoDesc.elementValues()) {
if (elementValue.element().name().equalsIgnoreCase("type"))
return elementValue.value().toString().replaceAll("\"", "");
}
return "unknown";
}
/**
* Get the Attribute annotation default value.
*/
private static String getAttributeDefaultValue(AnnotationDesc annoDesc) {
for (ElementValuePair elementValue : annoDesc.elementValues()) {
if (elementValue.element().name().equalsIgnoreCase("defaultValue"))
return elementValue.value().toString().replaceAll("\"", "").replace("\'", "");
}
return "N/A";
}
/**
* Get the Namespace annotation uri.
*/
private static String getNamespaceAttribute(AnnotationDesc annoDesc) {
for (ElementValuePair elementValue : annoDesc.elementValues()) {
if (elementValue.element().name().equalsIgnoreCase("uri"))
return elementValue.value().toString().replaceAll("\"", "");
}
return "unspecified";
}
/**
* Get the command name for the given class.
*
* @param command The class command.
*
* @return The name of the command.
*/
private static String getElementName(ClassDoc command) {
for (AnnotationDesc annotationDesc : command.annotations()) {
if (annotationDesc.annotationType().qualifiedName().equals(Element.class.getName())) {
for (ElementValuePair elementValue : annotationDesc.elementValues()) {
return elementValue.value().toString().replaceAll("\"", "");
}
}
}
return "unknown";
}
/**
* Determine if the given class has any Attributes.
*
* @param classDoc The class to check.
*
* @return True if the class has any attributes.
*/
private static boolean hasAttributes(ClassDoc classDoc) {
for (MethodDoc methodDoc : classDoc.methods()) {
if (isAttribute(methodDoc))
return true;
}
for (ClassDoc parentClassDoc : classDoc.interfaces()) {
if (hasAttributes(parentClassDoc))
return true;
}
if (classDoc.superclass() != null) {
return hasAttributes(classDoc.superclass());
}
return false;
}
/**
* Determine if any of the annotations in the given array are of the given class.
*
* @param annotations The array of annotations to check.
* @param annotationClass The class to check for.
*
* @return True if any of the annotations in the given array are of the given class. False otherwise.
*/
private static boolean hasAnnotation(AnnotationDesc[] annotations, Class annotationClass) {
for (AnnotationDesc annotationDesc : annotations) {
if (annotationDesc.annotationType().qualifiedName().equals(annotationClass.getName())) {
return true;
}
}
return false;
}
/**
* Get the annotation type from the array of annotations.
*
* @param annotations The array of annotations to search.
* @param annotationClass The class to check for.
*
* @return The annotation desc for the annotation or null if not found.
*/
public static AnnotationDesc getAnnotation(AnnotationDesc[] annotations, Class annotationClass) {
for (AnnotationDesc annotationDesc : annotations) {
if (annotationDesc.annotationType().qualifiedName().equals(annotationClass.getName())) {
return annotationDesc;
}
}
return null;
}
/**
* Determine if the given packages is a Namespace.
*
* @param packageDoc The package to check.
*
* @return True if the given package is a Namespace.
*/
private static boolean isNamespacePackage(PackageDoc packageDoc) {
return hasAnnotation(packageDoc.annotations(), Namespace.class);
}
/**
* Determine if the given class is an Element.
*
* @param classDoc The class to check.
*
* @return True if the given class is an Element.
*/
private static boolean isElement(ClassDoc classDoc) {
return hasAnnotation(classDoc.annotations(), Element.class);
}
/**
* Determine if the given method is an Attribute.
*
* @param methodDoc The method to check.
*
* @return True if the given method is an Attribute.
*/
private static boolean isAttribute(MethodDoc methodDoc) {
return hasAnnotation(methodDoc.annotations(), Attribute.class);
}
/**
* Make sure the give path exists.
*/
private static boolean makeDirectories(String path) {
// make sure that the base directory exists.
File rootDirectory = new File(path);
try {
System.out.println("Generated namespace document to '"+rootDirectory.getAbsoluteFile()+"'.");
if( rootDirectory.mkdirs() ) {
System.out.println("Directory '"+rootDirectory.getAbsoluteFile()+"' was created for the namespace documentation.");
}
}
catch( SecurityException se ) {
System.out.println("Could not create the namespace documentation root directory.");
se.printStackTrace();
return false;
}
return true;
}
/**
* Create a string of '\t' for the given indent count.
*/
private static String createIndent(int indent) {
StringBuffer indentString = new StringBuffer();
while (indent-- > 0) {
indentString.append("\t");
}
return indentString.toString();
}
/**
* Load the namespace doclet template.
*/
private static Templates loadTemplates()
throws TransformerConfigurationException, IOException, InstantiationException, IllegalAccessException
{
TransformerFactory factory = net.sf.saxon.TransformerFactoryImpl.class.newInstance();
// get the template as a resource.
URLConnection urlConnection = Thread.currentThread().getContextClassLoader().getResource("org/xchain/framework/doclets/namespace-doclet.xsl").openConnection();
try {
StreamSource streamSource = new StreamSource();
streamSource.setInputStream(urlConnection.getInputStream());
Templates templates = factory.newTemplates(streamSource);
return templates;
}
finally {
close(urlConnection.getInputStream());
}
}
private static void writeData( Templates templates, File xdocFile, String data )
throws TransformerException, IOException
{
// create the source.
StreamSource source = new StreamSource();
source.setReader(new StringReader(data));
// create the result.
StreamResult result = new StreamResult();
result.setWriter(new FileWriter(xdocFile));
// create the transformer.
Transformer transformer = templates.newTransformer();
// do the transformation.
transformer.transform( source, result );
}
private static void copyAttributeTypeDoc()
throws IOException
{
URLConnection urlConnection = Thread.currentThread().getContextClassLoader().getResource("org/xchain/framework/doclets/attributetypes.apt").openConnection();
try {
File attributeTypeFile = new File(APT_PATH + "attributetypes.apt");
attributeTypeFile.createNewFile();
FileOutputStream fileOutput = new FileOutputStream(attributeTypeFile);
InputStream input = urlConnection.getInputStream();
int readBytes = -1;
byte[] byteBuffer = new byte[1024];
do {
readBytes = input.read(byteBuffer);
if (readBytes != -1)
fileOutput.write(byteBuffer, 0, readBytes);
} while (readBytes != -1);
fileOutput.close();
} finally {
close(urlConnection.getInputStream());
}
}
private static void close( InputStream inputStream )
{
try {
inputStream.close();
}
catch( IOException ioe ) {
// TODO: log this.
}
}
/**
* Inner class to manage catalogs and commands.
*/
private static class Entry {
private String name;
private StringBuffer content;
private Map<String, Entry> subEntries = new HashMap<String, Entry>();
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public StringBuffer getContent() {
return content;
}
public void setContent(StringBuffer content) {
this.content = content;
}
public void addSubEntry(Entry entry) {
subEntries.put(entry.getName(), entry);
}
public Map<String,Entry> getSubEntries() {
return subEntries;
}
}
}