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.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.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="https://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 @author * | |
*/ | |
private static final String AUTHOR_TAG = "author"; | |
/** | |
* Tag name for @version * | |
*/ | |
private static final String VERSION_TAG = "version"; | |
/** | |
* Tag name for @since * | |
*/ | |
private static final String SINCE_TAG = "since"; | |
/** | |
* Tag name for @param * | |
*/ | |
private static final String PARAM_TAG = "param"; | |
/** | |
* Tag name for @return * | |
*/ | |
private static final String RETURN_TAG = "return"; | |
/** | |
* Tag name for @throws * | |
*/ | |
private static final String THROWS_TAG = "throws"; | |
/** | |
* Tag name for @link * | |
*/ | |
private static final String LINK_TAG = "link"; | |
/** | |
* Tag name for {@inheritDoc} * | |
*/ | |
private static final String INHERITED_TAG = "{@inheritDoc}"; | |
/** | |
* Start Javadoc String i.e. <code>/**</code> * | |
*/ | |
private static final String START_JAVADOC = "/**"; | |
/** | |
* End Javadoc String i.e. <code>*/</code> * | |
*/ | |
private static final String END_JAVADOC = "*/"; | |
/** | |
* Javadoc Separator i.e. <code> * </code> * | |
*/ | |
private static final String SEPARATOR_JAVADOC = " * "; | |
/** | |
* Inherited Javadoc i.e. <code>/** {@inheritDoc} */</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>@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>@since</code>. | |
*/ | |
@Parameter ( property = "defaultSince", defaultValue = "${project.version}" ) | |
private String defaultSince; | |
/** | |
* Default value for the Javadoc tag <code>@version</code>. | |
* <br/> | |
* By default, it is <code>$Id:$</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>**/*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 @author tag)</li> | |
* <li>version (fix only @version tag)</li> | |
* <li>since (fix only @since tag)</li> | |
* <li>param (fix only @param tag)</li> | |
* <li>return (fix only @return tag)</li> | |
* <li>throws (fix only @throws tag)</li> | |
* <li>link (fix only @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>**/*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>*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 ); | |
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<>(); | |
try ( BufferedReader 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 | |
// CHECKSTYLE_OFF: MagicNumber | |
switch ( code ) | |
{ | |
case 7011: | |
methodAdded( split ); | |
break; | |
case 7012: | |
methodAdded( split ); | |
break; | |
case 8000: | |
clirrNewClasses.add( split[2].trim() ); | |
break; | |
default: | |
break; | |
} | |
// CHECKSTYLE_ON: MagicNumber | |
} | |
} | |
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." ); | |
} | |
} | |
private void methodAdded( String[] split ) | |
{ | |
List<String> list = clirrNewMethods.get( split[2].trim() ); | |
if ( list == null ) | |
{ | |
list = new ArrayList<>(); | |
} | |
String[] splits2 = StringUtils.split( split[3].trim(), "'" ); | |
if ( splits2.length != 3 ) | |
{ | |
return; | |
} | |
list.add( splits2[1].trim() ); | |
clirrNewMethods.put( split[2].trim(), list ); | |
} | |
/** | |
* @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 ).toURI().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(); | |
boolean changeDetected = false; | |
try ( BufferedReader 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() ) | |
{ | |
final boolean commentUpdated = | |
fixMethodComment( stringWriter, originalContent, method, indent ); | |
if ( commentUpdated ) | |
{ | |
takeCareSingleComment( stringWriter, originalContent, method ); | |
} | |
changeDetected |= commentUpdated; | |
} | |
} | |
} | |
// 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 ) | |
{ | |
final boolean commentUpdated = | |
fixMethodComment( stringWriter, originalContent, method, indent ); | |
if ( commentUpdated ) | |
{ | |
takeCareSingleComment( stringWriter, originalContent, method ); | |
} | |
changeDetected |= commentUpdated; | |
} | |
} | |
stringWriter.write( line ); | |
stringWriter.write( EOL ); | |
} | |
} | |
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> <font color="#ffffff"> </font> | |
* <font color="#3f5fbf">/**</font><br /> | |
* <font color="#808080">2</font> <font color="#ffffff"> </font> | |
* <font color="#3f5fbf">* {Javadoc Comment}</font><br /> | |
* <font color="#808080">3</font> <font color="#ffffff"> </font> | |
* <font color="#3f5fbf">*/</font><br /> | |
* <font color="#808080">4</font> <font color="#ffffff"> </font> | |
* <font color="#3f7f5f">/*</font><br /> | |
* <font color="#808080">5</font> <font color="#ffffff"> </font> | |
* <font color="#3f7f5f">* {Block Comment}</font><br /> | |
* <font color="#808080">6</font> <font color="#ffffff"> </font> | |
* <font color="#3f7f5f">*/</font><br /> | |
* <font color="#808080">7</font> <font color="#ffffff"> </font> | |
* <font color="#3f7f5f">// {Single comment}</font><br /> | |
* <font color="#808080">8</font> <font color="#ffffff"> </font> | |
* <font color="#7f0055"><b>public </b></font><font color="#7f0055"><b>void </b></font> | |
* <font color="#000000">dummyMethod</font><font color="#000000">( </font> | |
* <font color="#000000">String s </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> <font color="#ffffff"> </font> | |
* <font color="#3f5fbf">/**</font><br /> | |
* <font color="#808080">2</font> <font color="#ffffff"> </font> | |
* <font color="#3f5fbf">* {Comment based on the class name}</font><br /> | |
* <font color="#808080">3</font> <font color="#ffffff"> </font> | |
* <font color="#3f5fbf">*</font><br /> | |
* <font color="#808080">4</font> <font color="#ffffff"> </font> | |
* <font color="#3f5fbf">* </font><font color="#7f9fbf">@author </font> | |
* <font color="#3f5fbf">X {added if addMissingAuthor}</font><br /> | |
* <font color="#808080">5</font> <font color="#ffffff"> </font> | |
* <font color="#3f5fbf">* </font><font color="#7f9fbf">@version </font> | |
* <font color="#3f5fbf">X {added if addMissingVersion}</font><br /> | |
* <font color="#808080">6</font> <font color="#ffffff"> </font> | |
* <font color="#3f5fbf">* </font><font color="#7f9fbf">@since </font> | |
* <font color="#3f5fbf">X {added if addMissingSince and new classes | |
* from previous version}</font><br /> | |
* <font color="#808080">7</font> <font color="#ffffff"> </font> | |
* <font color="#3f5fbf">*/</font><br /> | |
* <font color="#808080">8</font> <font color="#7f0055"><b>public class </b></font> | |
* <font color="#000000">DummyClass </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> <font color="#ffffff"> </font> | |
* <font color="#3f5fbf">/** Constant </font><font color="#7f7f9f"><code></font> | |
* <font color="#3f5fbf">MY_STRING_CONSTANT="value"</font> | |
* <font color="#7f7f9f"></code> </font><font color="#3f5fbf">*/</font><br /> | |
* <font color="#808080">2</font> <font color="#ffffff"> </font> | |
* <font color="#7f0055"><b>public static final </b></font> | |
* <font color="#000000">String MY_STRING_CONSTANT = </font> | |
* <font color="#2a00ff">"value"</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> <font color="#ffffff"> </font> | |
* <font color="#3f5fbf">/**</font><br /> | |
* <font color="#808080">2</font> <font color="#ffffff"> </font> | |
* <font color="#3f5fbf">* {Comment based on the method name}</font><br /> | |
* <font color="#808080">3</font> <font color="#ffffff"> </font> | |
* <font color="#3f5fbf">*</font><br /> | |
* <font color="#808080">4</font> <font color="#ffffff"> </font> | |
* <font color="#3f5fbf">* </font><font color="#7f9fbf">@param </font> | |
* <font color="#3f5fbf">X {added if addMissingParam}</font><br /> | |
* <font color="#808080">5</font> <font color="#ffffff"> </font> | |
* <font color="#3f5fbf">* </font><font color="#7f9fbf">@return </font> | |
* <font color="#3f5fbf">X {added if addMissingReturn}</font><br /> | |
* <font color="#808080">6</font> <font color="#ffffff"> </font> | |
* <font color="#3f5fbf">* </font><font color="#7f9fbf">@throws </font> | |
* <font color="#3f5fbf">X {added if addMissingThrows}</font><br /> | |
* <font color="#808080">7</font> <font color="#ffffff"> </font> | |
* <font color="#3f5fbf">* </font><font color="#7f9fbf">@since </font> | |
* <font color="#3f5fbf">X {added if addMissingSince and new classes | |
* from previous version}</font><br /> | |
* <font color="#808080">8</font> <font color="#ffffff"> </font> | |
* <font color="#3f5fbf">*/</font><br /> | |
* <font color="#808080">9</font> <font color="#7f0055"><b>public </b></font> | |
* <font color="#7f0055"><b>void </b></font><font color="#000000">dummyMethod</font> | |
* <font color="#000000">( </font><font color="#000000">String s </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 = entity instanceof JavaExecutable; | |
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 | |
updateJavadocTags( sb, originalContent, 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 ); | |
} | |
} | |
private static final Pattern REPLACE_LINK_TAGS_PATTERN = Pattern.compile( "\\{@link\\s" ); | |
static String replaceLinkTags( String comment, JavaAnnotatedElement entity ) | |
{ | |
StringBuilder resolvedComment = new StringBuilder(); | |
// scan comment for {@link someClassName} and try to resolve this | |
Matcher linktagMatcher = REPLACE_LINK_TAGS_PATTERN.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.getName() + ">" ).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.getFullyQualifiedName().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 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>@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>{@inheritDoc}</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() + " class."; | |
} | |
/** | |
* 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; | |
String resource = "META-INF/maven/" + CLIRR_MAVEN_PLUGIN_GROUPID + "/" + CLIRR_MAVEN_PLUGIN_ARTIFACTID | |
+ "/pom.properties"; | |
try ( InputStream resourceAsStream = | |
AbstractFixJavadocMojo.class.getClassLoader().getResourceAsStream( resource ) ) | |
{ | |
if ( resourceAsStream != null ) | |
{ | |
Properties properties = new Properties(); | |
properties.load( resourceAsStream ); | |
if ( StringUtils.isNotEmpty( properties.getProperty( "version" ) ) ) | |
{ | |
clirrVersion = properties.getProperty( "version" ); | |
} | |
} | |
} | |
catch ( IOException e ) | |
{ | |
// nop | |
} | |
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> | |
* /** {@inheritDoc} */ | |
* </pre> | |
* or | |
* <pre> | |
* /** | |
*  * {@inheritDoc} | |
*  */ | |
* </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> <font color="#ffffff"></font><br /> | |
* <font color="#808080">2</font> <font color="#ffffff"> </font> | |
* <font color="#3f5fbf">/**</font><br /> | |
* <font color="#808080">3</font> <font color="#ffffff"> </font> | |
* <font color="#3f5fbf">* Dummy Javadoc comment.</font><br /> | |
* <font color="#808080">4</font> <font color="#ffffff"> </font> | |
* <font color="#3f5fbf">* </font><font color="#7f9fbf">@param </font> | |
* <font color="#3f5fbf">s a String</font><br /> | |
* <font color="#808080">5</font> <font color="#ffffff"> </font> | |
* <font color="#3f5fbf">*/</font><br /> | |
* <font color="#808080">6</font> <font color="#ffffff"> </font> | |
* <font color="#7f0055"><b>public </b></font><font color="#7f0055"><b>void </b></font> | |
* <font color="#000000">dummyMethod</font><font color="#000000">( </font> | |
* <font color="#000000">String s </font><font color="#000000">){}</font><br /> | |
* </code> | |
* <p/> | |
* <br/> | |
* The return will be: | |
* <br/> | |
* <p/> | |
* <code> | |
* <font color="#808080">1</font> <font color="#ffffff"> </font> | |
* <font color="#3f5fbf">* Dummy Javadoc 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> <font color="#ffffff"></font><br /> | |
* <font color="#808080">2</font> <font color="#ffffff"> </font> | |
* <font color="#3f5fbf">/**</font><br /> | |
* <font color="#808080">3</font> <font color="#ffffff"> </font> | |
* <font color="#3f5fbf">* Dummy Javadoc comment.</font><br /> | |
* <font color="#808080">4</font> <font color="#ffffff"> </font> | |
* <font color="#3f5fbf">* </font><font color="#7f9fbf">@param </font> | |
* <font color="#3f5fbf">s a String</font><br /> | |
* <font color="#808080">5</font> <font color="#ffffff"> </font> | |
* <font color="#3f5fbf">*/</font><br /> | |
* <font color="#808080">6</font> <font color="#ffffff"> </font> | |
* <font color="#7f0055"><b>public </b></font><font color="#7f0055"><b>void </b></font> | |
* <font color="#000000">dummyMethod</font><font color="#000000">( </font> | |
* <font color="#000000">String s </font><font color="#000000">){}</font><br /> | |
* </code> | |
* <p/> | |
* <br/> | |
* The return will be: | |
* <br/> | |
* <p/> | |
* <code> | |
* <font color="#808080">1</font> <font color="#ffffff"> </font> | |
* <font color="#3f5fbf">* </font><font color="#7f9fbf">@param </font> | |
* <font color="#3f5fbf">s a 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> <font color="#ffffff"></font><br /> | |
* <font color="#808080">2</font> <font color="#ffffff"> </font> | |
* <font color="#3f5fbf">/**</font><br /> | |
* <font color="#808080">3</font> <font color="#ffffff"> </font> | |
* <font color="#3f5fbf">* Dummy Javadoc comment.</font><br /> | |
* <font color="#808080">4</font> <font color="#ffffff"> </font> | |
* <font color="#3f5fbf">* </font><font color="#7f9fbf">@param </font> | |
* <font color="#3f5fbf">s a String</font><br /> | |
* <font color="#808080">5</font> <font color="#ffffff"> </font> | |
* <font color="#3f5fbf">*/</font><br /> | |
* <font color="#808080">6</font> <font color="#ffffff"> </font> | |
* <font color="#7f0055"><b>public </b></font><font color="#7f0055"><b>void </b></font> | |
* <font color="#000000">dummyMethod</font><font color="#000000">( </font> | |
* <font color="#000000">String s </font><font color="#000000">){}</font><br /> | |
* </code> | |
* <p/> | |
* <br/> | |
* The return will be: | |
* <br/> | |
* <p/> | |
* <code> | |
* <font color="#808080">1</font> <font color="#ffffff"> </font> | |
* <font color="#3f5fbf">/**</font><br /> | |
* <font color="#808080">2</font> <font color="#ffffff"> </font> | |
* <font color="#3f5fbf">* Dummy Javadoc comment.</font><br /> | |
* <font color="#808080">3</font> <font color="#ffffff"> </font> | |
* <font color="#3f5fbf">* </font><font color="#7f9fbf">@param </font> | |
* <font color="#3f5fbf">s a String</font><br /> | |
* <font color="#808080">4</font> <font color="#ffffff"> </font> | |
* <font color="#3f5fbf">*/</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> <font color="#ffffff"></font><br /> | |
* <font color="#808080">2</font> <font color="#ffffff"> </font> | |
* <font color="#3f5fbf">/**</font><br /> | |
* <font color="#808080">3</font> <font color="#ffffff"> </font> | |
* <font color="#3f5fbf">* Dummy Javadoc comment.</font><br /> | |
* <font color="#808080">4</font> <font color="#ffffff"> </font> | |
* <font color="#3f5fbf">* </font><font color="#7f9fbf">@param </font> | |
* <font color="#3f5fbf">s a String</font><br /> | |
* <font color="#808080">5</font> <font color="#ffffff"> </font> | |
* <font color="#3f5fbf">*/</font><br /> | |
* <font color="#808080">6</font> <font color="#ffffff"> </font> | |
* <font color="#7f0055"><b>public </b></font><font color="#7f0055"><b>void </b></font> | |
* <font color="#000000">dummyMethod</font><font color="#000000">( </font> | |
* <font color="#000000">String s </font><font color="#000000">){}</font><br /> | |
* </code> | |
* <p/> | |
* <br/> | |
* The return will be: | |
* <br/> | |
* <p/> | |
* <code> | |
* <font color="#808080">1</font> <font color="#ffffff"> </font> | |
* <font color="#3f5fbf">* Dummy Javadoc comment.</font><br /> | |
* <font color="#808080">2</font> <font color="#ffffff"> </font> | |
* <font color="#3f5fbf">* </font><font color="#7f9fbf">@param </font> | |
* <font color="#3f5fbf">s a 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(); | |
} | |
} | |
} |