All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.codehaus.modello.plugin.xdoc.XdocGenerator Maven / Gradle / Ivy

Go to download

Modello XDOC Plugin generates model documentation using xdoc markup to be included in a Maven-generated reporting site.

The newest version!
package org.codehaus.modello.plugin.xdoc;

/*
 * Copyright (c) 2004, Codehaus.org
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
 * of the Software, and to permit persons to whom the Software is furnished to do
 * so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

import javax.inject.Named;

import java.io.File;
import java.io.IOException;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.stream.Collectors;

import com.github.chhorz.javadoc.JavaDoc;
import com.github.chhorz.javadoc.JavaDocParserBuilder;
import com.github.chhorz.javadoc.OutputType;
import com.github.chhorz.javadoc.tags.BlockTag;
import com.github.chhorz.javadoc.tags.SinceTag;
import org.codehaus.modello.ModelloException;
import org.codehaus.modello.ModelloParameterConstants;
import org.codehaus.modello.ModelloRuntimeException;
import org.codehaus.modello.model.BaseElement;
import org.codehaus.modello.model.Model;
import org.codehaus.modello.model.ModelAssociation;
import org.codehaus.modello.model.ModelClass;
import org.codehaus.modello.model.ModelDefault;
import org.codehaus.modello.model.ModelField;
import org.codehaus.modello.model.Version;
import org.codehaus.modello.model.VersionRange;
import org.codehaus.modello.plugin.xdoc.metadata.XdocClassMetadata;
import org.codehaus.modello.plugin.xdoc.metadata.XdocFieldMetadata;
import org.codehaus.modello.plugin.xsd.XsdModelHelper;
import org.codehaus.modello.plugins.xml.AbstractXmlGenerator;
import org.codehaus.modello.plugins.xml.metadata.XmlAssociationMetadata;
import org.codehaus.modello.plugins.xml.metadata.XmlClassMetadata;
import org.codehaus.modello.plugins.xml.metadata.XmlFieldMetadata;
import org.codehaus.modello.plugins.xml.metadata.XmlModelMetadata;
import org.codehaus.plexus.util.StringUtils;
import org.codehaus.plexus.util.io.CachingWriter;
import org.codehaus.plexus.util.xml.PrettyPrintXMLWriter;
import org.codehaus.plexus.util.xml.XMLWriter;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;

/**
 * @author Jason van Zyl
 * @author Emmanuel Venisse
 */
@Named("xdoc")
public class XdocGenerator extends AbstractXmlGenerator {
    private static final VersionRange DEFAULT_VERSION_RANGE = new VersionRange("0.0.0+");

    private Version firstVersion = DEFAULT_VERSION_RANGE.getFromVersion();

    private Version version = DEFAULT_VERSION_RANGE.getFromVersion();

    @Override
    public void generate(Model model, Map parameters) throws ModelloException {
        initialize(model, parameters);

        if (parameters.get(ModelloParameterConstants.FIRST_VERSION) != null) {
            firstVersion = new Version((String) parameters.get(ModelloParameterConstants.FIRST_VERSION));
        }

        if (parameters.get(ModelloParameterConstants.VERSION) != null) {
            version = new Version((String) parameters.get(ModelloParameterConstants.VERSION));
        }

        try {
            generateXdoc(parameters);
        } catch (IOException ex) {
            throw new ModelloException("Exception while generating XDoc.", ex);
        }
    }

