blob: fb18bf5c58c739dd09bd4908fe1b1bb2cb981a29 [file] [log] [blame]
package org.apache.maven.plugins.javadoc;
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.
*/
import com.thoughtworks.qdox.JavaProjectBuilder;
import com.thoughtworks.qdox.library.ClassLibraryBuilder;
import com.thoughtworks.qdox.library.OrderedClassLibraryBuilder;
import com.thoughtworks.qdox.model.DocletTag;
import com.thoughtworks.qdox.model.JavaAnnotatedElement;
import com.thoughtworks.qdox.model.JavaAnnotation;
import com.thoughtworks.qdox.model.JavaClass;
import com.thoughtworks.qdox.model.JavaConstructor;
import com.thoughtworks.qdox.model.JavaExecutable;
import com.thoughtworks.qdox.model.JavaField;
import com.thoughtworks.qdox.model.JavaGenericDeclaration;
import com.thoughtworks.qdox.model.JavaMember;
import com.thoughtworks.qdox.model.JavaMethod;
import com.thoughtworks.qdox.model.JavaParameter;
import com.thoughtworks.qdox.model.JavaType;
import com.thoughtworks.qdox.model.JavaTypeVariable;
import com.thoughtworks.qdox.parser.ParseException;
import com.thoughtworks.qdox.type.TypeResolver;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.ClassUtils;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.DependencyResolutionRequiredException;
import org.apache.maven.artifact.repository.ArtifactRepository;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.Component;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.project.MavenProject;
import org.apache.maven.settings.Settings;
import org.apache.maven.shared.invoker.MavenInvocationException;
import org.codehaus.plexus.components.interactivity.InputHandler;
import org.codehaus.plexus.languages.java.version.JavaVersion;
import org.codehaus.plexus.util.FileUtils;
import org.codehaus.plexus.util.IOUtil;
import org.codehaus.plexus.util.ReaderFactory;
import org.codehaus.plexus.util.StringUtils;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.io.StringWriter;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Abstract class to fix Javadoc documentation and tags in source files.
* <br>
* See <a href="http://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#wheretags">Where Tags
* Can Be Used</a>.
*
* @author <a href="mailto:vincent.siveton@gmail.com">Vincent Siveton</a>
* @since 2.6
*/
public abstract class AbstractFixJavadocMojo
extends AbstractMojo
{
/**
* The vm line separator
*/
private static final String EOL = System.getProperty( "line.separator" );
/**
* Tag name for &#64;author *
*/
private static final String AUTHOR_TAG = "author";
/**
* Tag name for &#64;version *
*/
private static final String VERSION_TAG = "version";
/**
* Tag name for &#64;since *
*/
private static final String SINCE_TAG = "since";
/**
* Tag name for &#64;param *
*/
private static final String PARAM_TAG = "param";
/**
* Tag name for &#64;return *
*/
private static final String RETURN_TAG = "return";
/**
* Tag name for &#64;throws *
*/
private static final String THROWS_TAG = "throws";
/**
* Tag name for &#64;link *
*/
private static final String LINK_TAG = "link";
/**
* Tag name for {&#64;inheritDoc} *
*/
private static final String INHERITED_TAG = "{@inheritDoc}";
/**
* Start Javadoc String i.e. <code>&#47;&#42;&#42;</code> *
*/
private static final String START_JAVADOC = "/**";
/**
* End Javadoc String i.e. <code>&#42;&#47;</code> *
*/
private static final String END_JAVADOC = "*/";
/**
* Javadoc Separator i.e. <code> &#42; </code> *
*/
private static final String SEPARATOR_JAVADOC = " * ";
/**
* Inherited Javadoc i.e. <code>&#47;&#42;&#42; {&#64;inheritDoc} &#42;&#47;</code> *
*/
private static final String INHERITED_JAVADOC = START_JAVADOC + " " + INHERITED_TAG + " " + END_JAVADOC;
/**
* <code>all</code> parameter used by {@link #fixTags} *
*/
private static final String FIX_TAGS_ALL = "all";
/**
* <code>public</code> parameter used by {@link #level} *
*/
private static final String LEVEL_PUBLIC = "public";
/**
* <code>protected</code> parameter used by {@link #level} *
*/
private static final String LEVEL_PROTECTED = "protected";
/**
* <code>package</code> parameter used by {@link #level} *
*/
private static final String LEVEL_PACKAGE = "package";
/**
* <code>private</code> parameter used by {@link #level} *
*/
private static final String LEVEL_PRIVATE = "private";
/**
* The Clirr Maven plugin groupId <code>org.codehaus.mojo</code> *
*/
private static final String CLIRR_MAVEN_PLUGIN_GROUPID = "org.codehaus.mojo";
/**
* The Clirr Maven plugin artifactId <code>clirr-maven-plugin</code> *
*/
private static final String CLIRR_MAVEN_PLUGIN_ARTIFACTID = "clirr-maven-plugin";
/**
* The latest Clirr Maven plugin version <code>2.2.2</code> *
*/
private static final String CLIRR_MAVEN_PLUGIN_VERSION = "2.2.2";
/**
* The Clirr Maven plugin goal <code>check</code> *
*/
private static final String CLIRR_MAVEN_PLUGIN_GOAL = "check";
/**
* Java Files Pattern.
*/
public static final String JAVA_FILES = "**\\/*.java";
/**
* Default version value.
*/
public static final String DEFAULT_VERSION_VALUE = "\u0024Id: \u0024Id";
// ----------------------------------------------------------------------
// Mojo components
// ----------------------------------------------------------------------
/**
* Input handler, needed for command line handling.
*/
@Component
private InputHandler inputHandler;
// ----------------------------------------------------------------------
// Mojo parameters
// ----------------------------------------------------------------------
/**
* Version to compare the current code against using the
* <a href="http://mojo.codehaus.org/clirr-maven-plugin/">Clirr Maven Plugin</a>.
* <br/>
* See <a href="#defaultSince">defaultSince</a>.
*/
@Parameter ( property = "comparisonVersion", defaultValue = "(,${project.version})" )
private String comparisonVersion;
/**
* Default value for the Javadoc tag <code>&#64;author</code>.
* <br/>
* If not specified, the <code>user.name</code> defined in the System properties will be used.
*/
@Parameter ( property = "defaultAuthor" )
private String defaultAuthor;
/**
* Default value for the Javadoc tag <code>&#64;since</code>.
*/
@Parameter ( property = "defaultSince", defaultValue = "${project.version}" )
private String defaultSince;
/**
* Default value for the Javadoc tag <code>&#64;version</code>.
* <br/>
* By default, it is <code>&#36;Id:&#36;</code>, corresponding to a
* <a href="http://svnbook.red-bean.com/en/1.1/ch07s02.html#svn-ch-7-sect-2.3.4">SVN keyword</a>.
* Refer to your SCM to use an other SCM keyword.
*/
@Parameter ( property = "defaultVersion", defaultValue = DEFAULT_VERSION_VALUE )
private String defaultVersion = "\u0024Id: \u0024"; // can't use default-value="\u0024Id: \u0024"
/**
* The file encoding to use when reading the source files. If the property
* <code>project.build.sourceEncoding</code> is not set, the platform default encoding is used.
*/
@Parameter ( property = "encoding", defaultValue = "${project.build.sourceEncoding}" )
private String encoding;
/**
* Comma separated excludes Java files, i.e. <code>&#42;&#42;/&#42;Test.java</code>.
*/
@Parameter ( property = "excludes" )
private String excludes;
/**
* Comma separated tags to fix in classes, interfaces or methods Javadoc comments.
* Possible values are:
* <ul>
* <li>all (fix all Javadoc tags)</li>
* <li>author (fix only &#64;author tag)</li>
* <li>version (fix only &#64;version tag)</li>
* <li>since (fix only &#64;since tag)</li>
* <li>param (fix only &#64;param tag)</li>
* <li>return (fix only &#64;return tag)</li>
* <li>throws (fix only &#64;throws tag)</li>
* <li>link (fix only &#64;link tag)</li>
* </ul>
*/
@Parameter ( property = "fixTags", defaultValue = "all" )
private String fixTags;
/**
* Flag to fix the classes or interfaces Javadoc comments according the <code>level</code>.
*/
@Parameter ( property = "fixClassComment", defaultValue = "true" )
private boolean fixClassComment;
/**
* Flag to fix the fields Javadoc comments according the <code>level</code>.
*/
@Parameter ( property = "fixFieldComment", defaultValue = "true" )
private boolean fixFieldComment;
/**
* Flag to fix the methods Javadoc comments according the <code>level</code>.
*/
@Parameter ( property = "fixMethodComment", defaultValue = "true" )
private boolean fixMethodComment;
/**
* <p>Flag to remove throws tags from unknown classes.</p>
* <p><strong>NOTE:</strong>Since 3.1.0 the default value has been changed to {@code true},
* due to JavaDoc 8 strictness.
*/
@Parameter ( property = "removeUnknownThrows", defaultValue = "true" )
private boolean removeUnknownThrows;
/**
* Forcing the goal execution i.e. skip warranty messages (not recommended).
*/
@Parameter ( property = "force" )
private boolean force;
/**
* Flag to ignore or not Clirr.
*/
@Parameter ( property = "ignoreClirr", defaultValue = "false" )
protected boolean ignoreClirr;
/**
* Comma separated includes Java files, i.e. <code>&#42;&#42;/&#42;Test.java</code>.
* <p/>
* <strong>Note:</strong> default value is {@code **\/*.java}.
*/
@Parameter ( property = "includes", defaultValue = JAVA_FILES )
private String includes;
/**
* Specifies the access level for classes and members to show in the Javadocs.
* Possible values are:
* <ul>
* <li><a href="http://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#public">public</a>
* (shows only public classes and members)</li>
* <li><a href="http://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#protected">protected</a>
* (shows only public and protected classes and members)</li>
* <li><a href="http://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#package">package</a>
* (shows all classes and members not marked private)</li>
* <li><a href="http://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#private">private</a>
* (shows all classes and members)</li>
* </ul>
*/
@Parameter ( property = "level", defaultValue = "protected" )
private String level;
/**
* The local repository where the artifacts are located, used by the tests.
*/
@Parameter ( property = "localRepository" )
private ArtifactRepository localRepository;
/**
* Output directory where Java classes will be rewritten.
*/
@Parameter ( property = "outputDirectory", defaultValue = "${project.build.sourceDirectory}" )
private File outputDirectory;
/**
* The Maven Project Object.
*/
@Parameter( defaultValue = "${project}", readonly = true, required = true )
private MavenProject project;
@Parameter ( defaultValue = "${session}", readonly = true, required = true )
private MavenSession session;
/**
* The current user system settings for use in Maven.
*/
@Parameter( defaultValue = "${settings}", readonly = true, required = true )
private Settings settings;
// ----------------------------------------------------------------------
// Internal fields
// ----------------------------------------------------------------------
/**
* The current project class loader.
*/
private ClassLoader projectClassLoader;
/**
* Split {@link #fixTags} by comma.
*
* @see #init()
*/
private String[] fixTagsSplitted;
/**
* New classes found by Clirr.
*/
private List<String> clirrNewClasses;
/**
* New Methods in a Class (the key) found by Clirr.
*/
private Map<String, List<String>> clirrNewMethods;
/**
* List of classes where <code>&#42;since</code> is added. Will be used to add or not this tag in the methods.
*/
private List<String> sinceClasses;
/**
* {@inheritDoc}
*/
@Override
public void execute()
throws MojoExecutionException, MojoFailureException
{
if ( !fixClassComment && !fixFieldComment && !fixMethodComment )
{
getLog().info( "Specified to NOT fix classes, fields and methods. Nothing to do." );
return;
}
// verify goal params
init();
if ( fixTagsSplitted.length == 0 )
{
getLog().info( "No fix tag specified. Nothing to do." );
return;
}
// add warranty msg
if ( !preCheck() )
{
return;
}
// run clirr
try
{
executeClirr();
}
catch ( MavenInvocationException e )
{
if ( getLog().isDebugEnabled() )
{
getLog().error( "MavenInvocationException: " + e.getMessage(), e );
}
else
{
getLog().error( "MavenInvocationException: " + e.getMessage() );
}
getLog().info( "Clirr is ignored." );
}
// run qdox and process
try
{
Collection<JavaClass> javaClasses = getQdoxClasses();
if ( javaClasses != null )
{
for ( JavaClass javaClass : javaClasses )
{
processFix( javaClass );
}
}
}
catch ( IOException e )
{
throw new MojoExecutionException( "IOException: " + e.getMessage(), e );
}
}
// ----------------------------------------------------------------------
// protected methods
// ----------------------------------------------------------------------
protected final MavenProject getProject()
{
return project;
}
/**
* @param p not null maven project.
* @return the artifact type.
*/
protected String getArtifactType( MavenProject p )
{
return p.getArtifact().getType();
}
/**
* @param p not null maven project.
* @return the list of source paths for the given project.
*/
protected List<String> getProjectSourceRoots( MavenProject p )
{
return ( p.getCompileSourceRoots() == null
? Collections.<String>emptyList()
: new LinkedList<>( p.getCompileSourceRoots() ) );
}
/**
* @param p not null
* @return the compile classpath elements
* @throws DependencyResolutionRequiredException
* if any
*/
protected List<String> getCompileClasspathElements( MavenProject p )
throws DependencyResolutionRequiredException
{
return ( p.getCompileClasspathElements() == null
? Collections.<String>emptyList()
: new LinkedList<>( p.getCompileClasspathElements() ) );
}
/**
* @param javaExecutable not null
* @return the fully qualify name of javaMethod with signature
*/
protected static String getJavaMethodAsString( JavaExecutable javaExecutable )
{
return javaExecutable.getDeclaringClass().getFullyQualifiedName() + "#" + javaExecutable.getCallSignature();
}
// ----------------------------------------------------------------------
// private methods
// ----------------------------------------------------------------------
/**
* Init goal parameters.
*/
private void init()
{
// defaultAuthor
if ( StringUtils.isEmpty( defaultAuthor ) )
{
defaultAuthor = System.getProperty( "user.name" );
}
// defaultSince
int i = defaultSince.indexOf( "-" + Artifact.SNAPSHOT_VERSION );
if ( i != -1 )
{
defaultSince = defaultSince.substring( 0, i );
}
// fixTags
if ( !FIX_TAGS_ALL.equalsIgnoreCase( fixTags.trim() ) )
{
String[] split = StringUtils.split( fixTags, "," );
List<String> filtered = new LinkedList<>();
for ( String aSplit : split )
{
String s = aSplit.trim();
if ( JavadocUtil.equalsIgnoreCase( s, FIX_TAGS_ALL, AUTHOR_TAG, VERSION_TAG, SINCE_TAG, PARAM_TAG,
THROWS_TAG, LINK_TAG, RETURN_TAG ) )
{
filtered.add( s );
}
else
{
if ( getLog().isWarnEnabled() )
{
getLog().warn( "Unrecognized '" + s + "' for fixTags parameter. Ignored it!" );
}
}
}
fixTags = StringUtils.join( filtered.iterator(), "," );
}
fixTagsSplitted = StringUtils.split( fixTags, "," );
// encoding
if ( StringUtils.isEmpty( encoding ) )
{
if ( getLog().isWarnEnabled() )
{
getLog().warn( "File encoding has not been set, using platform encoding " + ReaderFactory.FILE_ENCODING
+ ", i.e. build is platform dependent!" );
}
encoding = ReaderFactory.FILE_ENCODING;
}
// level
level = level.trim();
if ( !JavadocUtil.equalsIgnoreCase( level, LEVEL_PUBLIC, LEVEL_PROTECTED, LEVEL_PACKAGE, LEVEL_PRIVATE ) )
{
if ( getLog().isWarnEnabled() )
{
getLog().warn( "Unrecognized '" + level + "' for level parameter, using 'protected' level." );
}
level = "protected";
}
}
/**
* @return <code>true</code> if the user wants to proceed, <code>false</code> otherwise.
* @throws MojoExecutionException if any
*/
private boolean preCheck()
throws MojoExecutionException
{
if ( force )
{
return true;
}
if ( outputDirectory != null && !outputDirectory.getAbsolutePath().equals(
getProjectSourceDirectory().getAbsolutePath() ) )
{
return true;
}
if ( !settings.isInteractiveMode() )
{
getLog().error( "Maven is not attempt to interact with the user for input. "
+ "Verify the <interactiveMode/> configuration in your settings." );
return false;
}
getLog().warn( "" );
getLog().warn( " WARRANTY DISCLAIMER" );
getLog().warn( "" );
getLog().warn( "All warranties with regard to this Maven goal are disclaimed!" );
getLog().warn( "The changes will be done directly in the source code." );
getLog().warn( "The Maven Team strongly recommends the use of a SCM software BEFORE executing this goal." );
getLog().warn( "" );
while ( true )
{
getLog().info( "Are you sure to proceed? [Y]es [N]o" );
try
{
String userExpression = inputHandler.readLine();
if ( userExpression == null || JavadocUtil.equalsIgnoreCase( userExpression, "Y", "Yes" ) )
{
getLog().info( "OK, let's proceed..." );
break;
}
if ( JavadocUtil.equalsIgnoreCase( userExpression, "N", "No" ) )
{
getLog().info( "No changes in your sources occur." );
return false;
}
}
catch ( IOException e )
{
throw new MojoExecutionException( "Unable to read from standard input.", e );
}
}
return true;
}
/**
* @return the source dir as File for the given project
*/
private File getProjectSourceDirectory()
{
return new File( project.getBuild().getSourceDirectory() );
}
/**
* Invoke Maven to run clirr-maven-plugin to find API differences.
*
* @throws MavenInvocationException if any
*/
private void executeClirr()
throws MavenInvocationException
{
if ( ignoreClirr )
{
getLog().info( "Clirr is ignored." );
return;
}
String clirrGoal = getFullClirrGoal();
// http://mojo.codehaus.org/clirr-maven-plugin/check-mojo.html
File clirrTextOutputFile =
FileUtils.createTempFile( "clirr", ".txt", new File( project.getBuild().getDirectory() ) );
Properties properties = new Properties();
properties.put( "textOutputFile", clirrTextOutputFile.getAbsolutePath() );
properties.put( "comparisonVersion", comparisonVersion );
properties.put( "failOnError", "false" );
if ( JavaVersion.JAVA_SPECIFICATION_VERSION.isBefore( "8" ) )
{
// ensure that Java7 picks up TLSv1.2 when connecting with Central
properties.put( "https.protocols", "TLSv1.2" );
}
File invokerDir = new File( project.getBuild().getDirectory(), "invoker" );
invokerDir.mkdirs();
File invokerLogFile = FileUtils.createTempFile( "clirr-maven-plugin", ".txt", invokerDir );
new File( project.getBuild().getDirectory(), "invoker-clirr-maven-plugin.txt" );
JavadocUtil.invokeMaven( getLog(), new File( localRepository.getBasedir() ), project.getFile(),
Collections.singletonList( clirrGoal ), properties, invokerLogFile,
session.getRequest().getGlobalSettingsFile() );
try
{
if ( invokerLogFile.exists() )
{
String invokerLogContent =
StringUtils.unifyLineSeparators( FileUtils.fileRead( invokerLogFile, "UTF-8" ) );
// see org.codehaus.mojo.clirr.AbstractClirrMojo#getComparisonArtifact()
final String artifactNotFoundMsg = "Unable to find a previous version of the project in the repository";
if ( invokerLogContent.contains( artifactNotFoundMsg ) )
{
getLog().warn( "No previous artifact has been deployed, Clirr is ignored." );
return;
}
}
}
catch ( IOException e )
{
getLog().debug( "IOException: " + e.getMessage() );
}
try
{
parseClirrTextOutputFile( clirrTextOutputFile );
}
catch ( IOException e )
{
if ( getLog().isDebugEnabled() )
{
getLog().debug( "IOException: " + e.getMessage(), e );
}
getLog().info( "IOException when parsing Clirr output '" + clirrTextOutputFile.getAbsolutePath()
+ "', Clirr is ignored." );
}
}
/**
* @param clirrTextOutputFile not null
* @throws IOException if any
*/
private void parseClirrTextOutputFile( File clirrTextOutputFile )
throws IOException
{
if ( !clirrTextOutputFile.exists() )
{
if ( getLog().isInfoEnabled() )
{
getLog().info(
"No Clirr output file '" + clirrTextOutputFile.getAbsolutePath() + "' exists, Clirr is ignored." );
}
return;
}
if ( getLog().isInfoEnabled() )
{
getLog().info( "Clirr output file was created: " + clirrTextOutputFile.getAbsolutePath() );
}
clirrNewClasses = new LinkedList<>();
clirrNewMethods = new LinkedHashMap<>();
BufferedReader reader = null;
try
{
reader = new BufferedReader( ReaderFactory.newReader( clirrTextOutputFile, "UTF-8" ) );
for ( String line = reader.readLine(); line != null; line = reader.readLine() )
{
String[] split = StringUtils.split( line, ":" );
if ( split.length != 4 )
{
if ( getLog().isDebugEnabled() )
{
getLog().debug( "Unable to parse the clirr line: " + line );
}
continue;
}
int code;
try
{
code = Integer.parseInt( split[1].trim() );
}
catch ( NumberFormatException e )
{
if ( getLog().isDebugEnabled() )
{
getLog().debug( "Unable to parse the clirr line: " + line );
}
continue;
}
// http://clirr.sourceforge.net/clirr-core/exegesis.html
// 7011 - Method Added
// 7012 - Method Added to Interface
// 8000 - Class Added
List<String> list;
String[] splits2;
// CHECKSTYLE_OFF: MagicNumber
switch ( code )
{
case 7011:
list = clirrNewMethods.get( split[2].trim() );
if ( list == null )
{
list = new ArrayList<>();
}
splits2 = StringUtils.split( split[3].trim(), "'" );
if ( splits2.length != 3 )
{
continue;
}
list.add( splits2[1].trim() );
clirrNewMethods.put( split[2].trim(), list );
break;
case 7012:
list = clirrNewMethods.get( split[2].trim() );
if ( list == null )
{
list = new ArrayList<>();
}
splits2 = StringUtils.split( split[3].trim(), "'" );
if ( splits2.length != 3 )
{
continue;
}
list.add( splits2[1].trim() );
clirrNewMethods.put( split[2].trim(), list );
break;
case 8000:
clirrNewClasses.add( split[2].trim() );
break;
default:
break;
}
// CHECKSTYLE_ON: MagicNumber
}
reader.close();
reader = null;
}
finally
{
IOUtils.closeQuietly( reader );
}
if ( clirrNewClasses.isEmpty() && clirrNewMethods.isEmpty() )
{
getLog().info( "Clirr NOT found API differences." );
}
else
{
getLog().info( "Clirr found API differences, i.e. new classes/interfaces or methods." );
}
}
/**
* @param tag not null
* @return <code>true</code> if <code>tag</code> is defined in {@link #fixTags}.
*/
private boolean fixTag( String tag )
{
if ( fixTagsSplitted.length == 1 && fixTagsSplitted[0].equals( FIX_TAGS_ALL ) )
{
return true;
}
for ( String aFixTagsSplitted : fixTagsSplitted )
{
if ( aFixTagsSplitted.trim().equals( tag ) )
{
return true;
}
}
return false;
}
/**
* Calling Qdox to find {@link JavaClass} objects from the Maven project sources.
* Ignore java class if Qdox has parsing errors.
*
* @return an array of {@link JavaClass} found by QDox
* @throws IOException if any
* @throws MojoExecutionException if any
*/
private Collection<JavaClass> getQdoxClasses()
throws IOException, MojoExecutionException
{
if ( "pom".equalsIgnoreCase( project.getPackaging() ) )
{
getLog().warn( "This project has 'pom' packaging, no Java sources is available." );
return null;
}
List<File> javaFiles = new LinkedList<>();
for ( String sourceRoot : getProjectSourceRoots( project ) )
{
File f = new File( sourceRoot );
if ( f.isDirectory() )
{
javaFiles.addAll( FileUtils.getFiles( f, includes, excludes, true ) );
}
else
{
if ( getLog().isWarnEnabled() )
{
getLog().warn( f + " doesn't exist. Ignored it." );
}
}
}
ClassLibraryBuilder classLibraryBuilder = new OrderedClassLibraryBuilder();
classLibraryBuilder.appendClassLoader( getProjectClassLoader() );
JavaProjectBuilder builder = new JavaProjectBuilder( classLibraryBuilder );
builder.setEncoding( encoding );
for ( File f : javaFiles )
{
if ( !f.getAbsolutePath().toLowerCase( Locale.ENGLISH ).endsWith( ".java" ) && getLog().isWarnEnabled() )
{
getLog().warn( "'" + f + "' is not a Java file. Ignored it." );
continue;
}
try
{
builder.addSource( f );
}
catch ( ParseException e )
{
if ( getLog().isWarnEnabled() )
{
getLog().warn( "QDOX ParseException: " + e.getMessage() + ". Can't fix it." );
}
}
}
return builder.getClasses();
}
/**
* @return the classLoader for the given project using lazy instantiation.
* @throws MojoExecutionException if any
*/
private ClassLoader getProjectClassLoader()
throws MojoExecutionException
{
if ( projectClassLoader == null )
{
List<String> classPath;
try
{
classPath = getCompileClasspathElements( project );
}
catch ( DependencyResolutionRequiredException e )
{
throw new MojoExecutionException( "DependencyResolutionRequiredException: " + e.getMessage(), e );
}
List<URL> urls = new ArrayList<>( classPath.size() );
for ( String filename : classPath )
{
try
{
urls.add( new File( filename ).toURL() );
}
catch ( MalformedURLException e )
{
throw new MojoExecutionException( "MalformedURLException: " + e.getMessage(), e );
}
}
projectClassLoader = new URLClassLoader( urls.toArray( new URL[urls.size()] ), null );
}
return projectClassLoader;
}
/**
* Process the given {@link JavaClass}, ie add missing javadoc tags depending user parameters.
*
* @param javaClass not null
* @throws IOException if any
* @throws MojoExecutionException if any
*/
private void processFix( JavaClass javaClass )
throws IOException, MojoExecutionException
{
// Skipping inner classes
if ( javaClass.isInner() )
{
return;
}
File javaFile;
try
{
javaFile = Paths.get( javaClass.getSource().getURL().toURI() ).toFile();
}
catch ( URISyntaxException e )
{
throw new MojoExecutionException( e.getMessage() );
}
// the original java content in memory
final String originalContent = StringUtils.unifyLineSeparators( FileUtils.fileRead( javaFile, encoding ) );
if ( getLog().isDebugEnabled() )
{
getLog().debug( "Analyzing " + javaClass.getFullyQualifiedName() );
}
final StringWriter stringWriter = new StringWriter();
BufferedReader reader = null;
boolean changeDetected = false;
try
{
reader = new BufferedReader( new StringReader( originalContent ) );
int lineNumber = 0;
for ( String line = reader.readLine(); line != null; line = reader.readLine() )
{
lineNumber++;
final String indent = autodetectIndentation( line );
// fixing classes
if ( javaClass.getComment() == null && javaClass.getAnnotations() != null
&& !javaClass.getAnnotations().isEmpty() )
{
if ( lineNumber == javaClass.getAnnotations().get( 0 ).getLineNumber() )
{
changeDetected |= fixClassComment( stringWriter, originalContent, javaClass, indent );
takeCareSingleComment( stringWriter, originalContent, javaClass );
}
}
else if ( lineNumber == javaClass.getLineNumber() )
{
changeDetected |= fixClassComment( stringWriter, originalContent, javaClass, indent );
takeCareSingleComment( stringWriter, originalContent, javaClass );
}
// fixing fields
if ( javaClass.getFields() != null )
{
for ( JavaField field : javaClass.getFields() )
{
if ( lineNumber == field.getLineNumber() )
{
changeDetected |= fixFieldComment( stringWriter, javaClass, field, indent );
}
}
}
// fixing methods
if ( javaClass.getConstructors() != null )
{
for ( JavaConstructor method : javaClass.getConstructors() )
{
if ( lineNumber == method.getLineNumber() )
{
changeDetected |= fixMethodComment( stringWriter, originalContent, method, indent );
takeCareSingleComment( stringWriter, originalContent, method );
}
}
}
// fixing methods
for ( JavaMethod method : javaClass.getMethods() )
{
int methodLineNumber;
if ( method.getComment() == null && !method.getAnnotations().isEmpty() )
{
methodLineNumber = method.getAnnotations().get( 0 ).getLineNumber();
}
else
{
methodLineNumber = method.getLineNumber();
}
if ( lineNumber == methodLineNumber )
{
changeDetected |= fixMethodComment( stringWriter, originalContent, method, indent );
takeCareSingleComment( stringWriter, originalContent, method );
}
}
stringWriter.write( line );
stringWriter.write( EOL );
}
reader.close();
reader = null;
}
finally
{
IOUtil.close( reader );
}
if ( changeDetected )
{
if ( getLog().isInfoEnabled() )
{
getLog().info( "Saving changes to " + javaClass.getFullyQualifiedName() );
}
if ( outputDirectory != null && !outputDirectory.getAbsolutePath().equals(
getProjectSourceDirectory().getAbsolutePath() ) )
{
String path = StringUtils.replace( javaFile.getAbsolutePath().replaceAll( "\\\\", "/" ),
project.getBuild().getSourceDirectory().replaceAll( "\\\\", "/" ), "" );
javaFile = new File( outputDirectory, path );
javaFile.getParentFile().mkdirs();
}
writeFile( javaFile, encoding, stringWriter.toString() );
}
else
{
if ( getLog().isDebugEnabled() )
{
getLog().debug( "No changes made to " + javaClass.getFullyQualifiedName() );
}
}
}
/**
* Take care of block or single comments between Javadoc comment and entity declaration ie:
* <br/>
* <code>
* <font color="#808080">1</font>&nbsp;<font color="#ffffff">&nbsp;</font>
* <font color="#3f5fbf">&#47;&#42;&#42;</font><br />
* <font color="#808080">2</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;</font>
* <font color="#3f5fbf">&#42;&nbsp;{Javadoc&nbsp;Comment}</font><br />
* <font color="#808080">3</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;</font>
* <font color="#3f5fbf">&#42;&#47;</font><br />
* <font color="#808080">4</font>&nbsp;<font color="#ffffff">&nbsp;</font>
* <font color="#3f7f5f">&#47;&#42;</font><br />
* <font color="#808080">5</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;</font>
* <font color="#3f7f5f">&#42;&nbsp;{Block&nbsp;Comment}</font><br />
* <font color="#808080">6</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;</font>
* <font color="#3f7f5f">&#42;&#47;</font><br />
* <font color="#808080">7</font>&nbsp;<font color="#ffffff">&nbsp;</font>
* <font color="#3f7f5f">&#47;&#47;&nbsp;{Single&nbsp;comment}</font><br />
* <font color="#808080">8</font>&nbsp;<font color="#ffffff">&nbsp;</font>
* <font color="#7f0055"><b>public&nbsp;</b></font><font color="#7f0055"><b>void&nbsp;</b></font>
* <font color="#000000">dummyMethod</font><font color="#000000">(&nbsp;</font>
* <font color="#000000">String&nbsp;s&nbsp;</font><font color="#000000">){}</font>
* </code>
*
* @param stringWriter not null
* @param originalContent not null
* @param entity not null
* @throws IOException if any
* @see #extractOriginalJavadoc
*/
private void takeCareSingleComment( final StringWriter stringWriter, final String originalContent,
final JavaAnnotatedElement entity )
throws IOException
{
if ( entity.getComment() == null )
{
return;
}
String javadocComment = trimRight( extractOriginalJavadoc( originalContent, entity ) );
String extraComment = javadocComment.substring( javadocComment.indexOf( END_JAVADOC ) + END_JAVADOC.length() );
if ( StringUtils.isNotEmpty( extraComment ) )
{
if ( extraComment.contains( EOL ) )
{
stringWriter.write( extraComment.substring( extraComment.indexOf( EOL ) + EOL.length() ) );
}
else
{
stringWriter.write( extraComment );
}
stringWriter.write( EOL );
}
}
/**
* Add/update Javadoc class comment.
*
* @param stringWriter
* @param originalContent
* @param javaClass
* @param indent
* @return {@code true} if the comment is updated, otherwise {@code false}
* @throws MojoExecutionException
* @throws IOException
*/
private boolean fixClassComment( final StringWriter stringWriter, final String originalContent,
final JavaClass javaClass, final String indent )
throws MojoExecutionException, IOException
{
if ( !fixClassComment )
{
return false;
}
if ( !isInLevel( javaClass.getModifiers() ) )
{
return false;
}
// add
if ( javaClass.getComment() == null )
{
addDefaultClassComment( stringWriter, javaClass, indent );
return true;
}
// update
return updateEntityComment( stringWriter, originalContent, javaClass, indent );
}
/**
* @param modifiers list of modifiers (public, private, protected, package)
* @return <code>true</code> if modifier is align with <code>level</code>.
*/
private boolean isInLevel( List<String> modifiers )
{
if ( LEVEL_PUBLIC.equalsIgnoreCase( level.trim() ) )
{
return modifiers.contains( LEVEL_PUBLIC );
}
if ( LEVEL_PROTECTED.equalsIgnoreCase( level.trim() ) )
{
return ( modifiers.contains( LEVEL_PUBLIC ) || modifiers.contains( LEVEL_PROTECTED ) );
}
if ( LEVEL_PACKAGE.equalsIgnoreCase( level.trim() ) )
{
return !modifiers.contains( LEVEL_PRIVATE );
}
// should be private (shows all classes and members)
return true;
}
/**
* Add a default Javadoc for the given class, i.e.:
* <br/>
* <code>
* <font color="#808080">1</font>&nbsp;<font color="#ffffff">&nbsp;</font>
* <font color="#3f5fbf">&#47;&#42;&#42;</font><br />
* <font color="#808080">2</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;</font>
* <font color="#3f5fbf">&#42;&nbsp;{Comment&nbsp;based&nbsp;on&nbsp;the&nbsp;class&nbsp;name}</font><br />
* <font color="#808080">3</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;</font>
* <font color="#3f5fbf">&#42;</font><br />
* <font color="#808080">4</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;</font>
* <font color="#3f5fbf">&#42;&nbsp;</font><font color="#7f9fbf">@author&nbsp;</font>
* <font color="#3f5fbf">X&nbsp;{added&nbsp;if&nbsp;addMissingAuthor}</font><br />
* <font color="#808080">5</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;</font>
* <font color="#3f5fbf">&#42;&nbsp;</font><font color="#7f9fbf">@version&nbsp;</font>
* <font color="#3f5fbf">X&nbsp;{added&nbsp;if&nbsp;addMissingVersion}</font><br />
* <font color="#808080">6</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;</font>
* <font color="#3f5fbf">&#42;&nbsp;</font><font color="#7f9fbf">@since&nbsp;</font>
* <font color="#3f5fbf">X&nbsp;{added&nbsp;if&nbsp;addMissingSince&nbsp;and&nbsp;new&nbsp;classes
* from&nbsp;previous&nbsp;version}</font><br />
* <font color="#808080">7</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;</font>
* <font color="#3f5fbf">&#42;&#47;</font><br />
* <font color="#808080">8</font>&nbsp;<font color="#7f0055"><b>public&nbsp;class&nbsp;</b></font>
* <font color="#000000">DummyClass&nbsp;</font><font color="#000000">{}</font></code>
* </code>
*
* @param stringWriter not null
* @param javaClass not null
* @param indent not null
* @see #getDefaultClassJavadocComment(JavaClass)
* @see #appendDefaultAuthorTag(StringBuilder, String)
* @see #appendDefaultSinceTag(StringBuilder, String)
* @see #appendDefaultVersionTag(StringBuilder, String)
*/
private void addDefaultClassComment( final StringWriter stringWriter, final JavaClass javaClass,
final String indent )
{
StringBuilder sb = new StringBuilder();
sb.append( indent ).append( START_JAVADOC );
sb.append( EOL );
sb.append( indent ).append( SEPARATOR_JAVADOC );
sb.append( getDefaultClassJavadocComment( javaClass ) );
sb.append( EOL );
appendSeparator( sb, indent );
appendDefaultAuthorTag( sb, indent );
appendDefaultVersionTag( sb, indent );
if ( fixTag( SINCE_TAG ) )
{
if ( !ignoreClirr )
{
if ( isNewClassFromLastVersion( javaClass ) )
{
appendDefaultSinceTag( sb, indent );
}
}
else
{
appendDefaultSinceTag( sb, indent );
addSinceClasses( javaClass );
}
}
sb.append( indent ).append( " " ).append( END_JAVADOC );
sb.append( EOL );
stringWriter.write( sb.toString() );
}
/**
* Add Javadoc field comment, only for static fields or interface fields.
*
* @param stringWriter not null
* @param javaClass not null
* @param field not null
* @param indent not null
* @return {@code true} if comment was updated, otherwise {@code false}
* @throws IOException if any
*/
private boolean fixFieldComment( final StringWriter stringWriter, final JavaClass javaClass, final JavaField field,
final String indent )
throws IOException
{
if ( !fixFieldComment )
{
return false;
}
if ( !javaClass.isInterface() && ( !isInLevel( field.getModifiers() ) || !field.isStatic() ) )
{
return false;
}
// add
if ( field.getComment() == null )
{
addDefaultFieldComment( stringWriter, field, indent );
return true;
}
// no update
return false;
}
/**
* Add a default Javadoc for the given field, i.e.:
* <br/>
* <code>
* <font color="#808080">1</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;</font>
* <font color="#3f5fbf">&#47;&#42;&#42;&nbsp;Constant&nbsp;</font><font color="#7f7f9f">&lt;code&gt;</font>
* <font color="#3f5fbf">MY_STRING_CONSTANT=&#34;value&#34;</font>
* <font color="#7f7f9f">&lt;/code&gt;&nbsp;</font><font color="#3f5fbf">&#42;&#47;</font><br />
* <font color="#808080">2</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;</font>
* <font color="#7f0055"><b>public&nbsp;static&nbsp;final&nbsp;</b></font>
* <font color="#000000">String&nbsp;MY_STRING_CONSTANT&nbsp;=&nbsp;</font>
* <font color="#2a00ff">&#34;value&#34;</font><font color="#000000">;</font>
* </code>
*
* @param stringWriter not null
* @param field not null
* @param indent not null
* @throws IOException if any
*/
private void addDefaultFieldComment( final StringWriter stringWriter, final JavaField field, final String indent )
throws IOException
{
StringBuilder sb = new StringBuilder();
sb.append( indent ).append( START_JAVADOC ).append( " " );
sb.append( "Constant <code>" ).append( field.getName() );
if ( StringUtils.isNotEmpty( field.getInitializationExpression() ) )
{
String qualifiedName = field.getType().getFullyQualifiedName();
if ( qualifiedName.equals( Byte.TYPE.toString() ) || qualifiedName.equals( Short.TYPE.toString() )
|| qualifiedName.equals( Integer.TYPE.toString() ) || qualifiedName.equals( Long.TYPE.toString() )
|| qualifiedName.equals( Float.TYPE.toString() ) || qualifiedName.equals( Double.TYPE.toString() )
|| qualifiedName.equals( Boolean.TYPE.toString() ) || qualifiedName.equals(
Character.TYPE.toString() ) )
{
sb.append( "=" );
sb.append( field.getInitializationExpression().trim() );
}
if ( qualifiedName.equals( String.class.getName() ) )
{
StringBuilder value = new StringBuilder();
String[] lines = getLines( field.getInitializationExpression() );
for ( String line : lines )
{
StringTokenizer token = new StringTokenizer( line.trim(), "\"\n\r" );
while ( token.hasMoreTokens() )
{
String s = token.nextToken();
if ( s.trim().equals( "+" ) )
{
continue;
}
if ( s.trim().endsWith( "\\" ) )
{
s += "\"";
}
value.append( s );
}
}
sb.append( "=\"" );
// reduce the size
// CHECKSTYLE_OFF: MagicNumber
if ( value.length() < 40 )
{
sb.append( value.toString() ).append( "\"" );
}
else
{
sb.append( value.toString(), 0, 39 ).append( "\"{trunked}" );
}
// CHECKSTYLE_ON: MagicNumber
}
}
sb.append( "</code> " ).append( END_JAVADOC );
sb.append( EOL );
stringWriter.write( sb.toString() );
}
/**
* Add/update Javadoc method comment.
*
* @param stringWriter not null
* @param originalContent not null
* @param javaExecutable not null
* @param indent not null
* @return {@code true} if comment was updated, otherwise {@code false}
* @throws MojoExecutionException if any
* @throws IOException if any
*/
private boolean fixMethodComment( final StringWriter stringWriter, final String originalContent,
final JavaExecutable javaExecutable, final String indent )
throws MojoExecutionException, IOException
{
if ( !fixMethodComment )
{
return false;
}
if ( !javaExecutable.getDeclaringClass().isInterface() && !isInLevel( javaExecutable.getModifiers() ) )
{
return false;
}
// add
if ( javaExecutable.getComment() == null )
{
addDefaultMethodComment( stringWriter, javaExecutable, indent );
return true;
}
// update
return updateEntityComment( stringWriter, originalContent, javaExecutable, indent );
}
/**
* Add in the buffer a default Javadoc for the given class:
* <br/>
* <code>
* <font color="#808080">1</font>&nbsp;<font color="#ffffff">&nbsp;</font>
* <font color="#3f5fbf">&#47;&#42;&#42;</font><br />
* <font color="#808080">2</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;</font>
* <font color="#3f5fbf">&#42;&nbsp;{Comment&nbsp;based&nbsp;on&nbsp;the&nbsp;method&nbsp;name}</font><br />
* <font color="#808080">3</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;</font>
* <font color="#3f5fbf">&#42;</font><br />
* <font color="#808080">4</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;</font>
* <font color="#3f5fbf">&#42;&nbsp;</font><font color="#7f9fbf">@param&nbsp;</font>
* <font color="#3f5fbf">X&nbsp;{added&nbsp;if&nbsp;addMissingParam}</font><br />
* <font color="#808080">5</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;</font>
* <font color="#3f5fbf">&#42;&nbsp;</font><font color="#7f9fbf">@return&nbsp;</font>
* <font color="#3f5fbf">X&nbsp;{added&nbsp;if&nbsp;addMissingReturn}</font><br />
* <font color="#808080">6</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;</font>
* <font color="#3f5fbf">&#42;&nbsp;</font><font color="#7f9fbf">@throws&nbsp;</font>
* <font color="#3f5fbf">X&nbsp;{added&nbsp;if&nbsp;addMissingThrows}</font><br />
* <font color="#808080">7</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;</font>
* <font color="#3f5fbf">&#42;&nbsp;</font><font color="#7f9fbf">@since&nbsp;</font>
* <font color="#3f5fbf">X&nbsp;{added&nbsp;if&nbsp;addMissingSince&nbsp;and&nbsp;new&nbsp;classes
* from&nbsp;previous&nbsp;version}</font><br />
* <font color="#808080">8</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;</font>
* <font color="#3f5fbf">&#42;&#47;</font><br />
* <font color="#808080">9</font>&nbsp;<font color="#7f0055"><b>public&nbsp;</b></font>
* <font color="#7f0055"><b>void&nbsp;</b></font><font color="#000000">dummyMethod</font>
* <font color="#000000">(&nbsp;</font><font color="#000000">String&nbsp;s&nbsp;</font>
* <font color="#000000">){}</font>
* </code>
*
* @param stringWriter not null
* @param javaExecutable not null
* @param indent not null
* @throws MojoExecutionException if any
* @see #getDefaultMethodJavadocComment
* @see #appendDefaultSinceTag(StringBuilder, String)
*/
private void addDefaultMethodComment( final StringWriter stringWriter, final JavaExecutable javaExecutable,
final String indent )
throws MojoExecutionException
{
StringBuilder sb = new StringBuilder();
// special case
if ( isInherited( javaExecutable ) )
{
sb.append( indent ).append( INHERITED_JAVADOC );
sb.append( EOL );
stringWriter.write( sb.toString() );
return;
}
sb.append( indent ).append( START_JAVADOC );
sb.append( EOL );
sb.append( indent ).append( SEPARATOR_JAVADOC );
sb.append( getDefaultMethodJavadocComment( javaExecutable ) );
sb.append( EOL );
boolean separatorAdded = false;
if ( fixTag( PARAM_TAG ) )
{
if ( javaExecutable.getParameters() != null )
{
for ( JavaParameter javaParameter : javaExecutable.getParameters() )
{
separatorAdded = appendDefaultParamTag( sb, indent, separatorAdded, javaParameter );
}
}
// is generic?
if ( javaExecutable.getTypeParameters() != null )
{
for ( JavaTypeVariable<JavaGenericDeclaration> typeParam : javaExecutable.getTypeParameters() )
{
separatorAdded = appendDefaultParamTag( sb, indent, separatorAdded, typeParam );
}
}
}
if ( javaExecutable instanceof JavaMethod )
{
JavaMethod javaMethod = (JavaMethod) javaExecutable;
if ( fixTag( RETURN_TAG ) && javaMethod.getReturns() != null && !javaMethod.getReturns().isVoid() )
{
separatorAdded = appendDefaultReturnTag( sb, indent, separatorAdded, javaMethod );
}
}
if ( fixTag( THROWS_TAG ) && javaExecutable.getExceptions() != null )
{
for ( JavaType exception : javaExecutable.getExceptions() )
{
separatorAdded = appendDefaultThrowsTag( sb, indent, separatorAdded, exception );
}
}
if ( fixTag( SINCE_TAG ) && isNewMethodFromLastRevision( javaExecutable ) )
{
separatorAdded = appendDefaultSinceTag( sb, indent, separatorAdded );
}
sb.append( indent ).append( " " ).append( END_JAVADOC );
sb.append( EOL );
stringWriter.write( sb.toString() );
}
/**
* @param stringWriter not null
* @param originalContent not null
* @param entity not null
* @param indent not null
* @return the updated changeDetected flag
* @throws MojoExecutionException if any
* @throws IOException if any
*/
private boolean updateEntityComment( final StringWriter stringWriter, final String originalContent,
final JavaAnnotatedElement entity, final String indent )
throws MojoExecutionException, IOException
{
boolean changeDetected = false;
String old = null;
String s = stringWriter.toString();
int i = s.lastIndexOf( START_JAVADOC );
if ( i != -1 )
{
String tmp = s.substring( 0, i );
if ( tmp.lastIndexOf( EOL ) != -1 )
{
tmp = tmp.substring( 0, tmp.lastIndexOf( EOL ) );
}
old = stringWriter.getBuffer().substring( i );
stringWriter.getBuffer().delete( 0, stringWriter.getBuffer().length() );
stringWriter.write( tmp );
stringWriter.write( EOL );
}
else
{
changeDetected = true;
}
updateJavadocComment( stringWriter, originalContent, entity, indent );
if ( changeDetected )
{
return true; // return now if we already know there's a change
}
return !stringWriter.getBuffer().substring( i ).equals( old );
}
/**
* @param stringWriter not null
* @param originalContent not null
* @param entity not null
* @param indent not null
* @throws MojoExecutionException if any
* @throws IOException if any
*/
private void updateJavadocComment( final StringWriter stringWriter, final String originalContent,
final JavaAnnotatedElement entity, final String indent )
throws MojoExecutionException, IOException
{
if ( entity.getComment() == null && ( entity.getTags() == null || entity.getTags().isEmpty() ) )
{
return;
}
boolean isJavaExecutable = false;
if ( entity instanceof JavaExecutable )
{
isJavaExecutable = true;
}
StringBuilder sb = new StringBuilder();
// special case for inherited method
if ( isJavaExecutable )
{
JavaExecutable javaMethod = (JavaExecutable) entity;
if ( isInherited( javaMethod ) )
{
// QDOX-154 could be empty
if ( StringUtils.isEmpty( javaMethod.getComment() ) )
{
sb.append( indent ).append( INHERITED_JAVADOC );
sb.append( EOL );
stringWriter.write( sb.toString() );
return;
}
String javadoc = getJavadocComment( originalContent, javaMethod );
// case: /** {@inheritDoc} */ or no tags
if ( hasInheritedTag( javadoc ) && ( javaMethod.getTags() == null
|| javaMethod.getTags().isEmpty() ) )
{
sb.append( indent ).append( INHERITED_JAVADOC );
sb.append( EOL );
stringWriter.write( sb.toString() );
return;
}
if ( javadoc.contains( START_JAVADOC ) )
{
javadoc = javadoc.substring( javadoc.indexOf( START_JAVADOC ) + START_JAVADOC.length() );
}
if ( javadoc.contains( END_JAVADOC ) )
{
javadoc = javadoc.substring( 0, javadoc.indexOf( END_JAVADOC ) );
}
sb.append( indent ).append( START_JAVADOC );
sb.append( EOL );
if ( !javadoc.contains( INHERITED_TAG ) )
{
sb.append( indent ).append( SEPARATOR_JAVADOC ).append( INHERITED_TAG );
sb.append( EOL );
appendSeparator( sb, indent );
}
javadoc = removeLastEmptyJavadocLines( javadoc );
javadoc = alignIndentationJavadocLines( javadoc, indent );
sb.append( javadoc );
sb.append( EOL );
if ( javaMethod.getTags() != null )
{
for ( DocletTag docletTag : javaMethod.getTags() )
{
// Voluntary ignore these tags
if ( JavadocUtil.equals( docletTag.getName(), PARAM_TAG, RETURN_TAG, THROWS_TAG ) )
{
continue;
}
String s = getJavadocComment( originalContent, entity, docletTag );
s = removeLastEmptyJavadocLines( s );
s = alignIndentationJavadocLines( s, indent );
sb.append( s );
sb.append( EOL );
}
}
sb.append( indent ).append( " " ).append( END_JAVADOC );
sb.append( EOL );
if ( hasInheritedTag( sb.toString().trim() ) )
{
sb = new StringBuilder();
sb.append( indent ).append( INHERITED_JAVADOC );
sb.append( EOL );
stringWriter.write( sb.toString() );
return;
}
stringWriter.write( sb.toString() );
return;
}
}
sb.append( indent ).append( START_JAVADOC );
sb.append( EOL );
// comment
if ( StringUtils.isNotEmpty( entity.getComment() ) )
{
updateJavadocComment( sb, originalContent, entity, indent );
}
else
{
addDefaultJavadocComment( sb, entity, indent, isJavaExecutable );
}
// tags
if ( entity.getTags() != null && !entity.getTags().isEmpty() )
{
updateJavadocTags( sb, originalContent, entity, indent, isJavaExecutable );
}
else
{
addDefaultJavadocTags( sb, entity, indent, isJavaExecutable );
}
sb = new StringBuilder( removeLastEmptyJavadocLines( sb.toString() ) ).append( EOL );
sb.append( indent ).append( " " ).append( END_JAVADOC );
sb.append( EOL );
stringWriter.write( sb.toString() );
}
/**
* @param sb not null
* @param originalContent not null
* @param entity not null
* @param indent not null
* @throws IOException if any
*/
private void updateJavadocComment( final StringBuilder sb, final String originalContent,
final JavaAnnotatedElement entity, final String indent )
throws IOException
{
String comment = getJavadocComment( originalContent, entity );
comment = removeLastEmptyJavadocLines( comment );
comment = alignIndentationJavadocLines( comment, indent );
if ( comment.contains( START_JAVADOC ) )
{
comment = comment.substring( comment.indexOf( START_JAVADOC ) + START_JAVADOC.length() );
comment = indent + SEPARATOR_JAVADOC + comment.trim();
}
if ( comment.contains( END_JAVADOC ) )
{
comment = comment.substring( 0, comment.indexOf( END_JAVADOC ) );
}
if ( fixTag( LINK_TAG ) )
{
comment = replaceLinkTags( comment, entity );
}
String[] lines = getLines( comment );
for ( String line : lines )
{
sb.append( indent ).append( " " ).append( line.trim() );
sb.append( EOL );
}
}
static String replaceLinkTags( String comment, JavaAnnotatedElement entity )
{
StringBuilder resolvedComment = new StringBuilder();
// scan comment for {@link someClassName} and try to resolve this
Matcher linktagMatcher = Pattern.compile( "\\{@link\\s" ).matcher( comment );
int startIndex = 0;
while ( linktagMatcher.find() )
{
int startName = linktagMatcher.end();
resolvedComment.append( comment, startIndex, startName );
int endName = comment.indexOf( "}", startName );
if ( endName >= 0 )
{
String name;
String link = comment.substring( startName, endName );
int hashIndex = link.indexOf( '#' );
if ( hashIndex >= 0 )
{
name = link.substring( 0, hashIndex );
}
else
{
name = link;
}
if ( StringUtils.isNotBlank( name ) )
{
String typeName;
if ( entity instanceof JavaClass )
{
JavaClass clazz = (JavaClass) entity;
typeName =
TypeResolver.byClassName( clazz.getBinaryName(), clazz.getJavaClassLibrary(),
clazz.getSource().getImports() ).resolveType( name.trim() );
}
else if ( entity instanceof JavaMember )
{
JavaClass clazz = ( (JavaMember) entity ).getDeclaringClass();
typeName =
TypeResolver.byClassName( clazz.getBinaryName(), clazz.getJavaClassLibrary(),
clazz.getSource().getImports() ).resolveType( name.trim() );
}
else
{
typeName = null;
}
if ( typeName == null )
{
typeName = name.trim();
}
else
{
typeName = typeName.replaceAll( "\\$", "." );
}
//adjust name for inner classes
resolvedComment.append( typeName );
}
if ( hashIndex >= 0 )
{
resolvedComment.append( link.substring( hashIndex ).trim() );
}
startIndex = endName;
}
else
{
startIndex = startName;
}
}
resolvedComment.append( comment.substring( startIndex ) );
return resolvedComment.toString();
}
/**
* @param sb not null
* @param entity not null
* @param indent not null
* @param isJavaExecutable
*/
private void addDefaultJavadocComment( final StringBuilder sb, final JavaAnnotatedElement entity,
final String indent, final boolean isJavaExecutable )
{
sb.append( indent ).append( SEPARATOR_JAVADOC );
if ( isJavaExecutable )
{
sb.append( getDefaultMethodJavadocComment( (JavaExecutable) entity ) );
}
else
{
sb.append( getDefaultClassJavadocComment( (JavaClass) entity ) );
}
sb.append( EOL );
}
/**
* @param sb not null
* @param originalContent not null
* @param entity not null
* @param indent not null
* @param isJavaExecutable
* @throws IOException if any
* @throws MojoExecutionException if any
*/
private void updateJavadocTags( final StringBuilder sb, final String originalContent,
final JavaAnnotatedElement entity, final String indent,
final boolean isJavaExecutable )
throws IOException, MojoExecutionException
{
appendSeparator( sb, indent );
// parse tags
JavaEntityTags javaEntityTags = parseJavadocTags( originalContent, entity, indent, isJavaExecutable );
// update and write tags
updateJavadocTags( sb, entity, isJavaExecutable, javaEntityTags );
// add missing tags...
addMissingJavadocTags( sb, entity, indent, isJavaExecutable, javaEntityTags );
}
/**
* Parse entity tags
*
* @param originalContent not null
* @param entity not null
* @param indent not null
* @param isJavaMethod
* @return an instance of {@link JavaEntityTags}
* @throws IOException if any
*/
JavaEntityTags parseJavadocTags( final String originalContent, final JavaAnnotatedElement entity,
final String indent, final boolean isJavaMethod )
throws IOException
{
JavaEntityTags javaEntityTags = new JavaEntityTags( entity, isJavaMethod );
for ( DocletTag docletTag : entity.getTags() )
{
String originalJavadocTag = getJavadocComment( originalContent, entity, docletTag );
originalJavadocTag = removeLastEmptyJavadocLines( originalJavadocTag );
originalJavadocTag = alignIndentationJavadocLines( originalJavadocTag, indent );
javaEntityTags.getNamesTags().add( docletTag.getName() );
if ( isJavaMethod )
{
List<String> params = docletTag.getParameters();
if ( params.size() < 1 )
{
continue;
}
String paramName = params.get( 0 );
switch ( docletTag.getName() )
{
case PARAM_TAG:
javaEntityTags.putJavadocParamTag( paramName, docletTag.getValue(), originalJavadocTag );
break;
case RETURN_TAG:
javaEntityTags.setJavadocReturnTag( originalJavadocTag );
break;
case THROWS_TAG:
javaEntityTags.putJavadocThrowsTag( paramName, originalJavadocTag );
break;
default:
javaEntityTags.getUnknownTags().add( originalJavadocTag );
break;
}
}
else
{
javaEntityTags.getUnknownTags().add( originalJavadocTag );
}
}
return javaEntityTags;
}
/**
* Write tags according javaEntityTags.
*
* @param sb not null
* @param entity not null
* @param isJavaExecutable
* @param javaEntityTags not null
*/
private void updateJavadocTags( final StringBuilder sb, final JavaAnnotatedElement entity,
final boolean isJavaExecutable, final JavaEntityTags javaEntityTags )
{
for ( DocletTag docletTag : entity.getTags() )
{
if ( isJavaExecutable )
{
JavaExecutable javaExecutable = (JavaExecutable) entity;
List<String> params = docletTag.getParameters();
if ( params.size() < 1 )
{
continue;
}
if ( docletTag.getName().equals( PARAM_TAG ) )
{
writeParamTag( sb, javaExecutable, javaEntityTags, params.get( 0 ), docletTag.getValue() );
}
else if ( docletTag.getName().equals( RETURN_TAG ) && javaExecutable instanceof JavaMethod )
{
writeReturnTag( sb, (JavaMethod) javaExecutable, javaEntityTags );
}
else if ( docletTag.getName().equals( THROWS_TAG ) )
{
writeThrowsTag( sb, javaExecutable, javaEntityTags, params );
}
else
{
// write unknown tags
for ( Iterator<String> it = javaEntityTags.getUnknownTags().iterator(); it.hasNext(); )
{
String originalJavadocTag = it.next();
String simplified = StringUtils.removeDuplicateWhitespace( originalJavadocTag ).trim();
if ( simplified.contains( "@" + docletTag.getName() ) )
{
it.remove();
sb.append( originalJavadocTag );
sb.append( EOL );
}
}
}
}
else
{
for ( Iterator<String> it = javaEntityTags.getUnknownTags().iterator(); it.hasNext(); )
{
String originalJavadocTag = it.next();
String simplified = StringUtils.removeDuplicateWhitespace( originalJavadocTag ).trim();
if ( simplified.contains( "@" + docletTag.getName() ) )
{
it.remove();
sb.append( originalJavadocTag );
sb.append( EOL );
}
}
}
if ( sb.toString().endsWith( EOL ) )
{
sb.delete( sb.toString().lastIndexOf( EOL ), sb.toString().length() );
}
sb.append( EOL );
}
}
private void writeParamTag( final StringBuilder sb, final JavaExecutable javaExecutable,
final JavaEntityTags javaEntityTags, String paramName, String paramValue )
{
if ( !fixTag( PARAM_TAG ) )
{
// write original param tag if found
String originalJavadocTag = javaEntityTags.getJavadocParamTag( paramValue );
if ( originalJavadocTag != null )
{
sb.append( originalJavadocTag );
}
return;
}
boolean found = false;
JavaParameter javaParam = javaExecutable.getParameterByName( paramName );
if ( javaParam == null )
{
// is generic?
List<JavaTypeVariable<JavaGenericDeclaration>> typeParams = javaExecutable.getTypeParameters();
for ( JavaTypeVariable<JavaGenericDeclaration> typeParam : typeParams )
{
if ( typeParam.getGenericValue().equals( paramName ) )
{
found = true;
}
}
}
else
{
found = true;
}
if ( !found )
{
if ( getLog().isWarnEnabled() )
{
getLog().warn(
"Fixed unknown param '" + paramName + "' defined in " + getJavaMethodAsString( javaExecutable ) );
}
if ( sb.toString().endsWith( EOL ) )
{
sb.delete( sb.toString().lastIndexOf( EOL ), sb.toString().length() );
}
}
else
{
String originalJavadocTag = javaEntityTags.getJavadocParamTag( paramValue );
if ( originalJavadocTag != null )
{
sb.append( originalJavadocTag );
String s = "@" + PARAM_TAG + " " + paramName;
if ( StringUtils.removeDuplicateWhitespace( originalJavadocTag ).trim().endsWith( s ) )
{
sb.append( " " );
sb.append( getDefaultJavadocForType( javaParam.getJavaClass() ) );
}
}
}
}
private void writeReturnTag( final StringBuilder sb, final JavaMethod javaMethod,
final JavaEntityTags javaEntityTags )
{
String originalJavadocTag = javaEntityTags.getJavadocReturnTag();
if ( originalJavadocTag == null )
{
return;
}
if ( !fixTag( RETURN_TAG ) )
{
// write original param tag if found
sb.append( originalJavadocTag );
return;
}
if ( StringUtils.isNotEmpty( originalJavadocTag ) && javaMethod.getReturns() != null
&& !javaMethod.getReturns().isVoid() )
{
sb.append( originalJavadocTag );
if ( originalJavadocTag.trim().endsWith( "@" + RETURN_TAG ) )
{
sb.append( " " );
sb.append( getDefaultJavadocForType( javaMethod.getReturns() ) );
}
}
}
void writeThrowsTag( final StringBuilder sb, final JavaExecutable javaExecutable,
final JavaEntityTags javaEntityTags, final List<String> params )
{
String exceptionClassName = params.get( 0 );
String originalJavadocTag = javaEntityTags.getJavadocThrowsTag( exceptionClassName );
if ( originalJavadocTag == null )
{
return;
}
if ( !fixTag( THROWS_TAG ) )
{
// write original param tag if found
sb.append( originalJavadocTag );
return;
}
if ( javaExecutable.getExceptions() != null )
{
for ( JavaType exception : javaExecutable.getExceptions() )
{
if ( exception.getValue().endsWith( exceptionClassName ) )
{
originalJavadocTag = StringUtils.replace( originalJavadocTag, exceptionClassName,
exception.getFullyQualifiedName() );
if ( StringUtils.removeDuplicateWhitespace( originalJavadocTag ).trim().endsWith(
"@" + THROWS_TAG + " " + exception.getValue() ) )
{
originalJavadocTag += " if any.";
}
sb.append( originalJavadocTag );
// added qualified name
javaEntityTags.putJavadocThrowsTag( exception.getValue(), originalJavadocTag );
return;
}
}
}
Class<?> clazz = getClass( javaExecutable.getDeclaringClass(), exceptionClassName );
if ( clazz != null )
{
if ( RuntimeException.class.isAssignableFrom( clazz ) )
{
sb.append( StringUtils.replace( originalJavadocTag, exceptionClassName, clazz.getName() ) );
// added qualified name
javaEntityTags.putJavadocThrowsTag( clazz.getName(), originalJavadocTag );
}
else if ( Throwable.class.isAssignableFrom( clazz ) )
{
getLog().debug( "Removing '" + originalJavadocTag + "'; Throwable not specified by "
+ getJavaMethodAsString( javaExecutable ) + " and it is not a RuntimeException." );
}
else
{
getLog().debug( "Removing '" + originalJavadocTag + "'; It is not a Throwable" );
}
}
else if ( removeUnknownThrows )
{
getLog().warn( "Ignoring unknown throws '" + exceptionClassName + "' defined on "
+ getJavaMethodAsString( javaExecutable ) );
}
else
{
getLog().warn( "Found unknown throws '" + exceptionClassName + "' defined on "
+ getJavaMethodAsString( javaExecutable ) );
sb.append( originalJavadocTag );
if ( params.size() == 1 )
{
sb.append( " if any." );
}
javaEntityTags.putJavadocThrowsTag( exceptionClassName, originalJavadocTag );
}
}
/**
* Add missing tags not already written.
*
* @param sb not null
* @param entity not null
* @param indent not null
* @param isJavaExecutable
* @param javaEntityTags not null
* @throws MojoExecutionException if any
*/
private void addMissingJavadocTags( final StringBuilder sb, final JavaAnnotatedElement entity,
final String indent, final boolean isJavaExecutable,
final JavaEntityTags javaEntityTags )
throws MojoExecutionException
{
if ( isJavaExecutable )
{
JavaExecutable javaExecutable = (JavaExecutable) entity;
if ( fixTag( PARAM_TAG ) )
{
if ( javaExecutable.getParameters() != null )
{
for ( JavaParameter javaParameter : javaExecutable.getParameters() )
{
if ( !javaEntityTags.hasJavadocParamTag( javaParameter.getName() ) )
{
appendDefaultParamTag( sb, indent, javaParameter );
}
}
}
// is generic?
if ( javaExecutable.getTypeParameters() != null )
{
for ( JavaTypeVariable<JavaGenericDeclaration> typeParam : javaExecutable.getTypeParameters() )
{
if ( !javaEntityTags.hasJavadocParamTag( "<" + typeParam.getName() + ">" ) )
{
appendDefaultParamTag( sb, indent, typeParam );
}
}
}
}
if ( javaExecutable instanceof JavaMethod )
{
JavaMethod javaMethod = (JavaMethod) javaExecutable;
if ( fixTag( RETURN_TAG ) && StringUtils.isEmpty( javaEntityTags.getJavadocReturnTag() )
&& javaMethod.getReturns() != null && !javaMethod.getReturns().isVoid() )
{
appendDefaultReturnTag( sb, indent, javaMethod );
}
}
if ( fixTag( THROWS_TAG ) && javaExecutable.getExceptions() != null )
{
for ( JavaType exception : javaExecutable.getExceptions() )
{
if ( javaEntityTags.getJavadocThrowsTag( exception.getValue(), true ) == null )
{
appendDefaultThrowsTag( sb, indent, exception );
}
}
}
}
else
{
if ( !javaEntityTags.getNamesTags().contains( AUTHOR_TAG ) )
{
appendDefaultAuthorTag( sb, indent );
}
if ( !javaEntityTags.getNamesTags().contains( VERSION_TAG ) )
{
appendDefaultVersionTag( sb, indent );
}
}
if ( fixTag( SINCE_TAG ) && !javaEntityTags.getNamesTags().contains( SINCE_TAG ) )
{
if ( !isJavaExecutable )
{
if ( !ignoreClirr )
{
if ( isNewClassFromLastVersion( (JavaClass) entity ) )
{
appendDefaultSinceTag( sb, indent );
}
}
else
{
appendDefaultSinceTag( sb, indent );
addSinceClasses( (JavaClass) entity );
}
}
else
{
if ( !ignoreClirr )
{
if ( isNewMethodFromLastRevision( (JavaExecutable) entity ) )
{
appendDefaultSinceTag( sb, indent );
}
}
else if ( sinceClasses != null )
{
if ( entity instanceof JavaMember
&& !sinceClassesContains( ( (JavaMember) entity ).getDeclaringClass() ) )
{
appendDefaultSinceTag( sb, indent );
}
else if ( entity instanceof JavaClass
&& !sinceClassesContains( ( (JavaClass) entity ).getDeclaringClass() ) )
{
appendDefaultSinceTag( sb, indent );
}
}
}
}
}
/**
* @param sb not null
* @param entity not null
* @param indent not null
* @param isJavaExecutable
* @throws MojoExecutionException if any
*/
private void addDefaultJavadocTags( final StringBuilder sb, final JavaAnnotatedElement entity,
final String indent, final boolean isJavaExecutable )
throws MojoExecutionException
{
boolean separatorAdded = false;
if ( isJavaExecutable )
{
JavaExecutable javaExecutable = (JavaExecutable) entity;
if ( fixTag( PARAM_TAG ) && javaExecutable.getParameters() != null )
{
for ( JavaParameter javaParameter : javaExecutable.getParameters() )
{
separatorAdded = appendDefaultParamTag( sb, indent, separatorAdded, javaParameter );
}
}
if ( javaExecutable instanceof JavaMethod && fixTag( RETURN_TAG ) )
{
JavaMethod javaMethod = (JavaMethod) javaExecutable;
if ( javaMethod.getReturns() != null && !javaMethod.getReturns().isVoid() )
{
separatorAdded = appendDefaultReturnTag( sb, indent, separatorAdded, javaMethod );
}
}
if ( fixTag( THROWS_TAG ) && javaExecutable.getExceptions() != null )
{
for ( JavaType exception : javaExecutable.getExceptions() )
{
separatorAdded = appendDefaultThrowsTag( sb, indent, separatorAdded, exception );
}
}
}
else
{
separatorAdded = appendDefaultAuthorTag( sb, indent, separatorAdded );
separatorAdded = appendDefaultVersionTag( sb, indent, separatorAdded );
}
if ( fixTag( SINCE_TAG ) )
{
if ( !isJavaExecutable )
{
JavaClass javaClass = (JavaClass) entity;
if ( !ignoreClirr )
{
if ( isNewClassFromLastVersion( javaClass ) )
{
separatorAdded = appendDefaultSinceTag( sb, indent, separatorAdded );
}
}
else
{
separatorAdded = appendDefaultSinceTag( sb, indent, separatorAdded );
addSinceClasses( javaClass );
}
}
else
{
JavaExecutable javaExecutable = (JavaExecutable) entity;
if ( !ignoreClirr )
{
if ( isNewMethodFromLastRevision( javaExecutable ) )
{
separatorAdded = appendDefaultSinceTag( sb, indent, separatorAdded );
}
}
else
{
if ( sinceClasses != null && !sinceClassesContains( javaExecutable.getDeclaringClass() ) )
{
separatorAdded = appendDefaultSinceTag( sb, indent, separatorAdded );
}
}
}
}
}
/**
* @param sb not null
* @param indent not null
* @param separatorAdded
* @return true if separator has been added.
*/
private boolean appendDefaultAuthorTag( final StringBuilder sb, final String indent, boolean separatorAdded )
{
if ( !fixTag( AUTHOR_TAG ) )
{
return separatorAdded;
}
if ( !separatorAdded )
{
appendSeparator( sb, indent );
separatorAdded = true;
}
appendDefaultAuthorTag( sb, indent );
return separatorAdded;
}
/**
* @param sb not null
* @param indent not null
*/
private void appendDefaultAuthorTag( final StringBuilder sb, final String indent )
{
if ( !fixTag( AUTHOR_TAG ) )
{
return;
}
sb.append( indent ).append( " * @" ).append( AUTHOR_TAG ).append( " " );
sb.append( defaultAuthor );
sb.append( EOL );
}
/**
* @param sb not null
* @param indent not null
* @param separatorAdded
* @return true if separator has been added.
*/
private boolean appendDefaultSinceTag( final StringBuilder sb, final String indent, boolean separatorAdded )
{
if ( !fixTag( SINCE_TAG ) )
{
return separatorAdded;
}
if ( !separatorAdded )
{
appendSeparator( sb, indent );
separatorAdded = true;
}
appendDefaultSinceTag( sb, indent );
return separatorAdded;
}
/**
* @param sb not null
* @param indent not null
*/
private void appendDefaultSinceTag( final StringBuilder sb, final String indent )
{
if ( !fixTag( SINCE_TAG ) )
{
return;
}
sb.append( indent ).append( " * @" ).append( SINCE_TAG ).append( " " );
sb.append( defaultSince );
sb.append( EOL );
}
/**
* @param sb not null
* @param indent not null
* @param separatorAdded
* @return true if separator has been added.
*/
private boolean appendDefaultVersionTag( final StringBuilder sb, final String indent, boolean separatorAdded )
{
if ( !fixTag( VERSION_TAG ) )
{
return separatorAdded;
}
if ( !separatorAdded )
{
appendSeparator( sb, indent );
separatorAdded = true;
}
appendDefaultVersionTag( sb, indent );
return separatorAdded;
}
/**
* @param sb not null
* @param indent not null
*/
private void appendDefaultVersionTag( final StringBuilder sb, final String indent )
{
if ( !fixTag( VERSION_TAG ) )
{
return;
}
sb.append( indent ).append( " * @" ).append( VERSION_TAG ).append( " " );
sb.append( defaultVersion );
sb.append( EOL );
}
/**
* @param sb not null
* @param indent not null
* @param separatorAdded
* @param typeParam not null
* @return true if separator has been added.
*/
private boolean appendDefaultParamTag( final StringBuilder sb, final String indent, boolean separatorAdded,
final JavaParameter typeParam )
{
if ( !fixTag( PARAM_TAG ) )
{
return separatorAdded;
}
if ( !separatorAdded )
{
appendSeparator( sb, indent );
separatorAdded = true;
}
appendDefaultParamTag( sb, indent, typeParam );
return separatorAdded;
}
/**
* @param sb not null
* @param indent not null
* @param separatorAdded
* @param typeParameter not null
* @return true if separator has been added.
*/
private boolean appendDefaultParamTag( final StringBuilder sb, final String indent, boolean separatorAdded,
final JavaTypeVariable<JavaGenericDeclaration> typeParameter )
{
if ( !fixTag( PARAM_TAG ) )
{
return separatorAdded;
}
if ( !separatorAdded )
{
appendSeparator( sb, indent );
separatorAdded = true;
}
appendDefaultParamTag( sb, indent, typeParameter );
return separatorAdded;
}
/**
* @param sb not null
* @param indent not null
* @param typeParam not null
*/
private void appendDefaultParamTag( final StringBuilder sb, final String indent, final JavaParameter typeParam )
{
if ( !fixTag( PARAM_TAG ) )
{
return;
}
sb.append( indent ).append( " * @" ).append( PARAM_TAG ).append( " " );
sb.append( typeParam.getName() );
sb.append( " " );
sb.append( getDefaultJavadocForType( typeParam.getJavaClass() ) );
sb.append( EOL );
}
/**
* @param sb not null
* @param indent not null
* @param typeParameter not null
*/
private void appendDefaultParamTag( final StringBuilder sb, final String indent,
final JavaTypeVariable<JavaGenericDeclaration> typeParameter )
{
if ( !fixTag( PARAM_TAG ) )
{
return;
}
sb.append( indent ).append( " * @" ).append( PARAM_TAG ).append( " " );
sb.append( "<" ).append( typeParameter.getName() ).append( ">" );
sb.append( " " );
sb.append( getDefaultJavadocForType( typeParameter ) );
sb.append( EOL );
}
/**
* @param sb not null
* @param indent not null
* @param separatorAdded
* @param javaMethod not null
* @return true if separator has been added.
*/
private boolean appendDefaultReturnTag( final StringBuilder sb, final String indent, boolean separatorAdded,
final JavaMethod javaMethod )
{
if ( !fixTag( RETURN_TAG ) )
{
return separatorAdded;
}
if ( !separatorAdded )
{
appendSeparator( sb, indent );
separatorAdded = true;
}
appendDefaultReturnTag( sb, indent, javaMethod );
return separatorAdded;
}
/**
* @param sb not null
* @param indent not null
* @param javaMethod not null
*/
private void appendDefaultReturnTag( final StringBuilder sb, final String indent, final JavaMethod javaMethod )
{
if ( !fixTag( RETURN_TAG ) )
{
return;
}
sb.append( indent ).append( " * @" ).append( RETURN_TAG ).append( " " );
sb.append( getDefaultJavadocForType( javaMethod.getReturns() ) );
sb.append( EOL );
}
/**
* @param sb not null
* @param indent not null
* @param separatorAdded
* @param exception not null
* @return true if separator has been added.
*/
private boolean appendDefaultThrowsTag( final StringBuilder sb, final String indent, boolean separatorAdded,
final JavaType exception )
{
if ( !fixTag( THROWS_TAG ) )
{
return separatorAdded;
}
if ( !separatorAdded )
{
appendSeparator( sb, indent );
separatorAdded = true;
}
appendDefaultThrowsTag( sb, indent, exception );
return separatorAdded;
}
/**
* @param sb not null
* @param indent not null
* @param exception not null
*/
private void appendDefaultThrowsTag( final StringBuilder sb, final String indent, final JavaType exception )
{
if ( !fixTag( THROWS_TAG ) )
{
return;
}
sb.append( indent ).append( " * @" ).append( THROWS_TAG ).append( " " );
sb.append( exception.getFullyQualifiedName() );
sb.append( " if any." );
sb.append( EOL );
}
/**
* @param sb not null
* @param indent not null
*/
private void appendSeparator( final StringBuilder sb, final String indent )
{
sb.append( indent ).append( " *" );
sb.append( EOL );
}
/**
* Verify if a method has <code>&#64;java.lang.Override()</code> annotation or if it is an inherited method
* from an interface or a super class. The goal is to handle <code>&#123;&#64;inheritDoc&#125;</code> tag.
*
* @param javaMethod not null
* @return <code>true</code> if the method is inherited, <code>false</code> otherwise.
* @throws MojoExecutionException if any
*/
private boolean isInherited( JavaExecutable javaMethod )
throws MojoExecutionException
{
if ( javaMethod.getAnnotations() != null )
{
for ( JavaAnnotation annotation : javaMethod.getAnnotations() )
{
if ( annotation.toString().equals( "@java.lang.Override()" ) )
{
return true;
}
}
}
Class<?> clazz = getClass( javaMethod.getDeclaringClass().getFullyQualifiedName() );
List<Class<?>> interfaces = ClassUtils.getAllInterfaces( clazz );
for ( Class<?> intface : interfaces )
{
if ( isInherited( intface, javaMethod ) )
{
return true;
}
}
List<Class<?>> classes = ClassUtils.getAllSuperclasses( clazz );
for ( Class<?> superClass : classes )
{
if ( isInherited( superClass, javaMethod ) )
{
return true;
}
}
return false;
}
/**
* @param clazz the Java class object, not null
* @param javaMethod the QDox JavaMethod object not null
* @return <code>true</code> if <code>javaMethod</code> exists in the given <code>clazz</code>,
* <code>false</code> otherwise.
* @see #isInherited(JavaExecutable)
*/
private boolean isInherited( Class<?> clazz, JavaExecutable javaMethod )
{
for ( Method method : clazz.getDeclaredMethods() )
{
if ( !method.getName().equals( javaMethod.getName() ) )
{
continue;
}
if ( method.getParameterTypes().length != javaMethod.getParameters().size() )
{
continue;
}
boolean found = false;
int j = 0;
for ( Class<?> paramType : method.getParameterTypes() )
{
String name1 = paramType.getName();
String name2 = javaMethod.getParameters().get( j++ ).getType().getFullyQualifiedName();
found = name1.equals( name2 ); // TODO check algo, seems broken (only takes in account the last param)
}
return found;
}
return false;
}
/**
* @param clazz
* @return
*/
private String getDefaultJavadocForType( JavaClass clazz )
{
StringBuilder sb = new StringBuilder();
if ( !JavaTypeVariable.class.isAssignableFrom( clazz.getClass() ) && clazz.isPrimitive() )
{
if ( clazz.isArray() )
{
sb.append( "an array of " ).append( clazz.getComponentType().getCanonicalName() );
}
else
{
sb.append( "a " ).append( clazz.getCanonicalName() );
}
return sb.append( "." ).toString();
}
StringBuilder javadocLink = new StringBuilder();
try
{
getClass( clazz.getCanonicalName() );
javadocLink.append( "{@link " );
if ( clazz.isArray() )
{
javadocLink.append( clazz.getComponentType().getCanonicalName() );
}
else
{
javadocLink.append( clazz.getCanonicalName() );
}
javadocLink.append( "}" );
}
catch ( Exception e )
{
javadocLink.append( clazz.getValue() );
}
if ( clazz.isArray() )
{
sb.append( "an array of " ).append( javadocLink ).append( " objects." );
}
else
{
sb.append( "a " ).append( javadocLink ).append( " object." );
}
return sb.toString();
}
private String getDefaultJavadocForType( JavaTypeVariable<JavaGenericDeclaration> typeParameter )
{
return "a " + typeParameter.getName() + " object.";
}
/**
* Check under Clirr if this given class is newer from the last version.
*
* @param javaClass a given class not null
* @return <code>true</code> if Clirr said that this class is added from the last version,
* <code>false</code> otherwise or if {@link #clirrNewClasses} is null.
*/
private boolean isNewClassFromLastVersion( JavaClass javaClass )
{
return ( clirrNewClasses != null ) && clirrNewClasses.contains( javaClass.getFullyQualifiedName() );
}
/**
* Check under Clirr if this given method is newer from the last version.
*
* @param javaExecutable a given method not null
* @return <code>true</code> if Clirr said that this method is added from the last version,
* <code>false</code> otherwise or if {@link #clirrNewMethods} is null.
* @throws MojoExecutionException if any
*/
private boolean isNewMethodFromLastRevision( JavaExecutable javaExecutable )
throws MojoExecutionException
{
if ( clirrNewMethods == null )
{
return false;
}
List<String> clirrMethods = clirrNewMethods.get( javaExecutable.getDeclaringClass().getFullyQualifiedName() );
if ( clirrMethods == null )
{
return false;
}
for ( String clirrMethod : clirrMethods )
{
// see net.sf.clirr.core.internal.checks.MethodSetCheck#getMethodId(JavaType clazz, Method method)
String retrn = "";
if ( javaExecutable instanceof JavaMethod && ( (JavaMethod) javaExecutable ).getReturns() != null )
{
retrn = ( (JavaMethod) javaExecutable ).getReturns().getFullyQualifiedName();
}
StringBuilder params = new StringBuilder();
for ( JavaParameter parameter : javaExecutable.getParameters() )
{
if ( params.length() > 0 )
{
params.append( ", " );
}
params.append( parameter.getResolvedFullyQualifiedName() );
}
if ( clirrMethod.contains( retrn + " " ) && clirrMethod.contains( javaExecutable.getName() + "(" )
&& clirrMethod.contains( "(" + params.toString() + ")" ) )
{
return true;
}
}
return false;
}
/**
* @param className not null
* @return the Class corresponding to the given class name using the project classloader.
* @throws MojoExecutionException if class not found
* @see ClassUtils#getClass(ClassLoader, String, boolean)
* @see #getProjectClassLoader()
*/
private Class<?> getClass( String className )
throws MojoExecutionException
{
try
{
return ClassUtils.getClass( getProjectClassLoader(), className, false );
}
catch ( ClassNotFoundException e )
{
throw new MojoExecutionException( "ClassNotFoundException: " + e.getMessage(), e );
}
}
/**
* Returns the Class object assignable for {@link RuntimeException} class and associated with the given
* exception class name.
*
* @param currentClass not null
* @param exceptionClassName not null, an exception class name defined as:
* <ul>
* <li>exception class fully qualified</li>
* <li>exception class in the same package</li>
* <li>exception inner class</li>
* <li>exception class in java.lang package</li>
* </ul>
* @return the class if found, otherwise {@code null}.
* @see #getClass(String)
*/
private Class<?> getClass( JavaClass currentClass, String exceptionClassName )
{
String[] potentialClassNames =
new String[]{ exceptionClassName, currentClass.getPackage().getName() + "." + exceptionClassName,
currentClass.getPackage().getName() + "." + currentClass.getName() + "$" + exceptionClassName,
"java.lang." + exceptionClassName };
Class<?> clazz = null;
for ( String potentialClassName : potentialClassNames )
{
try
{
clazz = getClass( potentialClassName );
}
catch ( MojoExecutionException e )
{
// nop
}
if ( clazz != null )
{
return clazz;
}
}
return null;
}
/**
* @param javaClass not null
*/
private void addSinceClasses( JavaClass javaClass )
{
if ( sinceClasses == null )
{
sinceClasses = new ArrayList<>();
}
sinceClasses.add( javaClass.getFullyQualifiedName() );
}
private boolean sinceClassesContains( JavaClass javaClass )
{
return sinceClasses.contains( javaClass.getFullyQualifiedName() );
}
// ----------------------------------------------------------------------
// Static methods
// ----------------------------------------------------------------------
/**
* Write content into the given javaFile and using the given encoding.
* All line separators will be unified.
*
* @param javaFile not null
* @param encoding not null
* @param content not null
* @throws IOException if any
*/
private static void writeFile( final File javaFile, final String encoding, final String content )
throws IOException
{
String unified = StringUtils.unifyLineSeparators( content );
FileUtils.fileWrite( javaFile, encoding, unified );
}
/**
* @return the full clirr goal, i.e. <code>groupId:artifactId:version:goal</code>. The clirr-plugin version
* could be load from the pom.properties in the clirr-maven-plugin dependency.
*/
private static String getFullClirrGoal()
{
StringBuilder sb = new StringBuilder();
sb.append( CLIRR_MAVEN_PLUGIN_GROUPID ).append( ":" ).append( CLIRR_MAVEN_PLUGIN_ARTIFACTID ).append( ":" );
String clirrVersion = CLIRR_MAVEN_PLUGIN_VERSION;
InputStream resourceAsStream = null;
try
{
String resource = "META-INF/maven/" + CLIRR_MAVEN_PLUGIN_GROUPID + "/" + CLIRR_MAVEN_PLUGIN_ARTIFACTID
+ "/pom.properties";
resourceAsStream = AbstractFixJavadocMojo.class.getClassLoader().getResourceAsStream( resource );
if ( resourceAsStream != null )
{
Properties properties = new Properties();
properties.load( resourceAsStream );
resourceAsStream.close();
resourceAsStream = null;
if ( StringUtils.isNotEmpty( properties.getProperty( "version" ) ) )
{
clirrVersion = properties.getProperty( "version" );
}
}
}
catch ( IOException e )
{
// nop
}
finally
{
IOUtil.close( resourceAsStream );
}
sb.append( clirrVersion ).append( ":" ).append( CLIRR_MAVEN_PLUGIN_GOAL );
return sb.toString();
}
/**
* Default comment for class.
*
* @param javaClass not null
* @return a default comment for class.
*/
private static String getDefaultClassJavadocComment( final JavaClass javaClass )
{
StringBuilder sb = new StringBuilder();
sb.append( "<p>" );
if ( javaClass.isAbstract() )
{
sb.append( "Abstract " );
}
sb.append( javaClass.getName() );
if ( !javaClass.isInterface() )
{
sb.append( " class." );
}
else
{
sb.append( " interface." );
}
sb.append( "</p>" );
return sb.toString();
}
/**
* Default comment for method with taking care of getter/setter in the javaMethod name.
*
* @param javaExecutable not null
* @return a default comment for method.
*/
private static String getDefaultMethodJavadocComment( final JavaExecutable javaExecutable )
{
if ( javaExecutable instanceof JavaConstructor )
{
return "<p>Constructor for " + javaExecutable.getName() + ".</p>";
}
if ( javaExecutable.getName().length() > 3 && ( javaExecutable.getName().startsWith( "get" )
|| javaExecutable.getName().startsWith( "set" ) ) )
{
String field = StringUtils.lowercaseFirstLetter( javaExecutable.getName().substring( 3 ) );
JavaClass clazz = javaExecutable.getDeclaringClass();
if ( clazz.getFieldByName( field ) == null )
{
return "<p>" + javaExecutable.getName() + ".</p>";
}
StringBuilder sb = new StringBuilder();
sb.append( "<p>" );
if ( javaExecutable.getName().startsWith( "get" ) )
{
sb.append( "Getter " );
}
else if ( javaExecutable.getName().startsWith( "set" ) )
{
sb.append( "Setter " );
}
sb.append( "for the field <code>" ).append( field ).append( "</code>.</p>" );
return sb.toString();
}
return "<p>" + javaExecutable.getName() + ".</p>";
}
/**
* Try to find if a Javadoc comment has an {@link #INHERITED_TAG} for instance:
* <pre>
* &#47;&#42;&#42; {&#64;inheritDoc} &#42;&#47;
* </pre>
* or
* <pre>
* &#47;&#42;&#42;
* &#32;&#42; {&#64;inheritDoc}
* &#32;&#42;&#47;
* </pre>
*
* @param content not null
* @return <code>true</code> if the content has an inherited tag, <code>false</code> otherwise.
*/
private static boolean hasInheritedTag( final String content )
{
final String inheritedTagPattern =
"^\\s*(\\/\\*\\*)?(\\s*(\\*)?)*(\\{)@inheritDoc\\s*(\\})(\\s*(\\*)?)*(\\*\\/)?$";
return Pattern.matches( inheritedTagPattern, StringUtils.removeDuplicateWhitespace( content ) );
}
/**
* Workaround for QDOX-146 about whitespace.
* Ideally we want to use <code>entity.getComment()</code>
* <br/>
* For instance, with the following snippet:
* <br/>
* <p/>
* <code>
* <font color="#808080">1</font>&nbsp;<font color="#ffffff"></font><br />
* <font color="#808080">2</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;</font>
* <font color="#3f5fbf">&#47;&#42;&#42;</font><br />
* <font color="#808080">3</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</font>
* <font color="#3f5fbf">&#42;&nbsp;Dummy&nbsp;Javadoc&nbsp;comment.</font><br />
* <font color="#808080">4</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</font>
* <font color="#3f5fbf">&#42;&nbsp;</font><font color="#7f9fbf">@param&nbsp;</font>
* <font color="#3f5fbf">s&nbsp;a&nbsp;String</font><br />
* <font color="#808080">5</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</font>
* <font color="#3f5fbf">&#42;&#47;</font><br />
* <font color="#808080">6</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;</font>
* <font color="#7f0055"><b>public&nbsp;</b></font><font color="#7f0055"><b>void&nbsp;</b></font>
* <font color="#000000">dummyMethod</font><font color="#000000">(&nbsp;</font>
* <font color="#000000">String&nbsp;s&nbsp;</font><font color="#000000">){}</font><br />
* </code>
* <p/>
* <br/>
* The return will be:
* <br/>
* <p/>
* <code>
* <font color="#808080">1</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</font>
* <font color="#3f5fbf">&#42;&nbsp;Dummy&nbsp;Javadoc&nbsp;comment.</font><br />
* </code>
*
* @param javaClassContent original class content not null
* @param entity not null
* @return the javadoc comment for the entity without any tags.
* @throws IOException if any
*/
static String getJavadocComment( final String javaClassContent, final JavaAnnotatedElement entity )
throws IOException
{
if ( entity.getComment() == null )
{
return "";
}
String originalJavadoc = extractOriginalJavadocContent( javaClassContent, entity );
StringBuilder sb = new StringBuilder();
BufferedReader lr = new BufferedReader( new StringReader( originalJavadoc ) );
String line;
while ( ( line = lr.readLine() ) != null )
{
String l = StringUtils.removeDuplicateWhitespace( line.trim() );
if ( l.startsWith( "* @" ) || l.startsWith( "*@" ) )
{
break;
}
sb.append( line ).append( EOL );
}
return trimRight( sb.toString() );
}
/**
* Work around for QDOX-146 about whitespace.
* Ideally we want to use <code>docletTag.getValue()</code>
* <br/>
* For instance, with the following snippet:
* <br/>
* <p/>
* <code>
* <font color="#808080">1</font>&nbsp;<font color="#ffffff"></font><br />
* <font color="#808080">2</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;</font>
* <font color="#3f5fbf">&#47;&#42;&#42;</font><br />
* <font color="#808080">3</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</font>
* <font color="#3f5fbf">&#42;&nbsp;Dummy&nbsp;Javadoc&nbsp;comment.</font><br />
* <font color="#808080">4</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</font>
* <font color="#3f5fbf">&#42;&nbsp;</font><font color="#7f9fbf">@param&nbsp;</font>
* <font color="#3f5fbf">s&nbsp;a&nbsp;String</font><br />
* <font color="#808080">5</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</font>
* <font color="#3f5fbf">&#42;&#47;</font><br />
* <font color="#808080">6</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;</font>
* <font color="#7f0055"><b>public&nbsp;</b></font><font color="#7f0055"><b>void&nbsp;</b></font>
* <font color="#000000">dummyMethod</font><font color="#000000">(&nbsp;</font>
* <font color="#000000">String&nbsp;s&nbsp;</font><font color="#000000">){}</font><br />
* </code>
* <p/>
* <br/>
* The return will be:
* <br/>
* <p/>
* <code>
* <font color="#808080">1</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</font>
* <font color="#3f5fbf">&#42;&nbsp;</font><font color="#7f9fbf">@param&nbsp;</font>
* <font color="#3f5fbf">s&nbsp;a&nbsp;String</font><br />
* </code>
*
* @param javaClassContent original class content not null
* @param entity not null
* @param docletTag not null
* @return the javadoc comment for the entity without Javadoc tags.
* @throws IOException if any
*/
String getJavadocComment( final String javaClassContent, final JavaAnnotatedElement entity,
final DocletTag docletTag )
throws IOException
{
if ( docletTag.getValue() == null || docletTag.getParameters().isEmpty() )
{
return "";
}
String originalJavadoc = extractOriginalJavadocContent( javaClassContent, entity );
StringBuilder sb = new StringBuilder();
BufferedReader lr = new BufferedReader( new StringReader( originalJavadoc ) );
String line;
boolean found = false;
// matching first line of doclettag
Pattern p = Pattern.compile( "(\\s*\\*\\s?@" + docletTag.getName() + ")\\s+"
+ "(\\Q" + docletTag.getValue().split( "\r\n|\r|\n" )[0] + "\\E)" );
while ( ( line = lr.readLine() ) != null )
{
Matcher m = p.matcher( line );
if ( m.matches() )
{
if ( fixTag( LINK_TAG ) )
{
line = replaceLinkTags( line, entity );
}
sb.append( line ).append( EOL );
found = true;
}
else
{
if ( line.trim().startsWith( "* @" ) || line.trim().startsWith( "*@" ) )
{
found = false;
}
if ( found )
{
if ( fixTag( LINK_TAG ) )
{
line = replaceLinkTags( line, entity );
}
sb.append( line ).append( EOL );
}
}
}
return trimRight( sb.toString() );
}
/**
* Extract the original Javadoc and others comments up to {@link #START_JAVADOC} form the entity. This method
* takes care of the Javadoc indentation. All javadoc lines will be trimmed on right.
* <br/>
* For instance, with the following snippet:
* <br/>
* <p/>
* <code>
* <font color="#808080">1</font>&nbsp;<font color="#ffffff"></font><br />
* <font color="#808080">2</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;</font>
* <font color="#3f5fbf">&#47;&#42;&#42;</font><br />
* <font color="#808080">3</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</font>
* <font color="#3f5fbf">&#42;&nbsp;Dummy&nbsp;Javadoc&nbsp;comment.</font><br />
* <font color="#808080">4</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</font>
* <font color="#3f5fbf">&#42;&nbsp;</font><font color="#7f9fbf">@param&nbsp;</font>
* <font color="#3f5fbf">s&nbsp;a&nbsp;String</font><br />
* <font color="#808080">5</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</font>
* <font color="#3f5fbf">&#42;&#47;</font><br />
* <font color="#808080">6</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;</font>
* <font color="#7f0055"><b>public&nbsp;</b></font><font color="#7f0055"><b>void&nbsp;</b></font>
* <font color="#000000">dummyMethod</font><font color="#000000">(&nbsp;</font>
* <font color="#000000">String&nbsp;s&nbsp;</font><font color="#000000">){}</font><br />
* </code>
* <p/>
* <br/>
* The return will be:
* <br/>
* <p/>
* <code>
* <font color="#808080">1</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;</font>
* <font color="#3f5fbf">&#47;&#42;&#42;</font><br />
* <font color="#808080">2</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</font>
* <font color="#3f5fbf">&#42;&nbsp;Dummy&nbsp;Javadoc&nbsp;comment.</font><br />
* <font color="#808080">3</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</font>
* <font color="#3f5fbf">&#42;&nbsp;</font><font color="#7f9fbf">@param&nbsp;</font>
* <font color="#3f5fbf">s&nbsp;a&nbsp;String</font><br />
* <font color="#808080">4</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</font>
* <font color="#3f5fbf">&#42;&#47;</font><br />
* </code>
*
* @param javaClassContent not null
* @param entity not null
* @return return the original javadoc as String for the current entity
* @throws IOException if any
*/
static String extractOriginalJavadoc( final String javaClassContent, final JavaAnnotatedElement entity )
throws IOException
{
if ( entity.getComment() == null )
{
return "";
}
String[] javaClassContentLines = getLines( javaClassContent );
List<String> list = new LinkedList<>();
for ( int i = entity.getLineNumber() - 2; i >= 0; i-- )
{
String line = javaClassContentLines[i];
list.add( trimRight( line ) );
if ( line.trim().startsWith( START_JAVADOC ) )
{
break;
}
}
Collections.reverse( list );
return StringUtils.join( list.iterator(), EOL );
}
/**
* Extract the Javadoc comment between {@link #START_JAVADOC} and {@link #END_JAVADOC} form the entity. This method
* takes care of the Javadoc indentation. All javadoc lines will be trimmed on right.
* <br/>
* For instance, with the following snippet:
* <br/>
* <p/>
* <code>
* <font color="#808080">1</font>&nbsp;<font color="#ffffff"></font><br />
* <font color="#808080">2</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;</font>
* <font color="#3f5fbf">&#47;&#42;&#42;</font><br />
* <font color="#808080">3</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</font>
* <font color="#3f5fbf">&#42;&nbsp;Dummy&nbsp;Javadoc&nbsp;comment.</font><br />
* <font color="#808080">4</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</font>
* <font color="#3f5fbf">&#42;&nbsp;</font><font color="#7f9fbf">@param&nbsp;</font>
* <font color="#3f5fbf">s&nbsp;a&nbsp;String</font><br />
* <font color="#808080">5</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</font>
* <font color="#3f5fbf">&#42;&#47;</font><br />
* <font color="#808080">6</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;</font>
* <font color="#7f0055"><b>public&nbsp;</b></font><font color="#7f0055"><b>void&nbsp;</b></font>
* <font color="#000000">dummyMethod</font><font color="#000000">(&nbsp;</font>
* <font color="#000000">String&nbsp;s&nbsp;</font><font color="#000000">){}</font><br />
* </code>
* <p/>
* <br/>
* The return will be:
* <br/>
* <p/>
* <code>
* <font color="#808080">1</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</font>
* <font color="#3f5fbf">&#42;&nbsp;Dummy&nbsp;Javadoc&nbsp;comment.</font><br />
* <font color="#808080">2</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</font>
* <font color="#3f5fbf">&#42;&nbsp;</font><font color="#7f9fbf">@param&nbsp;</font>
* <font color="#3f5fbf">s&nbsp;a&nbsp;String</font><br />
* </code>
*
* @param javaClassContent not null
* @param entity not null
* @return return the original javadoc as String for the current entity
* @throws IOException if any
*/
static String extractOriginalJavadocContent( final String javaClassContent,
final JavaAnnotatedElement entity )
throws IOException
{
if ( entity.getComment() == null )
{
return "";
}
String originalJavadoc = extractOriginalJavadoc( javaClassContent, entity );
int index = originalJavadoc.indexOf( START_JAVADOC );
if ( index != -1 )
{
originalJavadoc = originalJavadoc.substring( index + START_JAVADOC.length() );
}
index = originalJavadoc.indexOf( END_JAVADOC );
if ( index != -1 )
{
originalJavadoc = originalJavadoc.substring( 0, index );
}
if ( originalJavadoc.startsWith( "\r\n" ) )
{
originalJavadoc = originalJavadoc.substring( 2 );
}
else if ( originalJavadoc.startsWith( "\n" ) || originalJavadoc.startsWith( "\r" ) )
{
originalJavadoc = originalJavadoc.substring( 1 );
}
return trimRight( originalJavadoc );
}
/**
* @param content not null
* @return the content without last lines containing javadoc separator (ie <code> * </code>)
* @throws IOException if any
* @see #getJavadocComment(String, JavaAnnotatedElement, DocletTag)
*/
private static String removeLastEmptyJavadocLines( final String content )
throws IOException
{
if ( !content.contains( EOL ) )
{
return content;
}
String[] lines = getLines( content );
if ( lines.length == 1 )
{
return content;
}
List<String> linesList = new LinkedList<>( Arrays.asList( lines ) );
Collections.reverse( linesList );
for ( Iterator<String> it = linesList.iterator(); it.hasNext(); )
{
String line = it.next();
if ( line.trim().equals( "*" ) )
{
it.remove();
}
else
{
break;
}
}
Collections.reverse( linesList );
return StringUtils.join( linesList.iterator(), EOL );
}
/**
* @param content not null
* @return the javadoc comment with the given indentation
* @throws IOException if any
* @see #getJavadocComment(String, JavaAnnotatedElement, DocletTag)
*/
private static String alignIndentationJavadocLines( final String content, final String indent )
throws IOException
{
StringBuilder sb = new StringBuilder();
for ( String line : getLines( content ) )
{
if ( sb.length() > 0 )
{
sb.append( EOL );
}
if ( !line.trim().startsWith( "*" ) )
{
line = "*" + line;
}
sb.append( indent ).append( " " ).append( trimLeft( line ) );
}
return sb.toString();
}
/**
* Autodetect the indentation of a given line:
* <pre>
* autodetectIndentation( null ) = "";
* autodetectIndentation( "a" ) = "";
* autodetectIndentation( " a" ) = " ";
* autodetectIndentation( "\ta" ) = "\t";
* </pre>
*
* @param line not null
* @return the indentation for the given line.
*/
private static String autodetectIndentation( final String line )
{
if ( StringUtils.isEmpty( line ) )
{
return "";
}
return line.substring( 0, line.indexOf( trimLeft( line ) ) );
}
/**
* @param content not null
* @return an array of all content lines
* @throws IOException if any
*/
private static String[] getLines( final String content )
throws IOException
{
List<String> lines = new LinkedList<>();
BufferedReader reader = new BufferedReader( new StringReader( content ) );
String line = reader.readLine();
while ( line != null )
{
lines.add( line );
line = reader.readLine();
}
return lines.toArray( new String[lines.size()] );
}
/**
* Trim a given line on the left:
* <pre>
* trimLeft( null ) = "";
* trimLeft( " " ) = "";
* trimLeft( "a" ) = "a";
* trimLeft( " a" ) = "a";
* trimLeft( "\ta" ) = "a";
* trimLeft( " a " ) = "a ";
* </pre>
*
* @param text
* @return the text trimmed on left side or empty if text is null.
*/
private static String trimLeft( final String text )
{
if ( StringUtils.isEmpty( text ) || StringUtils.isEmpty( text.trim() ) )
{
return "";
}
String textTrimmed = text.trim();
return text.substring( text.indexOf( textTrimmed ) );
}
/**
* Trim a given line on the right:
* <pre>
* trimRight( null ) = "";
* trimRight( " " ) = "";
* trimRight( "a" ) = "a";
* trimRight( "a\t" ) = "a";
* trimRight( " a " ) = " a";
* </pre>
*
* @param text
* @return the text trimmed on tight side or empty if text is null.
*/
private static String trimRight( final String text )
{
if ( StringUtils.isEmpty( text ) || StringUtils.isEmpty( text.trim() ) )
{
return "";
}
String textTrimmed = text.trim();
return text.substring( 0, text.indexOf( textTrimmed ) + textTrimmed.length() );
}
/**
* Wrapper class for the entity's tags.
*/
class JavaEntityTags
{
private final JavaAnnotatedElement entity;
private final boolean isJavaMethod;
/**
* List of tag names.
*/
private List<String> namesTags;
/**
* Map with java parameter as key and original Javadoc lines as values.
*/
private Map<String, String> tagParams;
private Set<String> documentedParams = new HashSet<>();
/**
* Original javadoc lines.
*/
private String tagReturn;
/**
* Map with java throw as key and original Javadoc lines as values.
*/
private Map<String, String> tagThrows;
/**
* Original javadoc lines for unknown tags.
*/
private List<String> unknownsTags;
JavaEntityTags( JavaAnnotatedElement entity, boolean isJavaMethod )
{
this.entity = entity;
this.isJavaMethod = isJavaMethod;
this.namesTags = new LinkedList<>();
this.tagParams = new LinkedHashMap<>();
this.tagThrows = new LinkedHashMap<>();
this.unknownsTags = new LinkedList<>();
}
public List<String> getNamesTags()
{
return namesTags;
}
public String getJavadocReturnTag()
{
return tagReturn;
}
public void setJavadocReturnTag( String s )
{
tagReturn = s;
}
public List<String> getUnknownTags()
{
return unknownsTags;
}
public void putJavadocParamTag( String paramName, String paramValue, String originalJavadocTag )
{
documentedParams.add( paramName );
tagParams.put( paramValue, originalJavadocTag );
}
public String getJavadocParamTag( String paramValue )
{
String originalJavadocTag = tagParams.get( paramValue );
if ( originalJavadocTag == null && getLog().isWarnEnabled() )
{
getLog().warn( getMessage( paramValue, "javaEntityTags.tagParams" ) );
}
return originalJavadocTag;
}
public boolean hasJavadocParamTag( String paramName )
{
return documentedParams.contains( paramName );
}
public void putJavadocThrowsTag( String paramName, String originalJavadocTag )
{
tagThrows.put( paramName, originalJavadocTag );
}
public String getJavadocThrowsTag( String paramName )
{
return getJavadocThrowsTag( paramName, false );
}
public String getJavadocThrowsTag( String paramName, boolean nullable )
{
String originalJavadocTag = tagThrows.get( paramName );
if ( !nullable && originalJavadocTag == null && getLog().isWarnEnabled() )
{
getLog().warn( getMessage( paramName, "javaEntityTags.tagThrows" ) );
}
return originalJavadocTag;
}
private String getMessage( String paramName, String mapName )
{
StringBuilder msg = new StringBuilder();
msg.append( "No param '" ).append( paramName ).append( "' key found in " ).append( mapName )
.append( " for the entity: " );
if ( isJavaMethod )
{
JavaMethod javaMethod = (JavaMethod) entity;
msg.append( getJavaMethodAsString( javaMethod ) );
}
else
{
JavaClass javaClass = (JavaClass) entity;
msg.append( javaClass.getFullyQualifiedName() );
}
return msg.toString();
}
/**
* {@inheritDoc}
*/
@Override
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append( "namesTags=" ).append( namesTags ).append( "\n" );
sb.append( "tagParams=" ).append( tagParams ).append( "\n" );
sb.append( "tagReturn=" ).append( tagReturn ).append( "\n" );
sb.append( "tagThrows=" ).append( tagThrows ).append( "\n" );
sb.append( "unknownsTags=" ).append( unknownsTags ).append( "\n" );
return sb.toString();
}
}
}