blob: 8ceb475ae5c8816ac804349bc850cbb66c32f000 [file] [log] [blame]
// Copyright 2007, 2008 The Apache Software Foundation
//
// 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.apache.tapestry5.mojo;
import nu.xom.*;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.SystemUtils;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.repository.ArtifactRepository;
import org.apache.maven.model.Resource;
import org.apache.maven.project.MavenProject;
import org.apache.maven.reporting.AbstractMavenReport;
import org.apache.maven.reporting.MavenReportException;
import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
import static org.apache.tapestry5.ioc.internal.util.CollectionFactory.newList;
import static org.apache.tapestry5.ioc.internal.util.CollectionFactory.newMap;
import org.apache.tapestry5.ioc.internal.util.InternalUtils;
import org.codehaus.doxia.sink.Sink;
import org.codehaus.doxia.site.renderer.SiteRenderer;
import org.codehaus.plexus.util.cli.CommandLineException;
import org.codehaus.plexus.util.cli.CommandLineUtils;
import org.codehaus.plexus.util.cli.Commandline;
import org.codehaus.plexus.util.cli.DefaultConsumer;
import java.io.*;
import java.util.*;
/**
* The component report generates documentation about components and parameters within the current project.
*
* @goal component-report
* @requiresDependencyResolution compile
* @execute phase="generate-sources"
*/
public class ComponentReport extends AbstractMavenReport
{
/**
* Subdirectory containing the component reference pages and index.
*/
private static final String REFERENCE_DIR = "ref";
private final static String[] PARAMETER_HEADERS = { "Name", "Type", "Flags", "Default", "Default Prefix",
"Description" };
/**
* Identifies the application root package.
*
* @parameter
* @required
*/
private String rootPackage;
/**
* The Maven Project Object
*
* @parameter expression="${project}"
* @required
* @readonly
*/
private MavenProject project;
/**
* Generates the site report
*
* @component
*/
private SiteRenderer siteRenderer;
/**
* Location of the generated site.
*
* @parameter default-value="${project.reporting.outputDirectory}"
* @required
*/
private String outputDirectory;
/**
* @parameter expression="${project.build.directory}/generated-site/xdoc"
* @required
*/
private File generatedDocsDirectory;
/**
* @parameter expression="${project.build.directory}/generated-site/resources"
* @required
*/
private File generatedResourcesDirectory;
/**
* Working directory for temporary files.
*
* @parameter default-value="target"
* @required
*/
private String workDirectory;
/**
* Relative path from the generated report to the API documentation (Javadoc). Defaults to "apidocs" but will often
* be changed to "../apidocs" when documentation is created at the project level.
*
* @parameter default-value="apidocs"
* @required
*/
private String apidocs;
@Override
protected String getOutputDirectory()
{
return outputDirectory;
}
@Override
protected MavenProject getProject()
{
return project;
}
@Override
protected SiteRenderer getSiteRenderer()
{
return siteRenderer;
}
public String getDescription(Locale locale)
{
return "Tapestry component parameter reference documentation";
}
public String getName(Locale locale)
{
return "Component Reference";
}
public String getOutputName()
{
return REFERENCE_DIR + "/index";
}
private final static Set<String> SUPPORTED_SUBPACKAGES = CollectionFactory.newSet("base", "components", "mixins",
"pages");
/**
* Generates the report; this consist of the index page
*
* @param locale
* @throws MavenReportException
*/
@Override
protected void executeReport(Locale locale) throws MavenReportException
{
Map<String, ClassDescription> descriptions = runJavadoc();
getLog().info("Generating reference pages ...");
try
{
File refDir = new File(generatedDocsDirectory, REFERENCE_DIR);
refDir.mkdirs();
List<File> docSearchPath = createDocSearchPath();
Sink sink = getSink();
sink.head();
sink.title();
sink.text("Component Reference");
sink.title_();
sink.head_();
sink.section1();
sink.sectionTitle1();
sink.text("Component Reference");
sink.sectionTitle1_();
sink.list();
String currentSubpackage = null;
for (String className : InternalUtils.sortedKeys(descriptions))
{
String subpackage = extractSubpackage(className);
if (!SUPPORTED_SUBPACKAGES.contains(subpackage)) continue;
if (!subpackage.equals(currentSubpackage))
{
if (currentSubpackage != null)
{
sink.list_();
sink.section2_();
}
sink.section2();
sink.sectionTitle2();
sink.text(StringUtils.capitalize(subpackage));
sink.sectionTitle2_();
sink.list();
currentSubpackage = subpackage;
}
sink.listItem();
sink.link(toPath(className) + ".html");
sink.text(className);
sink.link_();
writeClassDescription(descriptions, refDir, docSearchPath, className);
sink.listItem_();
}
if (currentSubpackage != null)
{
sink.list_();
sink.section2_();
}
}
catch (Exception ex)
{
throw new MavenReportException(ex.getMessage(), ex);
}
}
private String toPath(String className)
{
return className.replace('.', '/');
}
private String extractSubpackage(String className)
{
int dotx = className.indexOf(".", rootPackage.length() + 1);
// For classes directly in the root package.
if (dotx < 1) return "";
return className.substring(rootPackage.length() + 1, dotx);
}
private List<File> createDocSearchPath()
{
List<File> result = CollectionFactory.newList();
for (String sourceRoot : (List<String>) project.getCompileSourceRoots())
{
result.add(new File(sourceRoot));
}
for (Resource r : (List<Resource>) project.getResources())
{
String dir = r.getDirectory();
result.add(new File(dir));
}
return result;
}
private void writeClassDescription(Map<String, ClassDescription> descriptions, File refDir,
List<File> docSearchPath, String className) throws Exception
{
int dotx = className.lastIndexOf('.');
String packageName = className.substring(0, dotx);
File outputDir = new File(refDir, toPath(packageName));
outputDir.mkdirs();
File outputFile = new File(refDir, toPath(className) + ".xml");
Element root = new Element("document");
ClassDescription cd = descriptions.get(className);
Map<String, ParameterDescription> parameters = newMap(cd.getParameters());
List<String> parents = newList();
String current = cd.getSuperClassName();
while (true)
{
ClassDescription superDescription = descriptions.get(current);
if (superDescription == null) break;
parents.add(current);
parameters.putAll(superDescription.getParameters());
current = superDescription.getSuperClassName();
}
Collections.reverse(parents);
// XOM is pretty verbose; it really needs a builder/fluent interface.
Element properties = addChild(root, "properties");
addChild(properties, "title", String.format("Component Reference: %s", className));
Element body = new Element("body");
root.appendChild(body);
Element section = addSection(body, className);
addChild(section, "p", cd.getDescription());
StringBuilder javadocURL = new StringBuilder(200);
int depth = packageName.split("\\.").length;
for (int i = 0; i < depth; i++)
{
javadocURL.append("../");
}
String pathToRefRoot = javadocURL.toString();
javadocURL.append("../").append(apidocs).append("/").append(toPath(className)).append(".html");
addLink(addChild(section, "p"), javadocURL.toString(), "[JavaDoc]");
if (!parents.isEmpty())
{
section = addSection(body, "Component Inheritance");
Element container = section;
for (String name : parents)
{
Element ul = addChild(container, "ul");
Element li = addChild(ul, "li");
addLink(li, name + ".html", name);
container = li;
}
addChild(addChild(container, "ul"), "li", className);
}
if (!parameters.isEmpty())
{
section = addSection(body, "Component Parameters");
Element table = new Element("table");
section.appendChild(table);
Element headerRow = new Element("tr");
table.appendChild(headerRow);
for (String header : PARAMETER_HEADERS)
addChild(headerRow, "th", header);
List<String> flags = newList();
for (String name : InternalUtils.sortedKeys(parameters))
{
ParameterDescription pd = parameters.get(name);
flags.clear();
if (pd.getRequired()) flags.add("Required");
if (!pd.getCache()) flags.add("NOT Cached");
Element row = new Element("tr");
table.appendChild(row);
addChild(row, "td", pd.getName());
addChild(row, "td", pd.getType());
addChild(row, "td", InternalUtils.join(flags));
addChild(row, "td", pd.getDefaultValue());
addChild(row, "td", pd.getDefaultPrefix());
addChild(row, "td", pd.getDescription());
}
}
addExternalDocumentation(body, docSearchPath, className);
addChild(body, "hr");
addLink(addChild(body, "p"), pathToRefRoot + "index.html", "Back to index");
Document document = new Document(root);
getLog().info(String.format("Writing %s", outputFile));
FileOutputStream fos = new FileOutputStream(outputFile);
BufferedOutputStream bos = new BufferedOutputStream(fos);
PrintWriter writer = new PrintWriter(bos);
writer.print(document.toXML());
writer.close();
}
private void addExternalDocumentation(Element body, List<File> docSearchPath, String className)
throws ParsingException, IOException
{
String classNamePath = toPath(className);
String pathExtension = classNamePath + ".xdoc";
for (File path : docSearchPath)
{
File file = new File(path, pathExtension);
getLog().debug(String.format("Checking for %s", file));
if (!file.exists()) continue;
getLog().info(String.format("Reading extra documentation from %s", file));
Builder builder = new Builder();
Document doc = builder.build(file);
// Transfer the nodes inside document/body into our body
Element incomingBody = doc.getRootElement().getFirstChildElement("body");
for (int i = 0; i < incomingBody.getChildCount(); i++)
{
Node incoming = incomingBody.getChild(i).copy();
body.appendChild(incoming);
}
Nodes nodes = doc.query("//img/@src");
int lastslashx = classNamePath.lastIndexOf('/');
String packagePath = classNamePath.substring(0, lastslashx);
File generatedRefRoot = new File(generatedResourcesDirectory, REFERENCE_DIR);
File generatedPackageRoot = new File(generatedRefRoot, packagePath);
for (int i = 0; i < nodes.size(); i++)
{
Node src = nodes.get(i);
String srcPath = src.getValue();
File imgFile = new File(path, packagePath + "/" + srcPath);
File imgTargetFile = new File(generatedPackageRoot, srcPath);
copy(imgFile, imgTargetFile);
}
return;
}
}
private void copy(File sourceFile, File targetFile) throws IOException
{
getLog().info(String.format("Copying image file %s to %s", sourceFile, targetFile));
targetFile.getParentFile().mkdirs();
byte[] buffer = new byte[20000];
InputStream in = new BufferedInputStream(new FileInputStream(sourceFile));
OutputStream out = new BufferedOutputStream(new FileOutputStream(targetFile));
while (true)
{
int length = in.read(buffer);
if (length < 0) break;
out.write(buffer, 0, length);
}
in.close();
out.close();
}
private Map<String, ClassDescription> runJavadoc() throws MavenReportException
{
getLog().info("Running JavaDoc to collect component parameter data ...");
Commandline command = new Commandline();
try
{
command.setExecutable(pathToJavadoc());
}
catch (IOException ex)
{
throw new MavenReportException("Unable to locate javadoc command: " + ex.getMessage(), ex);
}
String parametersPath = workDirectory + File.separator + "component-parameters.xml";
String[] arguments = { "-private", "-o", parametersPath,
"-subpackages", rootPackage,
"-doclet", ParametersDoclet.class.getName(),
"-docletpath", docletPath(),
"-sourcepath", sourcePath(),
"-classpath", classPath() };
command.addArguments(arguments);
executeCommand(command);
return readXML(parametersPath);
}
@SuppressWarnings("unchecked")
private String sourcePath()
{
List<String> roots = project.getCompileSourceRoots();
return toArgumentPath(roots);
}
/**
* Needed to help locate this plugin's local JAR file for the -doclet argument.
*
* @parameter default-value="${localRepository}"
* @read-only
*/
private ArtifactRepository localRepository;
/**
* Needed to help locate this plugin's local JAR file for the -doclet argument.
*
* @parameter default-value="${plugin.groupId}"
* @read-only
*/
private String pluginGroupId;
/**
* Needed to help locate this plugin's local JAR file for the -doclet argument.
*
* @parameter default-value="${plugin.artifactId}"
* @read-only
*/
private String pluginArtifactId;
/**
* Needed to help locate this plugin's local JAR file for the -doclet argument.
*
* @parameter default-value="${plugin.version}"
* @read-only
*/
private String pluginVersion;
@SuppressWarnings("unchecked")
private String docletPath() throws MavenReportException
{
File file = new File(localRepository.getBasedir());
for (String term : pluginGroupId.split("\\."))
file = new File(file, term);
file = new File(file, pluginArtifactId);
file = new File(file, pluginVersion);
file = new File(file, String.format("%s-%s.jar", pluginArtifactId, pluginVersion));
return file.getAbsolutePath();
}
@SuppressWarnings("unchecked")
private String classPath() throws MavenReportException
{
List<Artifact> artifacts = project.getCompileArtifacts();
return artifactsToArgumentPath(artifacts);
}
private String artifactsToArgumentPath(List<Artifact> artifacts) throws MavenReportException
{
List<String> paths = newList();
for (Artifact artifact : artifacts)
{
if (artifact.getScope().equals("test")) continue;
File file = artifact.getFile();
if (file == null) throw new MavenReportException(
"Unable to execute Javadoc: compile dependencies are not fully resolved.");
paths.add(file.getAbsolutePath());
}
return toArgumentPath(paths);
}
private void executeCommand(Commandline command) throws MavenReportException
{
getLog().debug(command.toString());
CommandLineUtils.StringStreamConsumer err = new CommandLineUtils.StringStreamConsumer();
try
{
int exitCode = CommandLineUtils.executeCommandLine(command, new DefaultConsumer(), err);
if (exitCode != 0)
{
String message = String.format("Javadoc exit code: %d - %s\nCommand line was: %s", exitCode,
err.getOutput(), command);
throw new MavenReportException(message);
}
}
catch (CommandLineException ex)
{
throw new MavenReportException("Unable to execute javadoc command: " + ex.getMessage(), ex);
}
// ----------------------------------------------------------------------
// Handle Javadoc warnings
// ----------------------------------------------------------------------
if (StringUtils.isNotEmpty(err.getOutput()))
{
getLog().info("Javadoc Warnings");
StringTokenizer token = new StringTokenizer(err.getOutput(), "\n");
while (token.hasMoreTokens())
{
String current = token.nextToken().trim();
getLog().warn(current);
}
}
}
private String pathToJavadoc() throws IOException, MavenReportException
{
String executableName = SystemUtils.IS_OS_WINDOWS ? "javadoc.exe" : "javadoc";
File executable = initialGuessAtJavadocFile(executableName);
if (!executable.exists() || !executable.isFile())
throw new MavenReportException(String.format("Path %s does not exist or is not a file.", executable));
return executable.getAbsolutePath();
}
private File initialGuessAtJavadocFile(String executableName)
{
if (SystemUtils.IS_OS_MAC_OSX)
return new File(SystemUtils.getJavaHome() + File.separator + "bin", executableName);
return new File(SystemUtils.getJavaHome() + File.separator + ".." + File.separator + "bin", executableName);
}
private String toArgumentPath(List<String> paths)
{
StringBuilder builder = new StringBuilder();
String sep = "";
for (String path : paths)
{
builder.append(sep);
builder.append(path);
sep = SystemUtils.PATH_SEPARATOR;
}
return builder.toString();
}
public Map<String, ClassDescription> readXML(String path) throws MavenReportException
{
try
{
Builder builder = new Builder(false);
File input = new File(path);
Document doc = builder.build(input);
return buildMapFromDocument(doc);
}
catch (Exception ex)
{
throw new MavenReportException(String.format("Failure reading from %s: %s", path, ex
.getMessage()), ex);
}
}
private Map<String, ClassDescription> buildMapFromDocument(Document doc)
{
Map<String, ClassDescription> result = newMap();
Elements elements = doc.getRootElement().getChildElements("class");
for (int i = 0; i < elements.size(); i++)
{
Element element = elements.get(i);
String description = element.getFirstChildElement("description").getValue();
String className = element.getAttributeValue("name");
String superClassName = element.getAttributeValue("super-class");
ClassDescription cd = new ClassDescription(className, superClassName, description);
result.put(className, cd);
readParameters(cd, element);
}
return result;
}
private void readParameters(ClassDescription cd, Element classElement)
{
Elements elements = classElement.getChildElements("parameter");
for (int i = 0; i < elements.size(); i++)
{
Element node = elements.get(i);
String name = node.getAttributeValue("name");
String type = node.getAttributeValue("type");
int dotx = type.lastIndexOf('.');
if (dotx > 0 && type.substring(0, dotx).equals("java.lang")) type = type.substring(dotx + 1);
String defaultValue = node.getAttributeValue("default");
boolean required = Boolean.parseBoolean(node.getAttributeValue("required"));
boolean cache = Boolean.parseBoolean(node.getAttributeValue("cache"));
String defaultPrefix = node.getAttributeValue("default-prefix");
String description = node.getValue();
ParameterDescription pd = new ParameterDescription(name, type, defaultValue, defaultPrefix, required, cache,
description);
cd.getParameters().put(name, pd);
}
}
private Element addSection(Element container, String name)
{
Element section = new Element("section");
container.appendChild(section);
section.addAttribute(new Attribute("name", name));
return section;
}
private Element addLink(Element container, String URL, String text)
{
Element link = addChild(container, "a", text);
link.addAttribute(new Attribute("href", URL));
return link;
}
private Element addChild(Element container, String elementName)
{
Element child = new Element(elementName);
container.appendChild(child);
return child;
}
private Element addChild(Element container, String elementName, String text)
{
Element child = new Element(elementName);
container.appendChild(child);
child.appendChild(text);
return child;
}
}