    private void generateXdoc(Map parameters) throws IOException {
        Model objectModel = getModel();

        File directory = getOutputDirectory();

        if (isPackageWithVersion()) {
            directory = new File(directory, getGeneratedVersion().toString());
        }

        if (!directory.exists()) {
            directory.mkdirs();
        }

        // we assume parameters not null
        String xdocFileName = (String) parameters.get(ModelloParameterConstants.OUTPUT_XDOC_FILE_NAME);

        File f = new File(directory, objectModel.getId() + ".xml");

        if (xdocFileName != null) {
            f = new File(directory, xdocFileName);
        }

        Writer writer = new CachingWriter(f, StandardCharsets.UTF_8);

        XMLWriter w = new PrettyPrintXMLWriter(writer);

        writer.write("\n");

        initHeader(w);

        w.startElement("document");

        w.startElement("properties");

        writeTextElement(w, "title", objectModel.getName());

        w.endElement();

        // Body

        w.startElement("body");

        w.startElement("section");

        w.addAttribute("name", objectModel.getName());

        writeMarkupElement(w, "p", getDescription(objectModel));

        // XML representation of the model with links
        ModelClass root = objectModel.getClass(objectModel.getRoot(getGeneratedVersion()), getGeneratedVersion());

        writeMarkupElement(w, "source", "\n" + getModelXmlDescriptor(root));

        // Element descriptors
        // Traverse from root so "abstract" models aren't included
        writeModelDescriptor(w, root);

        w.endElement();

        w.endElement();

        w.endElement();

        writer.flush();

        writer.close();
    }

    /**
     * Get the anchor name by which model classes can be accessed in the generated xdoc/html file.
     *
     * @param tagName the name of the XML tag of the model class
     * @param modelClass the model class, that eventually can have customized anchor name
     * @return the corresponding anchor name
     */
    private String getAnchorName(String tagName, ModelClass modelClass) {
        XdocClassMetadata xdocClassMetadata = (XdocClassMetadata) modelClass.getMetadata(XdocClassMetadata.ID);

        String anchorName = xdocClassMetadata.getAnchorName();

        return "class_" + (anchorName == null ? tagName : anchorName);
    }

    /**
     * Write description of the whole model.
     *
     * @param w the output writer
     * @param rootModelClass the root class of the model
     */
    private void writeModelDescriptor(XMLWriter w, ModelClass rootModelClass) {
        writeElementDescriptor(w, rootModelClass, null, new HashSet<>(), new HashMap<>());
    }

    /**
     * Write description of an element of the XML representation of the model. This method is recursive.
     *
     * @param w the output writer
     * @param modelClass the mode class to describe
     * @param association the association we are coming from (can be null)
     * @param writtenIds set of data already written ids
     * @param writtenAnchors map of already written anchors with corresponding ids
     */
    private void writeElementDescriptor(
            XMLWriter w,
            ModelClass modelClass,
            ModelAssociation association,
            Set writtenIds,
            Map writtenAnchors) {
        String tagName = resolveTagName(modelClass, association);

        String id = getId(tagName, modelClass);
        if (writtenIds.contains(id)) {
            // tag already written for this model class accessed as this tag name
            return;
        }
        writtenIds.add(id);

        String anchorName = getAnchorName(tagName, modelClass);
        if (writtenAnchors.containsKey(anchorName)) {
            // TODO use logging API?
            System.out.println("[warn] model class " + id + " with tagName " + tagName + " gets duplicate anchorName "
                    + anchorName + ", conflicting with model class " + writtenAnchors.get(anchorName));
        } else {
            writtenAnchors.put(anchorName, id);
        }

        w.startElement("a");

        w.addAttribute("name", anchorName);

        w.endElement();

        w.startElement("subsection");

        w.addAttribute("name", tagName);

        writeMarkupElement(w, "p", getDescription(modelClass));

        List elementFields = getFieldsForXml(modelClass, getGeneratedVersion());

        ModelField contentField = getContentField(elementFields);

        if (contentField != null) {
            // this model class has a Content field
            w.startElement("p");

            writeTextElement(w, "b", "Element Content: ");

            w.writeMarkup(getDescription(contentField));

            w.endElement();
        }

        List attributeFields = getXmlAttributeFields(elementFields);

        elementFields.removeAll(attributeFields);

        writeFieldsTable(w, attributeFields, false); // write attributes
        writeFieldsTable(w, elementFields, true); // write elements

        w.endElement();

        // check every fields that are inner associations to write their element descriptor
        for (ModelField f : elementFields) {
            if (isInnerAssociation(f)) {
                ModelAssociation assoc = (ModelAssociation) f;
                ModelClass fieldModelClass = getModel().getClass(assoc.getTo(), getGeneratedVersion());

                if (!writtenIds.contains(getId(resolveTagName(fieldModelClass, assoc), fieldModelClass))) {
                    writeElementDescriptor(w, fieldModelClass, assoc, writtenIds, writtenAnchors);
                }
            }
        }
    }

