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.

There is a newer version: 2.4.0
Show 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 java.io.File;
import java.io.IOException;
import java.io.Writer;
import java.util.HashSet;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import java.util.Stack;

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.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.WriterFactory;
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
 */
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();

    public void generate( Model model, Properties parameters )
        throws ModelloException
    {
        initialize( model, parameters );

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

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

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

    private void generateXdoc( Properties 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 = parameters.getProperty( ModelloParameterConstants.OUTPUT_XDOC_FILE_NAME );

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

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

        Writer writer = WriterFactory.newXmlWriter( f );

        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
     * @return the corresponding anchor name
     */
    private String getAnchorName( String tagName )
    {
        return "class_" + tagName ;
    }

    /**
     * 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() );
    }

    /**
     * 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 written set of data already written
     */
    private void writeElementDescriptor( XMLWriter w, ModelClass modelClass, ModelAssociation association,
                                         Set written )
    {
        String tagName = resolveTagName( modelClass, association );

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

        written.add( tagName );

        w.startElement( "a" );

        w.addAttribute( "name", getAnchorName( tagName ) );

        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 ( !written.contains( getId( resolveTagName( fieldModelClass, assoc ), fieldModelClass ) ) )
                {
                    writeElementDescriptor( w, fieldModelClass, assoc, written );
                }
            }
        }
    }

    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 ) );
                    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 is: " ); writeTextElement( w, "code", f.getDefaultValue() ); w.writeText( "." ); } 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.size() == 0 ) { // 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.size() > 0 ) { 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.size() == 0 ) || ( ( 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 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 ) { Document document = Jsoup.parseBodyFragment( text ); document.outputSettings().syntax( Document.OutputSettings.Syntax.xml ); return document.body().html(); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy