blob: 87796f4020b2aa2d00b0cd9080ea990f40ab9f64 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.
*/
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.net.MalformedURLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import javax.xml.transform.TransformerException;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.types.FileSet;
import org.apache.xpath.XPathAPI;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import com.thoughtworks.qdox.ant.AbstractQdoxTask;
import com.thoughtworks.qdox.model.DocletTag;
import com.thoughtworks.qdox.model.JavaClass;
import com.thoughtworks.qdox.parser.ParseException;
/**
* Generate documentation for sitemap components based on javadoc tags
* in the source of the component.
*
* This is the first, experimental version - the code is a little bit
* hacky but straight forward :)
*
* @since 2.1.5
* @author <a href="mailto:cziegeler@apache.org">Carsten Ziegeler</a>
* @version CVS $Id$
*/
public final class SitemapTask extends AbstractQdoxTask {
/** The name of the component in the sitemap (required) */
public static final String NAME_TAG = "cocoon.sitemap.component.name";
/** The logger category (optional) */
public static final String LOGGER_TAG = "cocoon.sitemap.component.logger";
/** The label for views (optional) */
public static final String LABEL_TAG = "cocoon.sitemap.component.label";
/** The mime type for serializers and readers (optional) */
public static final String MIMETYPE_TAG = "cocoon.sitemap.component.mimetype";
/** If this tag is specified, the component is not added to the sitemap (optional) */
public static final String HIDDEN_TAG = "cocoon.sitemap.component.hide";
/** If this tag is specified no documentation is generated (optional) */
public static final String NO_DOC_TAG = "cocoon.sitemap.component.documentation.disabled";
/** The documentation (optional) */
public static final String DOC_TAG = "cocoon.sitemap.component.documentation";
/** Configuration (optional) */
public static final String CONF_TAG = "cocoon.sitemap.component.configuration";
/** Caching info (optional) */
public static final String CACHING_INFO_TAG = "cocoon.sitemap.component.documentation.caching";
/** Pooling max (optional) */
public static final String POOL_MAX_TAG = "cocoon.sitemap.component.pooling.max";
private static final String LINE_SEPARATOR = "\n";
/** The sitemap */
private File sitemap;
/** The doc dir */
private File docDir;
/** Cache for classes */
private static Map cache = new HashMap();
/** The directory containing the sources*/
private String directory;
/** The name of the block */
protected String blockName;
/** Is this block deprecated? */
protected boolean deprecated = false;
/** Is this block stable? */
protected boolean stable = true;
/**
* Set the directory containing the source files.
* Only .java files will be scanned
*/
public void setSource(File dir) {
try {
this.directory = dir.toURL().toExternalForm();
if ( !dir.isDirectory() ) {
throw new BuildException("Source is not a directory.");
}
} catch (IOException ioe) {
throw new BuildException(ioe);
}
FileSet set = new FileSet();
set.setDir(dir);
set.setIncludes("**/*.java");
super.addFileset(set);
}
/**
* The location of the sitemap
*/
public void setSitemap( final File sitemap ) {
this.sitemap = sitemap;
}
/**
* The location of the documentation files
*/
public void setDocDir( final File dir ) {
this.docDir = dir;
}
/**
* Set the block name
*/
public void setBlock(String value) {
this.blockName = value;
}
/**
* Is the block deprecated?
*/
public void setDeprecated(boolean value) {
this.deprecated = value;
}
/**
* Is the block stable?
*/
public void setStable(boolean value) {
this.stable = value;
}
/**
* Determine the type of the class
*/
private String classType(JavaClass clazz) {
if ( clazz.isA(GENERATOR) ) {
return "generator";
} else if ( clazz.isA(TRANSFORMER) ) {
return "transformer";
} else if ( clazz.isA(READER) ) {
return "reader";
} else if ( clazz.isA(SERIALIZER) ) {
return "serializer";
} else if ( clazz.isA(ACTION) ) {
return "action";
} else if ( clazz.isA(MATCHER) ) {
return "matcher";
} else if ( clazz.isA(SELECTOR) ) {
return "selector";
} else if ( clazz.isA(PIPELINE) ) {
return "pipe";
// Should qdox resolve recursively? ie: HTMLGenerator isA ServiceableGenerator isA AbstractGenerator isA Generator
} else if ( clazz.getPackage().equals("org.apache.cocoon.generation") && (clazz.isA("Generator") || clazz.isA("ServiceableGenerator")) ) {
return "generator";
} else if ( clazz.isA("org.apache.cocoon.generation.ServiceableGenerator") ) {
return "generator";
} else if ( clazz.getPackage().equals("org.apache.cocoon.transformation") && clazz.isA("Transformer") ) {
return "transformer";
} else if ( clazz.getPackage().equals("org.apache.cocoon.reading") && clazz.isA("Reader") ) {
return "reader";
} else if ( clazz.getPackage().equals("org.apache.cocoon.serialization") && clazz.isA("Serializer") ) {
return "serializer";
} else if ( clazz.getPackage().equals("org.apache.cocoon.acting") && clazz.isA("Action") ) {
return "action";
} else if ( clazz.getPackage().equals("org.apache.cocoon.matching") && clazz.isA("Matcher") ) {
return "matcher";
} else if ( clazz.getPackage().equals("org.apache.cocoon.selection") && clazz.isA("Selector") ) {
return "selector";
} else if ( clazz.getPackage().equals("org.apache.cocoon.components.pipeline") && clazz.isA("ProcessingPipeline") ) {
return "pipe";
} else {
return null;
}
}
/**
* Execute generator task.
*
* @throws BuildException if there was a problem collecting the info
*/
public void execute()
throws BuildException {
validate();
List components = (List)cache.get(this.directory);
if ( components == null ) {
// this does the hard work :)
try {
super.execute();
}
catch (ParseException pe) {
log("ParseException: " + pe);
}
components = this.collectInfo();
cache.put(this.directory, components);
}
try {
if ( this.sitemap != null ) {
this.processSitemap(components);
}
if ( this.docDir != null ) {
this.processDocDir(components);
}
} catch ( final BuildException e ) {
throw e;
} catch ( final Exception e ) {
throw new BuildException( e.toString(), e );
}
}
/**
* Validate that the parameters are valid.
*/
private void validate() {
if ( this.directory == null ) {
throw new BuildException("Source is not specified.");
}
if ( this.sitemap == null && this.docDir == null ) {
throw new BuildException("Sitemap or DocDir is not specified.");
}
if ( this.sitemap != null && this.sitemap.isDirectory() ) {
throw new BuildException( "Sitemap (" + this.sitemap + ") is not a file." );
}
if ( this.docDir != null && !this.docDir.isDirectory() ) {
throw new BuildException( "DocDir (" + this.docDir + ") is not a directory." );
}
}
/**
* Collect the component information
*/
private List collectInfo() {
log("Collecting sitemap components info");
final List components = new ArrayList();
final List allComponentNames = new ArrayList();
final Iterator it = super.allClasses.iterator();
while ( it.hasNext() ) {
final JavaClass javaClass = (JavaClass) it.next();
final DocletTag tag = javaClass.getTagByName( NAME_TAG );
if (classType(javaClass) != null) {
allComponentNames.add(javaClass.getFullyQualifiedName());
}
if ( null != tag ) {
final SitemapComponent comp = new SitemapComponent( javaClass );
log("Found component: " + comp, Project.MSG_DEBUG);
components.add(comp);
}
}
// Generate a list of all sitemap components
final String fileSeparator = System.getProperty("file.separator", "/");
final String matchString = "blocks" + fileSeparator;
final String outputFname = "build" + fileSeparator + "all-sitemap-components" +
(directory.endsWith(matchString) ? "-blocks" : "") + ".txt";
log("Listing all sitemap components to " + outputFname);
try {
File outputFile = new File(outputFname);
FileWriter out = new FileWriter(outputFile);
final String lineSeparator = System.getProperty("line.separator", "\n");
final Iterator iter = allComponentNames.iterator();
while ( iter.hasNext() ) {
out.write((String)iter.next());
out.write(lineSeparator);
}
out.close();
}
catch (IOException ioe) {
log("IOException: " + ioe);
}
return components;
}
/**
* Add components to sitemap
*/
private void processSitemap(List components)
throws Exception {
log("Adding sitemap components");
Document document;
document = DocumentCache.getDocument(this.sitemap, this);
boolean changed = false;
Iterator iter = components.iterator();
while ( iter.hasNext() ) {
SitemapComponent component = (SitemapComponent)iter.next();
final String type = component.getType();
final String section = type + 's';
NodeList nodes = XPathAPI.selectNodeList(document, "/sitemap/components/" + section);
if (nodes.getLength() != 1 ) {
throw new BuildException("Unable to find section for component type " + type);
}
// remove old node!
NodeList oldNodes = XPathAPI.selectNodeList(document,
"/sitemap/components/" + section + '/' + type + "[@name='" + component.getName() + "']");
for(int i=0; i < oldNodes.getLength(); i++ ) {
final Node node = oldNodes.item(i);
node.getParentNode().removeChild(node);
}
// and add it again
if (component.append(nodes.item(0)) ) {
changed = true;
}
}
if ( changed ) {
DocumentCache.writeDocument(this.sitemap, document, this);
}
DocumentCache.storeDocument(this.sitemap, document, this);
}
/**
* Add components to sitemap
*/
private void processDocDir(List components)
throws Exception {
log("Generating documentation");
Iterator iter = components.iterator();
while ( iter.hasNext() ) {
final SitemapComponent component = (SitemapComponent)iter.next();
// Read template
final File templateFile = this.getProject().resolveFile("src/documentation/templates/sitemap-component.xml");
Document template = DocumentCache.getDocument(templateFile, this);
// create directory - if required
final File componentsDir = new File(this.docDir, component.getType()+'s');
componentsDir.mkdir();
// get file name
String fileName = component.getName() + "-" + component.getType() + ".xml";
if ( this.blockName != null ) {
fileName = this.blockName + "-" + fileName;
}
final File docFile = new File(componentsDir, fileName);
// generate the doc
component.generateDocs(template, docFile, this.getProject());
// generate the index
final File indexFile = new File(componentsDir, "book.xml");
final Document indexDoc = DocumentCache.getDocument(indexFile, this);
final String section;
if ( this.blockName == null ) {
section = "Core";
} else {
section = this.blockName + " Block";
}
Node sectionNode = XPathAPI.selectSingleNode(indexDoc, "/book/menu[@label='"+section+"']");
if ( sectionNode == null ) {
sectionNode = indexDoc.createElement("menu");
((Element)sectionNode).setAttribute("label", section);
indexDoc.getDocumentElement().appendChild(sectionNode);
}
final String htmlName = docFile.getName().substring(0, docFile.getName().length()-3) + "html";
Node oldEntry = XPathAPI.selectSingleNode(sectionNode, "menu-item[@href='"+htmlName+"']");
Node newEntry = indexDoc.createElement("menu-item");
((Element)newEntry).setAttribute("href", htmlName);
final String label = capitalize(component.getName()) + " " + capitalize(component.getType());
((Element)newEntry).setAttribute("label", label);
if ( oldEntry != null ) {
oldEntry.getParentNode().replaceChild(newEntry, oldEntry);
} else {
Node nextLabel = null;
final NodeList childs = sectionNode.getChildNodes();
int i = 0;
while ( nextLabel == null && i < childs.getLength() ) {
final Node current = childs.item(i);
if ( current instanceof Element ) {
final String currentLabel = ((Element)current).getAttribute("label");
if ( label.compareTo(currentLabel) < 0 ) {
nextLabel = current;
}
}
i++;
}
if ( nextLabel == null ) {
sectionNode.appendChild(newEntry);
} else {
sectionNode.insertBefore(newEntry, nextLabel);
}
}
DocumentCache.writeDocument(indexFile, indexDoc, this);
}
}
/**
* Helper method to capitalize a string.
* This is taken from commons-lang, but we don't want the dependency
* right now!
*/
public static String capitalize(String str) {
int strLen;
if (str == null || (strLen = str.length()) == 0) {
return str;
}
return new StringBuffer(strLen)
.append(Character.toTitleCase(str.charAt(0)))
.append(str.substring(1))
.toString();
}
final class SitemapComponent {
final protected JavaClass javaClass;
final String name;
final String type;
public SitemapComponent(JavaClass javaClass) {
this.javaClass = javaClass;
this.name = this.javaClass.getTagByName( NAME_TAG ).getValue();
// TEST CODE
System.out.println("Name: " + this.name);
System.out.println("className: " + this.javaClass.getName());
System.out.println();
JavaClass[] jc = this.javaClass.getImplementedInterfaces();
if (jc.length> 0) {
System.out.println("Implemented interfaces:");
}
for (int i = 0; i < jc.length; i++) {
System.out.println(jc[i].getName() + ". Full name: " + jc[i].getFullyQualifiedName());
}
System.out.println("==== END of implements ===");
// END TEST CODE
this.type = classType(this.javaClass);
}
/* (non-Javadoc)
* @see java.lang.Object#toString()
*/
public String toString() {
return "Sitemap component: " + this.javaClass.getName();
}
public String getType() {
return this.type;
}
public String getName() {
return this.name;
}
public boolean append(Node parent) {
if ( this.getTagValue(HIDDEN_TAG, null) != null ) {
return false;
}
Document doc = parent.getOwnerDocument();
Node node;
// first check: deprecated?
if ( SitemapTask.this.deprecated || this.getTagValue("deprecated", null) != null ) {
indent(parent, 3);
StringBuffer buffer = new StringBuffer("The ");
buffer.append(this.type)
.append(" ")
.append(this.name)
.append(" is deprecated");
node = doc.createComment(buffer.toString());
parent.appendChild(node);
newLine(parent);
}
// unstable block?
if ( !SitemapTask.this.stable ) {
indent(parent, 3);
StringBuffer buffer = new StringBuffer("The ");
buffer.append(this.type)
.append(" ")
.append(this.name)
.append(" is in an unstable block");
node = doc.createComment(buffer.toString());
parent.appendChild(node);
newLine(parent);
}
indent(parent, 3);
node = doc.createElement("map:" + this.type);
((Element)node).setAttribute("name", this.name);
((Element)node).setAttribute("src", this.javaClass.getFullyQualifiedName());
// test for logger
// TODO Default logger?
if ( this.javaClass.isA(LOG_ENABLED) ) {
this.addAttribute(node, LOGGER_TAG, "logger", null);
}
// test for label
this.addAttribute(node, LABEL_TAG, "label", null);
// pooling?
if ( this.javaClass.isA(POOLABLE) ) {
// TODO - Think about default values
this.addAttribute(node, POOL_MAX_TAG, "pool-max", null);
}
// mime-type
if ( this.javaClass.isA(OUTPUT_COMPONENT) ) {
this.addAttribute(node, MIMETYPE_TAG, "mime-type", null);
}
// append node
parent.appendChild(node);
newLine(parent);
// add configuration
String configuration = this.getTagValue(CONF_TAG, null);
if ( configuration != null ) {
configuration = "<root>" + configuration + "</root>";
final Document confDoc = DocumentCache.getDocument(configuration, null);
setValue(node, null, confDoc.getDocumentElement().getChildNodes());
}
return true;
}
private void addAttribute(Node node, String tag, String attributeName, String defaultValue) {
final String tagValue = this.getTagValue(tag, defaultValue);
if ( tagValue != null ) {
((Element)node).setAttribute(attributeName, tagValue);
}
}
private void newLine(Node node) {
final Node n = node.getOwnerDocument().createTextNode(LINE_SEPARATOR);
node.appendChild(n);
}
private void indent(Node node, int depth) {
final StringBuffer buffer = new StringBuffer();
for(int i=0; i < depth*2; i++ ) {
buffer.append(' ');
}
final Node n = node.getOwnerDocument().createTextNode(buffer.toString());
node.appendChild(n);
}
public void generateDocs(Document template, File docFile, Project project)
throws TransformerException {
final String doc = this.getDocumentation();
if ( doc == null ) {
if ( docFile.exists() ) {
docFile.delete();
}
return;
}
// get body from template
final Node body = XPathAPI.selectSingleNode(template, "/document/body");
// append root element and surrounding paragraph
final String description = "<root><p>" + doc + "</p></root>";
String systemURI = null;
try {
systemURI = docFile.toURL().toExternalForm();
} catch (MalformedURLException mue) {
// we ignore this
}
final Document descriptionDoc = DocumentCache.getDocument(description, systemURI);
// Title
setValue(template, "/document/header/title",
"Description of the " + this.name + " " + this.type);
// Version
setValue(template, "/document/header/version",
project.getProperty("version"));
// Description
setValue(body, "s1[@title='Description']",
descriptionDoc.getDocumentElement().getChildNodes());
// check: deprecated?
if ( SitemapTask.this.deprecated || this.getTagValue("deprecated", null) != null ) {
Node node = XPathAPI.selectSingleNode(body, "s1[@title='Description']");
// node is never null - this is ensured by the test above
Element e = node.getOwnerDocument().createElement("note");
node.appendChild(e);
e.appendChild(node.getOwnerDocument().createTextNode("This component is deprecated."));
final String info = this.getTagValue("deprecated", null);
if ( info != null ) {
e.appendChild(node.getOwnerDocument().createTextNode(info));
}
}
// check: stable?
if ( !SitemapTask.this.stable ) {
Node node = XPathAPI.selectSingleNode(body, "s1[@title='Description']");
// node is never null - this is ensured by the test above
Element e = node.getOwnerDocument().createElement("note");
node.appendChild(e);
e.appendChild(node.getOwnerDocument().createTextNode("This component is in an unstable block."));
}
// Info Table
final Node tableNode = XPathAPI.selectSingleNode(body, "s1[@title='Info']/table");
// Info - Name
this.addRow(tableNode, "Name", this.name);
// Info - Block
if ( SitemapTask.this.blockName != null ) {
this.addRow(tableNode, "Block", SitemapTask.this.blockName);
}
// Info - Cacheable
if ( this.javaClass.isA(GENERATOR)
|| this.javaClass.isA(TRANSFORMER)
|| this.javaClass.isA(SERIALIZER)
|| this.javaClass.isA(READER)) {
String cacheInfo;
if ( this.javaClass.isA(CACHEABLE) ) {
cacheInfo = this.getTagValue(CACHING_INFO_TAG, null);
if ( cacheInfo != null ) {
cacheInfo = "Yes - " + cacheInfo;
} else {
cacheInfo = "Yes";
}
} else if ( this.javaClass.isA(DEPRECATED_CACHEABLE) ) {
cacheInfo = this.getTagValue(CACHING_INFO_TAG, null);
if ( cacheInfo != null ) {
cacheInfo = "Yes (2.0 Caching) - " + cacheInfo;
} else {
cacheInfo = "Yes (2.0 Caching)";
}
} else {
cacheInfo = "No";
}
this.addRow(tableNode, "Cacheable", cacheInfo);
}
// Info - mime-type
if ( this.javaClass.isA(OUTPUT_COMPONENT) ) {
final String value = this.getTagValue(MIMETYPE_TAG, "-");
this.addRow(tableNode, "Mime-Type", value);
}
// Info - Class
this.addRow(tableNode, "Class", this.javaClass.getFullyQualifiedName());
// merge with old doc
this.merge(body, docFile);
// finally write the doc
DocumentCache.writeDocument(docFile, template, SitemapTask.this);
}
/**
* Merge the sections of the old document with the new generated one.
* All sections (s1) of the old document are added to the generated one
* if not a section with the same title exists.
*/
private void merge(Node body, File docFile) throws TransformerException {
final Document mergeDocument;
try {
mergeDocument = DocumentCache.getDocument(docFile, SitemapTask.this);
} catch (Exception ignore) {
return;
}
NodeList sections = XPathAPI.selectNodeList(mergeDocument, "/document/body/s1");
if ( sections != null ) {
for(int i=0; i<sections.getLength(); i++) {
final Element current = (Element)sections.item(i);
final String title = current.getAttribute("title");
// is this section not in the template?
if (XPathAPI.selectSingleNode(body, "s1[@title='"+title+"']") == null ) {
body.appendChild(body.getOwnerDocument().importNode(current, true));
}
}
}
}
/**
* Return the documentation or null
* @return
*/
private String getDocumentation() {
if ( this.getTagValue(NO_DOC_TAG, null) != null ) {
return null;
}
return this.getTagValue(DOC_TAG, null);
}
private String getTagValue(String tagName, String defaultValue) {
final DocletTag tag = this.javaClass.getTagByName( tagName );
if ( tag != null ) {
return tag.getValue();
}
return defaultValue;
}
private void addRow(Node table, String title, String value) {
final Element row = table.getOwnerDocument().createElement("tr");
final Element firstColumn = table.getOwnerDocument().createElement("td");
firstColumn.appendChild(table.getOwnerDocument().createTextNode(title));
final Element secondColumn = table.getOwnerDocument().createElement("td");
secondColumn.appendChild(table.getOwnerDocument().createTextNode(value));
row.appendChild(firstColumn);
row.appendChild(secondColumn);
table.appendChild(row);
}
private void setValue(Node node, String xpath, String value) {
try {
final Node insertNode = (xpath == null ? node : XPathAPI.selectSingleNode(node, xpath));
if ( insertNode == null ) {
throw new BuildException("Node (" + xpath + ") not found.");
}
Node text = insertNode.getOwnerDocument().createTextNode(value);
while (insertNode.hasChildNodes() ) {
insertNode.removeChild(insertNode.getFirstChild());
}
insertNode.appendChild(text);
} catch (TransformerException e) {
throw new BuildException(e);
}
}
private void setValue(Node node, String xpath, NodeList nodes) {
try {
final Node insertNode = (xpath == null ? node : XPathAPI.selectSingleNode(node, xpath));
if ( insertNode == null ) {
throw new BuildException("Node (" + xpath + ") not found.");
}
while (insertNode.hasChildNodes() ) {
insertNode.removeChild(insertNode.getFirstChild());
}
for(int i=0; i<nodes.getLength(); i++) {
final Node current = nodes.item(i);
insertNode.appendChild(insertNode.getOwnerDocument().importNode(current, true));
}
} catch (TransformerException e) {
throw new BuildException(e);
}
}
}
// Class Constants
private static final String LOG_ENABLED = "org.apache.avalon.framework.logger.LogEnabled";
private static final String POOLABLE = "org.apache.avalon.excalibur.pool.Poolable";
private static final String CACHEABLE = "org.apache.cocoon.caching.CacheableProcessingComponent";
private static final String DEPRECATED_CACHEABLE = "org.apache.cocoon.caching.Cacheable";
private static final String OUTPUT_COMPONENT = "org.apache.cocoon.sitemap.SitemapOutputComponent";
private static final String GENERATOR = "org.apache.cocoon.generation.Generator";
private static final String TRANSFORMER = "org.apache.cocoon.transformation.Transformer";
private static final String SERIALIZER = "org.apache.cocoon.serialization.Serializer";
private static final String READER = "org.apache.cocoon.reading.Reader";
private static final String MATCHER = "org.apache.cocoon.matching.Matcher";
private static final String SELECTOR = "org.apache.cocoon.selection.Selector";
private static final String ACTION = "org.apache.cocoon.acting.Action";
private static final String PIPELINE = "org.apache.cocoon.components.pipeline.ProcessingPipeline";
}