/******************************************************************************** * CruiseControl, a Continuous Integration Toolkit * Copyright (c) 2001-2003, 2006, ThoughtWorks, Inc. * 200 E. Randolph, 25th Floor * Chicago, IL 60601 USA * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * + Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * + Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following * disclaimer in the documentation and/or other materials provided * with the distribution. * * + Neither the name of ThoughtWorks, Inc., CruiseControl, nor the * names of its contributors may be used to endorse or promote * products derived from this software without specific prior * written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ********************************************************************************/ package net.sourceforge.cruisecontrol.gendoc; import java.io.Serializable; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import net.sourceforge.cruisecontrol.gendoc.html.HtmlUtils; /** * This class represents an Plugin, storing information such as attributes and children * The setter methods of this class are package-private to prevent modification * after the class has been constructed. * * @author Anthony Love (lovea@msoe.edu) * @version 1.0 */ public class PluginInfo implements Serializable, Comparable<Object> { /** ID for serialization. */ private static final long serialVersionUID = 3L; /** Path separator for ancestry paths. */ private static final String PATH_SEPARATOR = "::"; /** The name of the Plugin, as it would appear in an XML config file */ private String name; /** Name of the Java class that corresponds with this Plugin. */ private final String className; /** The Plugin's description. */ private String description; /** The Plugin's example documentation. */ private String examples; /** The human-readable title for the plugin. */ private String title; /** List of attributes, preserving order as defined by AttributeInfo.compareTo(). */ private final List<AttributeInfo> attributes = new ArrayList<AttributeInfo>(); /** * List of children for this plugin. Default to zero capacity to save space, since most plugins * won't have children. */ private final List<ChildInfo> children = new ArrayList<ChildInfo>(0); /** * List of parents for this plugin. This includes all plugins which claim this plugin as a child. * Note that a plugin can have multiple parents, and a plugin can even be its own parent. */ private final List<PluginInfo> parents = new ArrayList<PluginInfo>(); /** * Direct parent for this plugin. This is the parent directly above it on the shortest path * up to the root plugin node. This value is lazily calculated and cached here. */ private PluginInfo directParent; /** * Depth of this plugin in the tree. A depth of zero means this is the root plugin of the * tree. A value of Integer.MAX_VALUE means the depth still needs to be calculated. * */ private int depth = Integer.MAX_VALUE; /** * List of errors that occurred while parsing this info. Default to zero capacity to * save space, since most plugins should be without errors. */ private final List<String> parsingErrors = new ArrayList<String>(0); /** Cache of HTML documentation for this plugin. */ private transient String html; /** * Creates a new PluginInfo with all fields defaulted. * @param className Name of the class being parsed to produce this plugin information. */ public PluginInfo(String className) { this.className = className; } /** * Creates a pre-populated PluginInfo. This can be used for creating mock * objects for testing. Note that finishConstruction() should be called on the * root of the resulting PluginInfo tree. * * @param className Name of class that produced this information. * @param name Name. * @param description Description. * @param title Title. * @param attributes Array of attributes. * @param children Array of children. */ public PluginInfo( String className, String name, String description, String examples, String title, AttributeInfo[] attributes, ChildInfo[] children ) { this(className); setName(name); setDescription(description); setExamples(examples); setTitle(title); for (AttributeInfo attr : attributes) { addAttribute(attr); } for (ChildInfo child : children) { addChild(child); } sortAttributes(); } public String getName() { return name; } protected void setName(String name) { this.name = name; } public String getDescription() { return description; } void setDescription(String description) { this.description = description; } public String getExamples() { return examples; } void setExamples(String description) { this.examples = description; } /** * Returns the List of Children for the plugin * * @return The List of Children for the plugin */ public Collection<ChildInfo> getChildren() { return children; } /** * Adds the given Child to the List. * * @param ch The Child to add. This object must be fully parsed and constructed * before being passed in to this method. */ void addChild(ChildInfo ch) { ch.addParent(this); children.add(ch); } /** * Returns the List of Attributes for this plugin * * @return The List of Attributes for the plugin */ public Collection<AttributeInfo> getAttributes() { return attributes; } /** * Adds the given attribute to the List * * @param att The attribute to add */ void addAttribute(AttributeInfo att) { attributes.add(att); } void setTitle(String title) { this.title = title; } /** * Returns the member's Title, or the name if the title was not * specified. */ public String getTitle() { return (title == null) ? name : title; } /** * Gets a child plugin for this plugin, given the name of the child plugin. * @param pluginName The name of the child plugin to get. * @return Null if the named plugin is not an allowed child of this plugin. * Otherwise, the return value is the PluginInfo for the requested * child plugin. */ public PluginInfo getChildPluginByName(String pluginName) { for (ChildInfo child : children) { PluginInfo node = child.getAllowedNodeByName(pluginName); if (node != null) { return node; } } // No suitable plugin was found. return null; } /** * Gets an attribute of this plugin, given the attribute name. * @param attributeName The name of the attribute whose information is to be fetched. * @return The AttributeInfo for the named attribute, or null if no attribute * exists with that name. */ public AttributeInfo getAttributeByName(String attributeName) { // Assume the list has been sorted. int index = Collections.binarySearch(attributes, attributeName); if (index < 0) { return null; } else { return attributes.get(index); } } /** * Gets a ChildInfo object which contains a specified plugin as an allowed node. * @param pluginName The name of the plugin to look for. * @return A ChildInfo object that is a child of this plugin and contains the plugin * 'pluginName' as an allowed node, or null if no such ChildInfo exists. */ public ChildInfo getChildByPluginName(String pluginName) { for (ChildInfo child : children) { if (child.getAllowedNodeByName(pluginName) != null) { return child; } } // No suitable child was found. return null; } /** * Logs an error during parsing. * @param errorMessage a parsing error message to log. */ void addParsingError(String errorMessage) { parsingErrors.add(errorMessage); } /** * Gets all the parsing errors that were encountered when this plugin was parsed. * @return The list of error messages. */ public List<String> getParsingErrors() { return parsingErrors; } /** * Adds a parent to this plugin. * @param p The parent to add. */ void addParent(PluginInfo p) { if (!parents.contains(p)) { parents.add(p); } } /** * Gets a list of all plugins that can take this plugin as a child. * @return The list of PluginInfos. */ public List<PluginInfo> getAllParents() { return parents; } /** * Gets the name of the Java class corresponding to this PluginInfo. * @return Fully-qualified class name. */ public String getClassName() { return className; } /** * Gets the direct parent of this plugin. The direct parent is the plugin immediately above * it on the shortest path that leads back to the root node. This method may take a lot of * time to perform the search, so its result is cached. Therefore, it should only be called * if necessary. Also, this method may only be called after the plugin tree has been fully * parsed and computeDepth() has been called on the root node of the tree. * @return The direct parent, or null if this Plugin has no parents. */ public PluginInfo getDirectParent() { if (parents.isEmpty()) { return null; // No parents. } else { if (directParent == null) { // Direct parent not cached; we need to compute it. // Simply find the parent with the shallowest depth in the tree. PluginInfo shallowestParent = null; for (PluginInfo parent : parents) { if ((shallowestParent == null) || (parent.depth < shallowestParent.depth)) { shallowestParent = parent; } } directParent = shallowestParent; } return directParent; } } /** * Gets a list of plugins in the hierarchy which starts at the root plugin and ends at this * plugin. * @return The list of plugins. */ public List<PluginInfo> getAncestry() { List<PluginInfo> ancestry = new ArrayList<PluginInfo>(); // Build the list in reverse (from children to parents). PluginInfo current = this; do { ancestry.add(current); current = current.getDirectParent(); } while (current != null); // Flip the list before returning it. Collections.reverse(ancestry); return ancestry; } /** * Gets the fully-qualified name for this plugin, which includes its ancestors' names, * separated by periods. * @return The name. */ public String getAncestralName() { StringBuilder text = new StringBuilder(); boolean first = true; for (PluginInfo ancestor : getAncestry()) { if (first) { first = false; } else { text.append(PATH_SEPARATOR); } text.append(ancestor.getName()); } return text.toString(); } /** * Finishes the construction of a tree of PluginInfos. * * <p>Computes the depth of this plugin (and all its children) in the tree, according to the shortest * path that leads back to the tree root. The result is stored in each object's depth field. This * method may only be called on the root plugin, or loops in the tree may not be handled properly * and may result in stack overflow.</p> */ public void finishConstruction() { // Make sure we are the root plugin. if (!parents.isEmpty()) { throw new UnsupportedOperationException("computeDepth() can only be invoked on root plugin"); } // Traverse the plugin tree in a breadth-first manner, computing the depth at each // level as we encounter it. int currentDepth = 0; List<PluginInfo> pluginsAtThisDepth = new ArrayList<PluginInfo>(); List<PluginInfo> pluginsAtNextDepth = new ArrayList<PluginInfo>(); pluginsAtThisDepth.add(this); // Start with this, the root plugin. do { // Search through all the plugins at this depth. for (PluginInfo plugin : pluginsAtThisDepth) { // Only deal with the node if it hasn't been touched yet. if (currentDepth < plugin.depth) { plugin.depth = currentDepth; // We now know that all the children of this plugin are at the next depth down, // so add all the children to the next list to be searched. for (ChildInfo child : plugin.getChildren()) { for (PluginInfo childPlugin : child.getAllowedNodes()) { pluginsAtNextDepth.add(childPlugin); } } } } // We are now done with this level. Move to the next one. Swap lists to reuse allocated memory. currentDepth++; List<PluginInfo> temp = pluginsAtThisDepth; pluginsAtThisDepth = pluginsAtNextDepth; pluginsAtNextDepth = temp; pluginsAtNextDepth.clear(); // Get ready for next iteration. } while (!pluginsAtThisDepth.isEmpty()); // Keep going until we have done all the plugins. } /** * Sorts the attributes in this plugin by name. */ void sortAttributes() { Collections.sort(attributes); } /** * Retrieves HTML to document this plugin, its attributes, and its children. This will be a * fragment of the full HTML documentation that can be generated for all plugins, and it may * contain hyperlinks to other plugins' documentation. * @return The HTML text. */ public String getHtmlDocumentation() { if (html == null) { html = buildHtmlDocumentation(); } return html; } /** * Generates HTML to document this plugin. * @return The HTML text. */ private String buildHtmlDocumentation() { StringBuilder text = new StringBuilder(); text .append("<div class=\"elementdocumentation\">\n") .append("<a class=\"toplink\" href=\"#top\">top</a>\n") .append("<h2><a name=\"") .append(getName()) // Allow linking by simple name ... .append("\"></a><a name=\"") .append(getAncestralName()) // ... or by full ancestral name. .append("\">"); writeBracketed(text, getName()); text .append("</a></h2>\n") .append("<div class=\"hierarchy\">\n") .append("<pre>"); writeHtmlParents(text); text .append("</pre>\n") .append("</div>\n") .append(HtmlUtils.emptyIfNull(getDescription())) .append("\n"); // Print attributes in a table. if (!attributes.isEmpty()) { text.append("<h3>Attributes of "); writeBracketed(text, getName()); text.append("</h3>\n"); AttributeInfo.writeTableStart(text); for (AttributeInfo attribute : attributes) { attribute.writeHtml(text); } AttributeInfo.writeTableEnd(text); } // Print children in a table. if (!children.isEmpty()) { text.append("<h3>Child Elements of "); writeBracketed(text, getName()); text.append("</h3>\n"); ChildInfo.writeTableStart(text); for (ChildInfo child : children) { child.writeHtml(text); } ChildInfo.writeTableEnd(text); } // Print examples. if (examples != null) { text.append("<h3>Examples of "); writeBracketed(text, getName()); text.append(" Usage</h3>"); text.append(examples); } // Print parsing errors in a table. if (!parsingErrors.isEmpty()) { text .append("<h3 class=\"errors\">Parsing Errors</h3>\n") .append("<p class=\"errors\">The HTML generator encountered errors when parsing\n") .append("the source code for this plugin.</p>\n") .append("<table class=\"documentation\">\n") .append("<tbody>\n"); for (String error : parsingErrors) { text .append("<tr><td class=\"errors\">\n") .append(error) .append("</td></tr>\n"); } text .append("</tbody>\n") .append("</table>\n"); } text.append("</div> <!-- elementdocumentation -->\n"); return text.toString(); } /** * Wraps text in angle brackets. */ private void writeBracketed(StringBuilder text, String content) { text .append("<") .append(content) .append(">"); } /** * Generates the HTML text (without the <pre></pre> tags) to display the list of * immediate parents of this plugin. * @param text Text buffer to write to. */ private void writeHtmlParents(StringBuilder text) { String indentation = ""; if (!parents.isEmpty()) { // There is at least one parent. if (!parents.get(0).getAllParents().isEmpty()) { // There is at least one grandparent. Write an ellipsis to represent it. writeBracketed(text, "..."); text.append("\n"); indentation = " "; // Indent everything else. } // Generate a hyperlink for each parent. for (PluginInfo parent : getAllParents()) { text.append(indentation); parent.writeHtmlLink(text); } indentation += " "; } // Write the plugin itself, indented a little. text.append(indentation); writeBracketed(text, getName()); } /** * Writes an HTML hyperlink to this plugin. * @param text Text buffer to write to. */ private void writeHtmlLink(StringBuilder text) { text .append("<a href=\"#") .append(getAncestralName()) .append("\">"); writeBracketed(text, getName()); text.append("</a>\n"); } /** * Imposes an alphabetical by-name ordering on PluginInfo objects. See toString(). * @param o Object to compare to. * @return Result of comparison. */ public int compareTo(Object o) { if (o == null) { return 1; // Sort nulls before non-nulls. } else { // Sort plugins by name. return this.toString().compareTo(o.toString()); } } /** * Gets a string representation (i.e. the name) for this plugin. This is used to properly * implement compareTo(Object). * @return The plugin's name. */ @Override public String toString() { return name; } }