You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tapestry.apache.org by hl...@apache.org on 2007/02/18 20:38:33 UTC

svn commit: r508958 - in /tapestry/tapestry5/tapestry-component-report/trunk/src/main/java/org/apache/tapestry: ./ mojo/ mojo/ClassDescription.java mojo/ComponentReport.java mojo/ParameterDescription.java mojo/ParametersDoclet.java

Author: hlship
Date: Sun Feb 18 11:38:32 2007
New Revision: 508958

URL: http://svn.apache.org/viewvc?view=rev&rev=508958
Log:
Finish initial import.

Added:
    tapestry/tapestry5/tapestry-component-report/trunk/src/main/java/org/apache/tapestry/
    tapestry/tapestry5/tapestry-component-report/trunk/src/main/java/org/apache/tapestry/mojo/
    tapestry/tapestry5/tapestry-component-report/trunk/src/main/java/org/apache/tapestry/mojo/ClassDescription.java
    tapestry/tapestry5/tapestry-component-report/trunk/src/main/java/org/apache/tapestry/mojo/ComponentReport.java
    tapestry/tapestry5/tapestry-component-report/trunk/src/main/java/org/apache/tapestry/mojo/ParameterDescription.java
    tapestry/tapestry5/tapestry-component-report/trunk/src/main/java/org/apache/tapestry/mojo/ParametersDoclet.java

Added: tapestry/tapestry5/tapestry-component-report/trunk/src/main/java/org/apache/tapestry/mojo/ClassDescription.java
URL: http://svn.apache.org/viewvc/tapestry/tapestry5/tapestry-component-report/trunk/src/main/java/org/apache/tapestry/mojo/ClassDescription.java?view=auto&rev=508958
==============================================================================
--- tapestry/tapestry5/tapestry-component-report/trunk/src/main/java/org/apache/tapestry/mojo/ClassDescription.java (added)
+++ tapestry/tapestry5/tapestry-component-report/trunk/src/main/java/org/apache/tapestry/mojo/ClassDescription.java Sun Feb 18 11:38:32 2007
@@ -0,0 +1,58 @@
+// Copyright 2007 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.tapestry.mojo;
+
+import static org.apache.tapestry.ioc.internal.util.CollectionFactory.newMap;
+
+import java.util.Map;
+
+public class ClassDescription
+{
+    private final String _superClassName;
+
+    private final String _className;
+
+    private final String _description;
+
+    private final Map<String, ParameterDescription> _parameters = newMap();
+
+    public ClassDescription(String className, String superClassName, String description)
+    {
+        _className = className;
+        _superClassName = superClassName;
+        _description = description;
+    }
+
+    public String getClassName()
+    {
+        return _className;
+    }
+
+    public String getDescription()
+    {
+        return _description;
+    }
+
+    public Map<String, ParameterDescription> getParameters()
+    {
+        return _parameters;
+    }
+
+    public String getSuperClassName()
+    {
+        return _superClassName;
+    }
+
+}

Added: tapestry/tapestry5/tapestry-component-report/trunk/src/main/java/org/apache/tapestry/mojo/ComponentReport.java
URL: http://svn.apache.org/viewvc/tapestry/tapestry5/tapestry-component-report/trunk/src/main/java/org/apache/tapestry/mojo/ComponentReport.java?view=auto&rev=508958
==============================================================================
--- tapestry/tapestry5/tapestry-component-report/trunk/src/main/java/org/apache/tapestry/mojo/ComponentReport.java (added)
+++ tapestry/tapestry5/tapestry-component-report/trunk/src/main/java/org/apache/tapestry/mojo/ComponentReport.java Sun Feb 18 11:38:32 2007
@@ -0,0 +1,587 @@
+// Copyright 2007 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.tapestry.mojo;
+
+import static org.apache.tapestry.ioc.internal.util.CollectionFactory.newList;
+import static org.apache.tapestry.ioc.internal.util.CollectionFactory.newMap;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.StringTokenizer;
+
+import nu.xom.Builder;
+import nu.xom.Document;
+import nu.xom.Element;
+import nu.xom.Elements;
+
+import org.apache.commons.lang.SystemUtils;
+import org.apache.maven.artifact.Artifact;
+import org.apache.maven.artifact.repository.ArtifactRepository;
+import org.apache.maven.project.MavenProject;
+import org.apache.maven.reporting.AbstractMavenReport;
+import org.apache.maven.reporting.MavenReportException;
+import org.apache.tapestry.ioc.IOCUtilities;
+import org.apache.tapestry.ioc.internal.util.InternalUtils;
+import org.codehaus.doxia.sink.Sink;
+import org.codehaus.doxia.site.renderer.SiteRenderer;
+import org.codehaus.plexus.util.StringUtils;
+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;
+
+/**
+ * The component report generates documentation about components and parameters within the current
+ * project.
+ * <p>
+ * It works in two steps. First it runs Javadoc with a custom Doclet that will locate the components
+ * and the parameters. This information is written to a temporary XML file.
+ * <p>
+ * The second stage reads the XML file and generates the final report page.
+ * 
+ * @goal component-report
+ * @requiresDependencyResolution compile
+ * @execute phase="generate-sources"
+ */
+public class ComponentReport extends AbstractMavenReport
+{
+    /**
+     * Identifies the application root package. Only classes beneath the root package will be
+     * searched for components and parameters If a module contains multiple root packages then they
+     * should be seperated by commas.
+     * 
+     * @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;
+
+    /**
+     * Working directory for temporary files.
+     * 
+     * @parameter default-value="target"
+     * @required
+     */
+    private String workDirectory;
+
+    protected String getOutputDirectory()
+    {
+        return outputDirectory;
+    }
+
+    protected MavenProject getProject()
+    {
+        return project;
+    }
+
+    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 "component-parameters";
+    }
+
+    @Override
+    protected void executeReport(Locale locale) throws MavenReportException
+    {
+        Map<String, ClassDescription> descriptions = runJavadoc();
+
+        getLog().info("Executing ComponentReport ...");
+
+        Sink sink = getSink();
+
+        sink.section1();
+        sink.sectionTitle1();
+        sink.text("Component Index");
+        sink.sectionTitle1_();
+        sink.list();
+
+        for (String className : InternalUtils.sortedKeys(descriptions))
+        {
+            String simpleName = IOCUtilities.toSimpleId(className);
+
+            sink.listItem();
+
+            // Something is convertin the name attribute of the anchors to lower case, so
+            // we'll follow suit.
+
+            sink.link("#" + className.toLowerCase());
+            sink.text(simpleName);
+            sink.link_();
+
+            sink.listItem_();
+        }
+
+        sink.list_();
+
+        for (String className : InternalUtils.sortedKeys(descriptions))
+        {
+            writeClassDescription(descriptions, sink, className);
+        }
+
+    }
+
+    private void writeClassDescription(Map<String, ClassDescription> descriptions, Sink sink,
+            String className)
+    {
+        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);
+
+        sink.section2();
+
+        sink.sectionTitle2();
+        sink.anchor(className);
+        sink.text(className);
+        sink.anchor_();
+
+        sink.sectionTitle2_();
+
+        sink.paragraph();
+        sink.text(cd.getDescription());
+        sink.paragraph_();
+
+        sink.paragraph();
+
+        String javadocURL = String.format("apidocs/%s.html", className.replace('.', '/'));
+
+        sink.link(javadocURL);
+        sink.text("[JavaDoc]");
+        sink.link_();
+
+        sink.paragraph_();
+
+        if (!parents.isEmpty())
+        {
+            sink.sectionTitle3();
+            sink.text("Component inheritance");
+            sink.sectionTitle3_();
+
+            sink.list();
+            sink.listItem();
+
+            for (String name : parents)
+            {
+                sink.link("#" + name.toLowerCase());
+                sink.text(name);
+                sink.link_();
+
+                sink.list();
+                sink.listItem();
+            }
+
+            sink.text(className);
+
+            for (int i = 0; i <= parents.size(); i++)
+            {
+                sink.listItem_();
+                sink.list_();
+            }
+        }
+
+        if (!parameters.isEmpty())
+        {
+            List<String> flags = newList();
+
+            sink.sectionTitle3();
+            sink.text("Parameters");
+            sink.sectionTitle3_();
+
+            sink.table();
+            sink.tableRow();
+
+            for (String header : PARAMETER_HEADERS)
+            {
+                sink.tableHeaderCell();
+                sink.text(header);
+                sink.tableHeaderCell_();
+            }
+
+            sink.tableRow_();
+
+            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");
+
+                sink.tableRow();
+
+                cell(sink, pd.getName());
+                cell(sink, pd.getType());
+                cell(sink, InternalUtils.join(flags));
+                cell(sink, pd.getDefaultValue());
+                cell(sink, pd.getDefaultPrefix());
+                cell(sink, pd.getDescription());
+
+                sink.tableRow_();
+
+            }
+
+            sink.table_();
+        }
+
+        sink.section2_();
+    }
+
+    private void cell(Sink sink, String value)
+    {
+        sink.tableCell();
+        sink.text(value);
+        sink.tableCell_();
+    }
+
+    private final static String[] PARAMETER_HEADERS =
+    { "Name", "Type", "Flags", "Default", "Default Prefix", "Description" };
+
+    private Map<String, ClassDescription> runJavadoc() throws MavenReportException
+    {
+        getLog().info("Running JavaDoc to collection 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 = (List<String>) 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 = (List<Artifact>) 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().info(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);
+        }
+    }
+}

Added: tapestry/tapestry5/tapestry-component-report/trunk/src/main/java/org/apache/tapestry/mojo/ParameterDescription.java
URL: http://svn.apache.org/viewvc/tapestry/tapestry5/tapestry-component-report/trunk/src/main/java/org/apache/tapestry/mojo/ParameterDescription.java?view=auto&rev=508958
==============================================================================
--- tapestry/tapestry5/tapestry-component-report/trunk/src/main/java/org/apache/tapestry/mojo/ParameterDescription.java (added)
+++ tapestry/tapestry5/tapestry-component-report/trunk/src/main/java/org/apache/tapestry/mojo/ParameterDescription.java Sun Feb 18 11:38:32 2007
@@ -0,0 +1,80 @@
+// Copyright 2007 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.tapestry.mojo;
+
+public class ParameterDescription
+{
+    private final String _name;
+
+    private final String _type;
+
+    private final String _defaultValue;
+
+    private final String _defaultPrefix;
+
+    private final boolean _required;
+
+    private final boolean _cache;
+
+    private final String _description;
+
+    public ParameterDescription(String name, String type, String defaultValue,
+            String defaultPrefix, boolean required, boolean cache, String description)
+    {
+        _name = name;
+        _type = type;
+        _defaultValue = defaultValue;
+        _defaultPrefix = defaultPrefix;
+        _required = required;
+        _cache = cache;
+        _description = description;
+    }
+
+    public boolean getCache()
+    {
+        return _cache;
+    }
+
+    public String getDefaultPrefix()
+    {
+        return _defaultPrefix;
+    }
+
+    public String getDefaultValue()
+    {
+        return _defaultValue;
+    }
+
+    public String getDescription()
+    {
+        return _description;
+    }
+
+    public String getName()
+    {
+        return _name;
+    }
+
+    public boolean getRequired()
+    {
+        return _required;
+    }
+
+    public String getType()
+    {
+        return _type;
+    }
+
+}

Added: tapestry/tapestry5/tapestry-component-report/trunk/src/main/java/org/apache/tapestry/mojo/ParametersDoclet.java
URL: http://svn.apache.org/viewvc/tapestry/tapestry5/tapestry-component-report/trunk/src/main/java/org/apache/tapestry/mojo/ParametersDoclet.java?view=auto&rev=508958
==============================================================================
--- tapestry/tapestry5/tapestry-component-report/trunk/src/main/java/org/apache/tapestry/mojo/ParametersDoclet.java (added)
+++ tapestry/tapestry5/tapestry-component-report/trunk/src/main/java/org/apache/tapestry/mojo/ParametersDoclet.java Sun Feb 18 11:38:32 2007
@@ -0,0 +1,337 @@
+// Copyright 2007 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.tapestry.mojo;
+
+import java.io.File;
+import java.io.PrintWriter;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Pattern;
+
+import com.sun.javadoc.AnnotationDesc;
+import com.sun.javadoc.ClassDoc;
+import com.sun.javadoc.ConstructorDoc;
+import com.sun.javadoc.Doc;
+import com.sun.javadoc.DocErrorReporter;
+import com.sun.javadoc.Doclet;
+import com.sun.javadoc.FieldDoc;
+import com.sun.javadoc.LanguageVersion;
+import com.sun.javadoc.RootDoc;
+import com.sun.javadoc.SeeTag;
+import com.sun.javadoc.Tag;
+import com.sun.javadoc.AnnotationDesc.ElementValuePair;
+
+/**
+ * Generates an XML file that identifies all the classes that contain parameters, and all the
+ * parameters within each component class. This XML is later converted into part of the Maven
+ * generated HTML site.
+ * <p>
+ * To keep the -doclet parameter passed to javadoc simple, this class should not have any outside
+ * dependencies.
+ * <p>
+ * Works in two passes: First we find any classes that have a field that has the parameter
+ * annotation. Second we locate any subclasses of the initial set of classes, regardless of whether
+ * they have a parameter or not.
+ */
+public class ParametersDoclet extends Doclet
+{
+    static String OUTPUT_PATH_OPTION = "-o";
+
+    static String _outputPath;
+
+    static class Worker
+    {
+        private PrintWriter _out;
+
+        private RootDoc _root;
+
+        private final Set<String> _processedClassNames = new HashSet<String>();
+
+        /** Queue of class names to be processed. */
+
+        private final LinkedList<ClassDoc> _queue = new LinkedList<ClassDoc>();
+
+        private final Pattern _stripper = java.util.regex.Pattern.compile(
+                "(<.*?>|&.*?;)",
+                Pattern.DOTALL);
+
+        public void run(String outputPath, RootDoc root) throws Exception
+        {
+            _root = root;
+
+            File output = new File(outputPath);
+
+            _out = new PrintWriter(output);
+
+            println("<component-parameters>");
+
+            for (ClassDoc cd : root.classes())
+            {
+                emitClass(cd, true, false);
+            }
+
+            while (!_queue.isEmpty())
+            {
+                ClassDoc cd = _queue.removeFirst();
+
+                // Its in the queue because it has a parent class that has parameters
+                // but (if it wasn't already processed) it doesn't define parameters of its own,
+                // so we force it into the output regardless.
+
+                emitClass(cd, false, true);
+            }
+
+            println("</component-parameters>");
+
+            _out.close();
+        }
+
+        private void emitClass(ClassDoc classDoc, boolean queueSubclasses, boolean forceClassOutput)
+        {
+            String className = classDoc.name();
+
+            if (_processedClassNames.contains(className)) return;
+
+            if (!classDoc.isPublic()) return;
+
+            // Check for a no-args public constructor
+
+            boolean found = false;
+
+            for (ConstructorDoc cons : classDoc.constructors())
+            {
+                if (cons.isPublic() && cons.parameters().length == 0)
+                {
+                    found = true;
+                    break;
+                }
+            }
+
+            if (!found) return;
+
+            boolean wroteClass = false;
+
+            for (FieldDoc fd : classDoc.fields())
+            {
+                if (fd.isStatic()) continue;
+
+                if (!fd.isPrivate()) continue;
+
+                Map<String, String> annotationValues = findParameterAnnotation(fd);
+
+                if (annotationValues == null) continue;
+
+                if (!wroteClass)
+                {
+                    printClassDescriptionStart(classDoc);
+                    wroteClass = true;
+                }
+
+                String name = annotationValues.get("name");
+                if (name == null) name = fd.name().replaceAll("^[$_]*", "");
+
+                String defaultValue = fd.constantValueExpression();
+                if (defaultValue == null) defaultValue = "";
+
+                print(
+                        "<parameter name=\"%s\" type=\"%s\" default=\"%s\" required=\"%s\" cache=\"%s\" default-prefix=\"%s\">",
+                        name,
+                        fd.type().qualifiedTypeName(),
+                        get(annotationValues, "value", defaultValue),
+                        get(annotationValues, "required", "false"),
+                        get(annotationValues, "cache", "true"),
+                        get(annotationValues, "defaultPrefix", "prop"));
+
+                // Body of a parameter is the comment text.
+
+                printDescription(fd);
+
+                println("\n</parameter>");
+            }
+
+            if (wroteClass)
+                println("</class>");
+            else if (forceClassOutput)
+            {
+                printClassDescriptionStart(classDoc);
+                println("</class>");
+            }
+
+            if (wroteClass || forceClassOutput) _processedClassNames.add(className);
+
+            if (wroteClass && queueSubclasses)
+            {
+                for (ClassDoc potential : _root.classes())
+                {
+                    // I believe subclassOf() will work even if there are intervening levels of
+                    // heirarchy. This should mean that we don't have to queue up subclasses
+                    // of classes that were added via the queue (rather than by the presence of
+                    // parameter fields).
+
+                    if (potential.subclassOf(classDoc)) _queue.addLast(potential);
+                }
+            }
+        }
+
+        private void printClassDescriptionStart(ClassDoc classDoc)
+        {
+            println(
+                    "<class name=\"%s\" super-class=\"%s\">",
+                    classDoc.qualifiedTypeName(),
+                    classDoc.superclass().qualifiedTypeName());
+            print("<description>");
+            printDescription(classDoc);
+            println("</description>", classDoc.commentText());
+        }
+
+        private String get(Map<String, String> map, String key, String defaultValue)
+        {
+            if (map.containsKey(key)) return map.get(key);
+
+            return defaultValue;
+        }
+
+        private Map<String, String> findParameterAnnotation(FieldDoc fd)
+        {
+            for (AnnotationDesc annotation : fd.annotations())
+            {
+                if (annotation.annotationType().qualifiedTypeName().equals(
+                        "org.apache.tapestry.annotations.Parameter"))
+                {
+                    Map<String, String> result = new HashMap<String, String>();
+
+                    for (ElementValuePair pair : annotation.elementValues())
+                        result.put(pair.element().name(), pair.value().value().toString());
+
+                    return result;
+                }
+            }
+
+            return null;
+        }
+
+        private void print(String format, Object... arguments)
+        {
+            String line = String.format(format, arguments);
+
+            _out.print(line);
+        }
+
+        private void println(String format, Object... arguments)
+        {
+            print(format, arguments);
+
+            _out.println();
+        }
+
+        private void printDescription(Doc holder)
+        {
+            StringBuilder builder = new StringBuilder();
+
+            for (Tag tag : holder.inlineTags())
+            {
+                if (tag.name().equals("Text"))
+                {
+                    builder.append(tag.text());
+                    continue;
+                }
+
+                if (tag.name().equals("@link"))
+                {
+                    SeeTag seeTag = (SeeTag) tag;
+
+                    String label = seeTag.label();
+                    if (label != null && !label.equals(""))
+                    {
+                        builder.append(label);
+                        continue;
+                    }
+
+                    if (seeTag.referencedClassName() != null)
+                        builder.append(seeTag.referencedClassName());
+
+                    if (seeTag.referencedMemberName() != null)
+                    {
+                        builder.append("#");
+                        builder.append(seeTag.referencedMemberName());
+                    }
+
+                    continue;
+                }
+            }
+
+            String text = builder.toString();
+
+            // Fix it up a little.
+
+            // Remove any simple open or close tags found in the text, as well as any XML entities.
+
+            String stripped = _stripper.matcher(text).replaceAll("");
+
+            _out.print(stripped);
+        }
+    }
+
+    /** Yes we are interested in annotations, etc. */
+    public static LanguageVersion languageVersion()
+    {
+        return LanguageVersion.JAVA_1_5;
+    }
+
+    public static int optionLength(String option)
+    {
+        if (option.equals(OUTPUT_PATH_OPTION)) return 2;
+
+        return 0;
+    }
+
+    public static boolean validOptions(String options[][], DocErrorReporter reporter)
+    {
+        for (String[] group : options)
+        {
+            if (group[0].equals(OUTPUT_PATH_OPTION)) _outputPath = group[1];
+
+            // Do we need to check for other unexpected options?
+            // TODO: Check for duplicate -o?
+        }
+
+        if (_outputPath == null)
+            reporter.printError(String.format("Usage: javadoc %s path", OUTPUT_PATH_OPTION));
+
+        return true;
+    }
+
+    public static boolean start(RootDoc root)
+    {
+        // Enough of this static method bullshit. What the fuck were they thinking?
+
+        try
+        {
+            new Worker().run(_outputPath, root);
+        }
+        catch (Exception ex)
+        {
+            root.printError(ex.getMessage());
+
+            return false;
+        }
+
+        return true;
+    }
+
+}