    private String getId(String tagName, ModelClass modelClass) {
        return tagName + '/' + modelClass.getPackageName() + '.' + modelClass.getName();
    }

    /**
     * Write a table containing model fields description.
     *
     * @param w the output writer
     * @param fields the fields to add in the table
     * @param elementFields true if fields are elements, false if fields are attributes
     */
    private void writeFieldsTable(XMLWriter w, List fields, boolean elementFields) {
        if (fields == null || fields.isEmpty()) {
            // skip empty table
            return;
        }

        // skip if only one element field with xml.content == true
        if (elementFields && (fields.size() == 1) && hasContentField(fields)) {
            return;
        }

        w.startElement("table");

        w.startElement("tr");

        writeTextElement(w, "th", elementFields ? "Element" : "Attribute");

        writeTextElement(w, "th", "Type");

        boolean showSinceColumn = version.greaterThan(firstVersion);

        if (showSinceColumn) {
            writeTextElement(w, "th", "Since");
        }

        writeTextElement(w, "th", "Description");

        w.endElement(); // tr

        for (ModelField f : fields) {
            XmlFieldMetadata xmlFieldMetadata = (XmlFieldMetadata) f.getMetadata(XmlFieldMetadata.ID);

            if (xmlFieldMetadata.isContent()) {
                continue;
            }

            w.startElement("tr");

            // Element/Attribute column

            String tagName = resolveTagName(f, xmlFieldMetadata);

            w.startElement("td");

            w.startElement("code");

            boolean manyAssociation = false;

            if (f instanceof ModelAssociation) {
                ModelAssociation assoc = (ModelAssociation) f;

                XmlAssociationMetadata xmlAssociationMetadata =
                        (XmlAssociationMetadata) assoc.getAssociationMetadata(XmlAssociationMetadata.ID);

                manyAssociation = assoc.isManyMultiplicity();

                String itemTagName = manyAssociation ? resolveTagName(tagName, xmlAssociationMetadata) : tagName;

                if (manyAssociation && xmlAssociationMetadata.isWrappedItems()) {
                    w.writeText(tagName);
                    w.writeMarkup("/");
                }
                if (isInnerAssociation(f)) {
                    w.startElement("a");
                    w.addAttribute("href", "#" + getAnchorName(itemTagName, assoc.getToClass()));
                    w.writeText(itemTagName);
                    w.endElement();
                } else if (ModelDefault.PROPERTIES.equals(f.getType())) {
                    if (xmlAssociationMetadata.isMapExplode()) {
                        w.writeText("(key,value)");
                    } else {
                        w.writeMarkup("key=value");
                    }
                } else {
                    w.writeText(itemTagName);
                }
                if (manyAssociation) {
                    w.writeText("*");
                }
            } else {
                w.writeText(tagName);
            }

            w.endElement(); // code

            w.endElement(); // td

            // Type column

            w.startElement("td");

            w.startElement("code");

            if (f instanceof ModelAssociation) {
                ModelAssociation assoc = (ModelAssociation) f;

                if (assoc.isOneMultiplicity()) {
                    w.writeText(assoc.getTo());
                } else {
                    w.writeText(assoc.getType().substring("java.util.".length()));

                    if (assoc.isGenericType()) {
                        w.writeText("<" + assoc.getTo() + ">");
                    }
                }
            } else {
                w.writeText(f.getType());
            }

            w.endElement(); // code

            w.endElement(); // td

            // Since column

            if (showSinceColumn) {
                w.startElement("td");

                if (f.getVersionRange() != null) {
                    Version fromVersion = f.getVersionRange().getFromVersion();
                    if (fromVersion != null && fromVersion.greaterThan(firstVersion)) {
                        w.writeMarkup(fromVersion.toString());
                    }
                }

                w.endElement();
            }

            // Description column

            w.startElement("td");

            if (manyAssociation) {
                w.writeMarkup("(Many) ");
            }

            w.writeMarkup(getDescription(f));

            // Write the default value, if it exists.
            // But only for fields that are not a ModelAssociation
            if (f.getDefaultValue() != null && !(f instanceof ModelAssociation)) {
                w.writeMarkup("

Default value: "); writeTextElement(w, "code", f.getDefaultValue()); w.writeMarkup("

"); } w.endElement(); // td w.endElement(); // tr } w.endElement(); // table } /** * Build the pretty tree describing the XML representation of the model. * * @param rootModelClass the model root class * @return the String representing the tree model */ private String getModelXmlDescriptor(ModelClass rootModelClass) { return getElementXmlDescriptor(rootModelClass, null, new Stack<>()); } /** * Build the pretty tree describing the XML representation of an element of the model. This method is recursive. * * @param modelClass the class we are printing the model * @param association the association we are coming from (can be null) * @param stack the stack of elements that have been traversed to come to the current one * @return the String representing the tree model * @throws ModelloRuntimeException */ private String getElementXmlDescriptor(ModelClass modelClass, ModelAssociation association, Stack stack) throws ModelloRuntimeException { StringBuilder sb = new StringBuilder(); appendSpacer(sb, stack.size()); String tagName = resolveTagName(modelClass, association); // "); sb.append(tagName).append(""); boolean addNewline = false; if (stack.isEmpty()) { // try to add XML Schema reference try { String targetNamespace = XsdModelHelper.getTargetNamespace(modelClass.getModel(), getGeneratedVersion()); XmlModelMetadata xmlModelMetadata = (XmlModelMetadata) modelClass.getModel().getMetadata(XmlModelMetadata.ID); if (StringUtils.isNotBlank(targetNamespace) && (xmlModelMetadata.getSchemaLocation() != null)) { String schemaLocation = xmlModelMetadata.getSchemaLocation(getGeneratedVersion()); sb.append(" xmlns=\"" + targetNamespace + "\""); sb.append(" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n"); sb.append(" xsi:schemaLocation=\"" + targetNamespace); sb.append(" " + schemaLocation + "\""); addNewline = true; } } catch (ModelloException me) { // ignore unavailable XML Schema configuration } } String id = tagName + '/' + modelClass.getPackageName() + '.' + modelClass.getName(); if (stack.contains(id)) { // recursion detected sb.append(">...recursion...<").append(tagName).append(">\n"); return sb.toString(); } List fields = getFieldsForXml(modelClass, getGeneratedVersion()); List attributeFields = getXmlAttributeFields(fields); if (!attributeFields.isEmpty()) { for (ModelField f : attributeFields) { XmlFieldMetadata xmlFieldMetadata = (XmlFieldMetadata) f.getMetadata(XmlFieldMetadata.ID); if (addNewline) { addNewline = false; sb.append("\n "); } else { sb.append(' '); } sb.append(resolveTagName(f, xmlFieldMetadata)).append("=.."); } sb.append(' '); } fields.removeAll(attributeFields); if ((fields.isEmpty()) || ((fields.size() == 1) && hasContentField(fields))) { sb.append("/>\n"); } else { sb.append(">\n"); stack.push(id); for (ModelField f : fields) { XmlFieldMetadata xmlFieldMetadata = (XmlFieldMetadata) f.getMetadata(XmlFieldMetadata.ID); XdocFieldMetadata xdocFieldMetadata = (XdocFieldMetadata) f.getMetadata(XdocFieldMetadata.ID); if (XdocFieldMetadata.BLANK.equals(xdocFieldMetadata.getSeparator())) { sb.append('\n'); } String fieldTagName = resolveTagName(f, xmlFieldMetadata); if (isInnerAssociation(f)) { ModelAssociation assoc = (ModelAssociation) f; boolean wrappedItems = false; if (assoc.isManyMultiplicity()) { XmlAssociationMetadata xmlAssociationMetadata = (XmlAssociationMetadata) assoc.getAssociationMetadata(XmlAssociationMetadata.ID); wrappedItems = xmlAssociationMetadata.isWrappedItems(); } if (wrappedItems) { appendSpacer(sb, stack.size()); sb.append("<").append(fieldTagName).append(">\n"); stack.push(fieldTagName); } ModelClass fieldModelClass = getModel().getClass(assoc.getTo(), getGeneratedVersion()); sb.append(getElementXmlDescriptor(fieldModelClass, assoc, stack)); if (wrappedItems) { stack.pop(); appendSpacer(sb, stack.size()); sb.append("</").append(fieldTagName).append(">\n"); } } else if (ModelDefault.PROPERTIES.equals(f.getType())) { ModelAssociation assoc = (ModelAssociation) f; XmlAssociationMetadata xmlAssociationMetadata = (XmlAssociationMetadata) assoc.getAssociationMetadata(XmlAssociationMetadata.ID); appendSpacer(sb, stack.size()); sb.append("<").append(fieldTagName).append(">\n"); if (xmlAssociationMetadata.isMapExplode()) { appendSpacer(sb, stack.size() + 1); sb.append("<key/>\n"); appendSpacer(sb, stack.size() + 1); sb.append("<value/>\n"); } else { appendSpacer(sb, stack.size() + 1); sb.append("<key>value</key>\n"); } appendSpacer(sb, stack.size()); sb.append("</").append(fieldTagName).append(">\n"); } else { appendSpacer(sb, stack.size()); sb.append("<").append(fieldTagName).append("/>\n"); } } stack.pop(); appendSpacer(sb, stack.size()); sb.append("</").append(tagName).append(">\n"); } return sb.toString(); } /** * Compute the tagName of a given class, living inside an association. * @param modelClass the class we are looking for the tag name * @param association the association where this class is used * @return the tag name to use * @todo refactor to use XmlModelHelpers.resolveTagName helpers instead */ private String resolveTagName(ModelClass modelClass, ModelAssociation association) { XmlClassMetadata xmlClassMetadata = (XmlClassMetadata) modelClass.getMetadata(XmlClassMetadata.ID); String tagName; if (xmlClassMetadata == null || xmlClassMetadata.getTagName() == null) { if (association == null) { tagName = uncapitalise(modelClass.getName()); } else { tagName = association.getName(); if (association.isManyMultiplicity()) { tagName = singular(tagName); } } } else { tagName = xmlClassMetadata.getTagName(); } if (association != null) { XmlFieldMetadata xmlFieldMetadata = (XmlFieldMetadata) association.getMetadata(XmlFieldMetadata.ID); XmlAssociationMetadata xmlAssociationMetadata = (XmlAssociationMetadata) association.getAssociationMetadata(XmlAssociationMetadata.ID); if (xmlFieldMetadata != null) { if (xmlAssociationMetadata.getTagName() != null) { tagName = xmlAssociationMetadata.getTagName(); } else if (xmlFieldMetadata.getTagName() != null) { tagName = xmlFieldMetadata.getTagName(); if (association.isManyMultiplicity()) { tagName = singular(tagName); } } } } return tagName; } /** * Appends the required spacers to the given StringBuilder. * @param sb where to append the spacers * @param depth the depth of spacers to generate */ private static void appendSpacer(StringBuilder sb, int depth) { for (int i = 0; i < depth; i++) { sb.append(" "); } } private static String getDescription(BaseElement element) { return (element.getDescription() == null) ? "No description." : rewrite(element.getDescription()); } private static void writeTextElement(XMLWriter w, String name, String text) { w.startElement(name); w.writeText(text); w.endElement(); } private static void writeMarkupElement(XMLWriter w, String name, String markup) { w.startElement(name); w.writeMarkup(markup); w.endElement(); } /** * Ensures that text will have balanced tags * * @param text xml or html based content * @return valid XML string */ private static String rewrite(String text) { JavaDoc javaDoc = JavaDocParserBuilder.withStandardJavadocTags() .withOutputType(OutputType.HTML) .build() .parse(text); String html = javaDoc.getDescription() + javaDoc.getTags().stream() .map(XdocGenerator::renderJavaDocTag) .filter(Objects::nonNull) .collect(Collectors.joining("\n", "\n", "")); Document document = Jsoup.parseBodyFragment(html); document.outputSettings().syntax(Document.OutputSettings.Syntax.xml); return document.body().html(); } private static String renderJavaDocTag(BlockTag tag) { if (tag instanceof SinceTag) { return "

Since: " + ((SinceTag) tag).getSinceText() + "

"; } return null; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy