org.codehaus.modello.plugin.xdoc.XdocGenerator Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of modello-plugin-xdoc Show documentation
Show all versions of modello-plugin-xdoc Show documentation
Modello XDOC Plugin generates model documentation using xdoc markup to be included in a Maven-generated reporting
site.
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();
}
}