blob: c5a634ccc8a9fc255fbfd4a0fe2855fb2f9a70c9 [file] [log] [blame]
/*
* 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.
*/
package org.apache.maven.plugins.javadoc;
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;
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.commons.text.StringEscapeUtils;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.DependencyResolutionRequiredException;
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;
/**
* Abstract class to fix Javadoc documentation and tags in source files.
*
* @see <a href="https://docs.oracle.com/en/java/javase/17/docs/specs/javadoc/doc-comment-spec.html#where-tags-can-be-used">Where Tags Can Be Used</a>
* @author <a href="mailto:vincent.siveton@gmail.com">Vincent Siveton</a>
* @since 2.6
*/
public abstract class AbstractFixJavadocMojo extends AbstractMojo {
/**
* The vm line separator
*/
private static final String EOL = System.getProperty("line.separator");
/**
* Tag name for &#64;author *
*/
private static final String AUTHOR_TAG = "author";
/**
* Tag name for &#64;version *
*/
private static final String VERSION_TAG = "version";
/**
* Tag name for &#64;since *
*/
private static final String SINCE_TAG = "since";
/**
* Tag name for &#64;param *
*/
private static final String PARAM_TAG = "param";
/**
* Tag name for &#64;return *
*/
private static final String RETURN_TAG = "return";
/**
* Tag name for &#64;throws *
*/
private static final String THROWS_TAG = "throws";
/**
* Tag name for &#64;link *
*/
private static final String LINK_TAG = "link";
/**
* Tag name for {&#64;inheritDoc} *
*/
private static final String INHERITED_TAG = "{@inheritDoc}";
/**
* Start Javadoc String i.e. <code>&#47;&#42;&#42;</code> *
*/
private static final String START_JAVADOC = "/**";
/**
* End Javadoc String i.e. <code>&#42;&#47;</code> *
*/
private static final String END_JAVADOC = "*/";
/**
* Javadoc Separator i.e. <code> &#42; </code> *
*/
private static final String SEPARATOR_JAVADOC = " * ";
/**
* Inherited Javadoc i.e. <code>&#47;&#42;&#42; {&#64;inheritDoc} &#42;&#47;</code> *
*/
private static final String INHERITED_JAVADOC = START_JAVADOC + " " + INHERITED_TAG + " " + END_JAVADOC;
/**
* <code>all</code> parameter used by {@link #fixTags} *
*/
private static final String FIX_TAGS_ALL = "all";
/**
* <code>public</code> parameter used by {@link #level} *
*/
private static final String LEVEL_PUBLIC = "public";
/**
* <code>protected</code> parameter used by {@link #level} *
*/
private static final String LEVEL_PROTECTED = "protected";
/**
* <code>package</code> parameter used by {@link #level} *
*/
private static final String LEVEL_PACKAGE = "package";
/**
* <code>private</code> parameter used by {@link #level} *
*/
private static final String LEVEL_PRIVATE = "private";
/**
* The Clirr Maven plugin groupId <code>org.codehaus.mojo</code> *
*/
private static final String CLIRR_MAVEN_PLUGIN_GROUPID = "org.codehaus.mojo";
/**
* The Clirr Maven plugin artifactId <code>clirr-maven-plugin</code> *
*/
private static final String CLIRR_MAVEN_PLUGIN_ARTIFACTID = "clirr-maven-plugin";
/**
* The latest Clirr Maven plugin version <code>2.2.2</code> *
*/
private static final String CLIRR_MAVEN_PLUGIN_VERSION = "2.2.2";
/**
* The Clirr Maven plugin goal <code>check</code> *
*/
private static final String CLIRR_MAVEN_PLUGIN_GOAL = "check";
/**
* Java Files Pattern.
*/
public static final String JAVA_FILES = "**\\/*.java";
/**
* Default version value.
*/
public static final String DEFAULT_VERSION_VALUE = "\u0024Id: \u0024Id";
// ----------------------------------------------------------------------
// Mojo components
// ----------------------------------------------------------------------
/**
* Input handler, needed for command line handling.
*/
@Component
private InputHandler inputHandler;
// ----------------------------------------------------------------------
// Mojo parameters
// ----------------------------------------------------------------------
/**
* Version to compare the current code against using the
* <a href="http://mojo.codehaus.org/clirr-maven-plugin/">Clirr Maven Plugin</a>.
* <br/>
* See <a href="#defaultSince">defaultSince</a>.
*/
@Parameter(property = "comparisonVersion", defaultValue = "(,${project.version})")
private String comparisonVersion;
/**
* Default value for the Javadoc tag <code>&#64;author</code>.
* <br/>
* If not specified, the <code>user.name</code> defined in the System properties will be used.
*/
@Parameter(property = "defaultAuthor")
private String defaultAuthor;
/**
* Default value for the Javadoc tag <code>&#64;since</code>.
*/
@Parameter(property = "defaultSince", defaultValue = "${project.version}")
private String defaultSince;
/**
* Default value for the Javadoc tag <code>&#64;version</code>.
* <br/>
* By default, it is <code>&#36;Id:&#36;</code>, corresponding to a
* <a href="http://svnbook.red-bean.com/en/1.1/ch07s02.html#svn-ch-7-sect-2.3.4">SVN keyword</a>.
* Refer to your SCM to use an other SCM keyword.
*/
@Parameter(property = "defaultVersion", defaultValue = DEFAULT_VERSION_VALUE)
private String defaultVersion = "\u0024Id: \u0024"; // can't use default-value="\u0024Id: \u0024"
/**
* The file encoding to use when reading the source files. If the property
* <code>project.build.sourceEncoding</code> is not set, the platform default encoding is used.
*/
@Parameter(property = "encoding", defaultValue = "${project.build.sourceEncoding}")
private String encoding;
/**
* Comma separated excludes Java files, i.e. <code>&#42;&#42;/&#42;Test.java</code>.
*/
@Parameter(property = "excludes")
private String excludes;
/**
* Comma separated tags to fix in classes, interfaces or methods Javadoc comments.
* Possible values are:
* <ul>
* <li>all (fix all Javadoc tags)</li>
* <li>author (fix only &#64;author tag)</li>
* <li>version (fix only &#64;version tag)</li>
* <li>since (fix only &#64;since tag)</li>
* <li>param (fix only &#64;param tag)</li>
* <li>return (fix only &#64;return tag)</li>
* <li>throws (fix only &#64;throws tag)</li>
* <li>link (fix only &#64;link tag)</li>
* </ul>
*/
@Parameter(property = "fixTags", defaultValue = "all")
private String fixTags;
/**
* Flag to fix the classes or interfaces Javadoc comments according the <code>level</code>.
*/
@Parameter(property = "fixClassComment", defaultValue = "true")
private boolean fixClassComment;
/**
* Flag to fix the fields Javadoc comments according the <code>level</code>.
*/
@Parameter(property = "fixFieldComment", defaultValue = "true")
private boolean fixFieldComment;
/**
* Flag to fix the methods Javadoc comments according the <code>level</code>.
*/
@Parameter(property = "fixMethodComment", defaultValue = "true")
private boolean fixMethodComment;
/**
* <p>Flag to remove throws tags from unknown classes.</p>
* <p><strong>NOTE:</strong>Since 3.1.0 the default value has been changed to {@code true},
* due to JavaDoc 8 strictness.
*/
@Parameter(property = "removeUnknownThrows", defaultValue = "true")
private boolean removeUnknownThrows;
/**
* Forcing the goal execution i.e. skip warranty messages (not recommended).
*/
@Parameter(property = "force")
private boolean force;
/**
* Flag to ignore or not Clirr.
*/
@Parameter(property = "ignoreClirr", defaultValue = "false")
protected boolean ignoreClirr;
/**
* Comma separated includes Java files, i.e. <code>&#42;&#42;/&#42;Test.java</code>.
* <p/>
* <strong>Note:</strong> default value is {@code **\/*.java}.
*/
@Parameter(property = "includes", defaultValue = JAVA_FILES)
private String includes;
/**
* Specifies the access level for classes and members to show in the Javadocs.
* Possible values are:
* <ul>
* <li>public (shows only public classes and members)</li>
* <li>protected (shows only public and protected classes and members)</li>
* <li>package (shows all classes and members not marked private)</li>
* <li>private (shows all classes and members)</li>
* </ul>
* @see <a href="https://docs.oracle.com/en/java/javase/17/docs/specs/man/javadoc.html#options-for-javadoc">private, protected, public, package options for Javadoc</a>
*/
@Parameter(property = "level", defaultValue = "protected")
private String level;
/**
* Output directory where Java classes will be rewritten.
*/
@Parameter(property = "outputDirectory", defaultValue = "${project.build.sourceDirectory}")
private File outputDirectory;
/**
* The Maven Project Object.
*/
@Parameter(defaultValue = "${project}", readonly = true, required = true)
private MavenProject project;
@Parameter(defaultValue = "${session}", readonly = true, required = true)
private MavenSession session;
/**
* The current user system settings for use in Maven.
*/
@Parameter(defaultValue = "${settings}", readonly = true, required = true)
private Settings settings;
// ----------------------------------------------------------------------
// Internal fields
// ----------------------------------------------------------------------
/**
* The current project class loader.
*/
private ClassLoader projectClassLoader;
/**
* Split {@link #fixTags} by comma.
*
* @see #init()
*/
private String[] fixTagsSplitted;
/**
* New classes found by Clirr.
*/
private List<String> clirrNewClasses;
/**
* New Methods in a Class (the key) found by Clirr.
*/
private Map<String, List<String>> clirrNewMethods;
/**
* List of classes where <code>&#42;since</code> is added. Will be used to add or not this tag in the methods.
*/
private List<String> sinceClasses;
/**
* {@inheritDoc}
*/
@Override
public void execute() throws MojoExecutionException, MojoFailureException {
if (!fixClassComment && !fixFieldComment && !fixMethodComment) {
getLog().info("Specified to NOT fix classes, fields and methods. Nothing to do.");
return;
}
// verify goal params
init();
if (fixTagsSplitted.length == 0) {
getLog().info("No fix tag specified. Nothing to do.");
return;
}
// add warranty msg
if (!preCheck()) {
return;
}
// run clirr
try {
executeClirr();
} catch (MavenInvocationException e) {
if (getLog().isDebugEnabled()) {
getLog().error("MavenInvocationException: " + e.getMessage(), e);
} else {
getLog().error("MavenInvocationException: " + e.getMessage());
}
getLog().info("Clirr is ignored.");
}
// run qdox and process
try {
Collection<JavaClass> javaClasses = getQdoxClasses();
if (javaClasses != null) {
for (JavaClass javaClass : javaClasses) {
processFix(javaClass);
}
}
} catch (IOException e) {
throw new MojoExecutionException("IOException: " + e.getMessage(), e);
}
}
// ----------------------------------------------------------------------
// protected methods
// ----------------------------------------------------------------------
protected final MavenProject getProject() {
return project;
}
/**
* @param p not null maven project.
* @return the artifact type.
*/
protected String getArtifactType(MavenProject p) {
return p.getArtifact().getType();
}
/**
* @param p not null maven project.
* @return the list of source paths for the given project.
*/
protected List<String> getProjectSourceRoots(MavenProject p) {
return p.getCompileSourceRoots() == null
? Collections.<String>emptyList()
: new LinkedList<>(p.getCompileSourceRoots());
}
/**
* @param p not null
* @return the compile classpath elements
* @throws DependencyResolutionRequiredException
* if any
*/
protected List<String> getCompileClasspathElements(MavenProject p) throws DependencyResolutionRequiredException {
return p.getCompileClasspathElements() == null
? Collections.<String>emptyList()
: new LinkedList<>(p.getCompileClasspathElements());
}
/**
* @param javaExecutable not null
* @return the fully qualify name of javaMethod with signature
*/
protected static String getJavaMethodAsString(JavaExecutable javaExecutable) {
return javaExecutable.getDeclaringClass().getFullyQualifiedName() + "#" + javaExecutable.getCallSignature();
}
// ----------------------------------------------------------------------
// private methods
// ----------------------------------------------------------------------
/**
* Init goal parameters.
*/
private void init() {
// defaultAuthor
if (defaultAuthor == null || defaultAuthor.isEmpty()) {
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 (encoding == null || encoding.isEmpty()) {
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(),
session.getRepositorySession().getLocalRepository().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()]), ClassLoader.getPlatformClassLoader());
}
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>&nbsp;<font color="#ffffff">&nbsp;</font>
* <font color="#3f5fbf">&#47;&#42;&#42;</font><br />
* <font color="#808080">2</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;</font>
* <font color="#3f5fbf">&#42;&nbsp;{Javadoc&nbsp;Comment}</font><br />
* <font color="#808080">3</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;</font>
* <font color="#3f5fbf">&#42;&#47;</font><br />
* <font color="#808080">4</font>&nbsp;<font color="#ffffff">&nbsp;</font>
* <font color="#3f7f5f">&#47;&#42;</font><br />
* <font color="#808080">5</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;</font>
* <font color="#3f7f5f">&#42;&nbsp;{Block&nbsp;Comment}</font><br />
* <font color="#808080">6</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;</font>
* <font color="#3f7f5f">&#42;&#47;</font><br />
* <font color="#808080">7</font>&nbsp;<font color="#ffffff">&nbsp;</font>
* <font color="#3f7f5f">&#47;&#47;&nbsp;{Single&nbsp;comment}</font><br />
* <font color="#808080">8</font>&nbsp;<font color="#ffffff">&nbsp;</font>
* <font color="#7f0055"><b>public&nbsp;</b></font><font color="#7f0055"><b>void&nbsp;</b></font>
* <font color="#000000">dummyMethod</font><font color="#000000">(&nbsp;</font>
* <font color="#000000">String&nbsp;s&nbsp;</font><font color="#000000">){}</font>
* </code>
*
* @param stringWriter not null
* @param originalContent not null
* @param entity not null
* @throws IOException if any
* @see #extractOriginalJavadoc
*/
private void takeCareSingleComment(
final StringWriter stringWriter, final String originalContent, final JavaAnnotatedElement entity)
throws IOException {
if (entity.getComment() == null) {
return;
}
String javadocComment = trimRight(extractOriginalJavadoc(originalContent, entity));
String extraComment = javadocComment.substring(javadocComment.indexOf(END_JAVADOC) + END_JAVADOC.length());
if (extraComment != null && !extraComment.isEmpty()) {
if (extraComment.contains(EOL)) {
stringWriter.write(extraComment.substring(extraComment.indexOf(EOL) + EOL.length()));
} else {
stringWriter.write(extraComment);
}
stringWriter.write(EOL);
}
}
/**
* Add/update Javadoc class comment.
*
* @param stringWriter
* @param originalContent
* @param javaClass
* @param indent
* @return {@code true} if the comment is updated, otherwise {@code false}
* @throws MojoExecutionException
* @throws IOException
*/
private boolean fixClassComment(
final StringWriter stringWriter,
final String originalContent,
final JavaClass javaClass,
final String indent)
throws MojoExecutionException, IOException {
if (!fixClassComment) {
return false;
}
if (!isInLevel(javaClass.getModifiers())) {
return false;
}
// add
if (javaClass.getComment() == null) {
addDefaultClassComment(stringWriter, javaClass, indent);
return true;
}
// update
return updateEntityComment(stringWriter, originalContent, javaClass, indent);
}
/**
* @param modifiers list of modifiers (public, private, protected, package)
* @return <code>true</code> if modifier is align with <code>level</code>.
*/
private boolean isInLevel(List<String> modifiers) {
if (LEVEL_PUBLIC.equalsIgnoreCase(level.trim())) {
return modifiers.contains(LEVEL_PUBLIC);
}
if (LEVEL_PROTECTED.equalsIgnoreCase(level.trim())) {
return modifiers.contains(LEVEL_PUBLIC) || modifiers.contains(LEVEL_PROTECTED);
}
if (LEVEL_PACKAGE.equalsIgnoreCase(level.trim())) {
return !modifiers.contains(LEVEL_PRIVATE);
}
// should be private (shows all classes and members)
return true;
}
/**
* Add a default Javadoc for the given class, i.e.:
* <br/>
* <code>
* <font color="#808080">1</font>&nbsp;<font color="#ffffff">&nbsp;</font>
* <font color="#3f5fbf">&#47;&#42;&#42;</font><br />
* <font color="#808080">2</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;</font>
* <font color="#3f5fbf">&#42;&nbsp;{Comment&nbsp;based&nbsp;on&nbsp;the&nbsp;class&nbsp;name}</font><br />
* <font color="#808080">3</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;</font>
* <font color="#3f5fbf">&#42;</font><br />
* <font color="#808080">4</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;</font>
* <font color="#3f5fbf">&#42;&nbsp;</font><font color="#7f9fbf">@author&nbsp;</font>
* <font color="#3f5fbf">X&nbsp;{added&nbsp;if&nbsp;addMissingAuthor}</font><br />
* <font color="#808080">5</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;</font>
* <font color="#3f5fbf">&#42;&nbsp;</font><font color="#7f9fbf">@version&nbsp;</font>
* <font color="#3f5fbf">X&nbsp;{added&nbsp;if&nbsp;addMissingVersion}</font><br />
* <font color="#808080">6</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;</font>
* <font color="#3f5fbf">&#42;&nbsp;</font><font color="#7f9fbf">@since&nbsp;</font>
* <font color="#3f5fbf">X&nbsp;{added&nbsp;if&nbsp;addMissingSince&nbsp;and&nbsp;new&nbsp;classes
* from&nbsp;previous&nbsp;version}</font><br />
* <font color="#808080">7</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;</font>
* <font color="#3f5fbf">&#42;&#47;</font><br />
* <font color="#808080">8</font>&nbsp;<font color="#7f0055"><b>public&nbsp;class&nbsp;</b></font>
* <font color="#000000">DummyClass&nbsp;</font><font color="#000000">{}</font></code>
* </code>
*
* @param stringWriter not null
* @param javaClass not null
* @param indent not null
* @see #getDefaultClassJavadocComment(JavaClass)
* @see #appendDefaultAuthorTag(StringBuilder, String)
* @see #appendDefaultSinceTag(StringBuilder, String)
* @see #appendDefaultVersionTag(StringBuilder, String)
*/
private void addDefaultClassComment(
final StringWriter stringWriter, final JavaClass javaClass, final String indent) {
StringBuilder sb = new StringBuilder();
sb.append(indent).append(START_JAVADOC);
sb.append(EOL);
sb.append(indent).append(SEPARATOR_JAVADOC);
sb.append(getDefaultClassJavadocComment(javaClass));
sb.append(EOL);
appendSeparator(sb, indent);
appendDefaultAuthorTag(sb, indent);
appendDefaultVersionTag(sb, indent);
if (fixTag(SINCE_TAG)) {
if (!ignoreClirr) {
if (isNewClassFromLastVersion(javaClass)) {
appendDefaultSinceTag(sb, indent);
}
} else {
appendDefaultSinceTag(sb, indent);
addSinceClasses(javaClass);
}
}
sb.append(indent).append(" ").append(END_JAVADOC);
sb.append(EOL);
stringWriter.write(sb.toString());
}
/**
* Add Javadoc field comment, only for static fields or interface fields.
*
* @param stringWriter not null
* @param javaClass not null
* @param field not null
* @param indent not null
* @return {@code true} if comment was updated, otherwise {@code false}
* @throws IOException if any
*/
private boolean fixFieldComment(
final StringWriter stringWriter, final JavaClass javaClass, final JavaField field, final String indent)
throws IOException {
if (!fixFieldComment) {
return false;
}
if (!javaClass.isInterface() && (!isInLevel(field.getModifiers()) || !field.isStatic())) {
return false;
}
// add
if (field.getComment() == null) {
addDefaultFieldComment(stringWriter, field, indent);
return true;
}
// no update
return false;
}
/**
* Add a default Javadoc for the given field, i.e.:
* <br/>
* <code>
* <font color="#808080">1</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;</font>
* <font color="#3f5fbf">&#47;&#42;&#42;&nbsp;Constant&nbsp;</font><font color="#7f7f9f">&lt;code&gt;</font>
* <font color="#3f5fbf">MY_STRING_CONSTANT=&#34;value&#34;</font>
* <font color="#7f7f9f">&lt;/code&gt;&nbsp;</font><font color="#3f5fbf">&#42;&#47;</font><br />
* <font color="#808080">2</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;</font>
* <font color="#7f0055"><b>public&nbsp;static&nbsp;final&nbsp;</b></font>
* <font color="#000000">String&nbsp;MY_STRING_CONSTANT&nbsp;=&nbsp;</font>
* <font color="#2a00ff">&#34;value&#34;</font><font color="#000000">;</font>
* </code>
*
* @param stringWriter not null
* @param field not null
* @param indent not null
* @throws IOException if any
*/
private void addDefaultFieldComment(final StringWriter stringWriter, final JavaField field, final String indent)
throws IOException {
StringBuilder sb = new StringBuilder();
sb.append(indent).append(START_JAVADOC).append(" ");
sb.append("Constant <code>").append(field.getName());
if (StringUtils.isNotEmpty(field.getInitializationExpression())) {
String qualifiedName = field.getType().getFullyQualifiedName();
if (qualifiedName.equals(Byte.TYPE.toString())
|| qualifiedName.equals(Short.TYPE.toString())
|| qualifiedName.equals(Integer.TYPE.toString())
|| qualifiedName.equals(Long.TYPE.toString())
|| qualifiedName.equals(Float.TYPE.toString())
|| qualifiedName.equals(Double.TYPE.toString())
|| qualifiedName.equals(Boolean.TYPE.toString())
|| qualifiedName.equals(Character.TYPE.toString())) {
sb.append("=");
sb.append(StringEscapeUtils.escapeHtml4(
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("=\"");
String escapedValue = StringEscapeUtils.escapeHtml4(value.toString());
// reduce the size
// CHECKSTYLE_OFF: MagicNumber
if (escapedValue.length() < 40) {
sb.append(escapedValue).append("\"");
} else {
sb.append(escapedValue, 0, 39).append("\"{trunked}");
}
// CHECKSTYLE_ON: MagicNumber
}
}
sb.append("</code> ").append(END_JAVADOC);
sb.append(EOL);
stringWriter.write(sb.toString());
}
/**
* Add/update Javadoc method comment.
*
* @param stringWriter not null
* @param originalContent not null
* @param javaExecutable not null
* @param indent not null
* @return {@code true} if comment was updated, otherwise {@code false}
* @throws MojoExecutionException if any
* @throws IOException if any
*/
private boolean fixMethodComment(
final StringWriter stringWriter,
final String originalContent,
final JavaExecutable javaExecutable,
final String indent)
throws MojoExecutionException, IOException {
if (!fixMethodComment) {
return false;
}
if (!javaExecutable.getDeclaringClass().isInterface() && !isInLevel(javaExecutable.getModifiers())) {
return false;
}
// add
if (javaExecutable.getComment() == null) {
addDefaultMethodComment(stringWriter, javaExecutable, indent);
return true;
}
// update
return updateEntityComment(stringWriter, originalContent, javaExecutable, indent);
}
/**
* Add in the buffer a default Javadoc for the given class:
* <br/>
* <code>
* <font color="#808080">1</font>&nbsp;<font color="#ffffff">&nbsp;</font>
* <font color="#3f5fbf">&#47;&#42;&#42;</font><br />
* <font color="#808080">2</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;</font>
* <font color="#3f5fbf">&#42;&nbsp;{Comment&nbsp;based&nbsp;on&nbsp;the&nbsp;method&nbsp;name}</font><br />
* <font color="#808080">3</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;</font>
* <font color="#3f5fbf">&#42;</font><br />
* <font color="#808080">4</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;</font>
* <font color="#3f5fbf">&#42;&nbsp;</font><font color="#7f9fbf">@param&nbsp;</font>
* <font color="#3f5fbf">X&nbsp;{added&nbsp;if&nbsp;addMissingParam}</font><br />
* <font color="#808080">5</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;</font>
* <font color="#3f5fbf">&#42;&nbsp;</font><font color="#7f9fbf">@return&nbsp;</font>
* <font color="#3f5fbf">X&nbsp;{added&nbsp;if&nbsp;addMissingReturn}</font><br />
* <font color="#808080">6</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;</font>
* <font color="#3f5fbf">&#42;&nbsp;</font><font color="#7f9fbf">@throws&nbsp;</font>
* <font color="#3f5fbf">X&nbsp;{added&nbsp;if&nbsp;addMissingThrows}</font><br />
* <font color="#808080">7</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;</font>
* <font color="#3f5fbf">&#42;&nbsp;</font><font color="#7f9fbf">@since&nbsp;</font>
* <font color="#3f5fbf">X&nbsp;{added&nbsp;if&nbsp;addMissingSince&nbsp;and&nbsp;new&nbsp;classes
* from&nbsp;previous&nbsp;version}</font><br />
* <font color="#808080">8</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;</font>
* <font color="#3f5fbf">&#42;&#47;</font><br />
* <font color="#808080">9</font>&nbsp;<font color="#7f0055"><b>public&nbsp;</b></font>
* <font color="#7f0055"><b>void&nbsp;</b></font><font color="#000000">dummyMethod</font>
* <font color="#000000">(&nbsp;</font><font color="#000000">String&nbsp;s&nbsp;</font>
* <font color="#000000">){}</font>
* </code>
*
* @param stringWriter not null
* @param javaExecutable not null
* @param indent not null
* @throws MojoExecutionException if any
* @see #getDefaultMethodJavadocComment
* @see #appendDefaultSinceTag(StringBuilder, String)
*/
private void addDefaultMethodComment(
final StringWriter stringWriter, final JavaExecutable javaExecutable, final String indent)
throws MojoExecutionException {
StringBuilder sb = new StringBuilder();
// special case
if (isInherited(javaExecutable)) {
sb.append(indent).append(INHERITED_JAVADOC);
sb.append(EOL);
stringWriter.write(sb.toString());
return;
}
sb.append(indent).append(START_JAVADOC);
sb.append(EOL);
sb.append(indent).append(SEPARATOR_JAVADOC);
sb.append(getDefaultMethodJavadocComment(javaExecutable));
sb.append(EOL);
boolean separatorAdded = false;
if (fixTag(PARAM_TAG)) {
if (javaExecutable.getParameters() != null) {
for (JavaParameter javaParameter : javaExecutable.getParameters()) {
separatorAdded = appendDefaultParamTag(sb, indent, separatorAdded, javaParameter);
}
}
// is generic?
if (javaExecutable.getTypeParameters() != null) {
for (JavaTypeVariable<JavaGenericDeclaration> typeParam : javaExecutable.getTypeParameters()) {
separatorAdded = appendDefaultParamTag(sb, indent, separatorAdded, typeParam);
}
}
}
if (javaExecutable instanceof JavaMethod) {
JavaMethod javaMethod = (JavaMethod) javaExecutable;
if (fixTag(RETURN_TAG)
&& javaMethod.getReturns() != null
&& !javaMethod.getReturns().isVoid()) {
separatorAdded = appendDefaultReturnTag(sb, indent, separatorAdded, javaMethod);
}
}
if (fixTag(THROWS_TAG) && javaExecutable.getExceptions() != null) {
for (JavaType exception : javaExecutable.getExceptions()) {
separatorAdded = appendDefaultThrowsTag(sb, indent, separatorAdded, exception);
}
}
if (fixTag(SINCE_TAG) && isNewMethodFromLastRevision(javaExecutable)) {
separatorAdded = appendDefaultSinceTag(sb, indent, separatorAdded);
}
sb.append(indent).append(" ").append(END_JAVADOC);
sb.append(EOL);
stringWriter.write(sb.toString());
}
/**
* @param stringWriter not null
* @param originalContent not null
* @param entity not null
* @param indent not null
* @return the updated changeDetected flag
* @throws MojoExecutionException if any
* @throws IOException if any
*/
private boolean updateEntityComment(
final StringWriter stringWriter,
final String originalContent,
final JavaAnnotatedElement entity,
final String indent)
throws MojoExecutionException, IOException {
boolean changeDetected = false;
String old = null;
String s = stringWriter.toString();
int i = s.lastIndexOf(START_JAVADOC);
if (i != -1) {
String tmp = s.substring(0, i);
if (tmp.lastIndexOf(EOL) != -1) {
tmp = tmp.substring(0, tmp.lastIndexOf(EOL));
}
old = stringWriter.getBuffer().substring(i);
stringWriter.getBuffer().delete(0, stringWriter.getBuffer().length());
stringWriter.write(tmp);
stringWriter.write(EOL);
} else {
changeDetected = true;
}
updateJavadocComment(stringWriter, originalContent, entity, indent);
if (changeDetected) {
return true; // return now if we already know there's a change
}
return !stringWriter.getBuffer().substring(i).equals(old);
}
/**
* @param stringWriter not null
* @param originalContent not null
* @param entity not null
* @param indent not null
* @throws MojoExecutionException if any
* @throws IOException if any
*/
private void updateJavadocComment(
final StringWriter stringWriter,
final String originalContent,
final JavaAnnotatedElement entity,
final String indent)
throws MojoExecutionException, IOException {
if (entity.getComment() == null
&& (entity.getTags() == null || entity.getTags().isEmpty())) {
return;
}
boolean isJavaExecutable = 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 ((originalJavadocTag != null && !originalJavadocTag.isEmpty())
&& 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
*/
private void appendDefaultAuthorTag(final StringBuilder sb, final String indent) {
if (!fixTag(AUTHOR_TAG)) {
return;
}
sb.append(indent).append(" * @").append(AUTHOR_TAG).append(" ");
sb.append(defaultAuthor);
sb.append(EOL);
}
/**
* @param sb not null
* @param indent not null
* @param separatorAdded
* @return true if separator has been added.
*/
private boolean appendDefaultSinceTag(final StringBuilder sb, final String indent, boolean separatorAdded) {
if (!fixTag(SINCE_TAG)) {
return separatorAdded;
}
if (!separatorAdded) {
appendSeparator(sb, indent);
separatorAdded = true;
}
appendDefaultSinceTag(sb, indent);
return separatorAdded;
}
/**
* @param sb not null
* @param indent not null
*/
private void appendDefaultSinceTag(final StringBuilder sb, final String indent) {
if (!fixTag(SINCE_TAG)) {
return;
}
sb.append(indent).append(" * @").append(SINCE_TAG).append(" ");
sb.append(defaultSince);
sb.append(EOL);
}
/**
* @param sb not null
* @param indent not null
* @param separatorAdded
* @return true if separator has been added.
*/
private boolean appendDefaultVersionTag(final StringBuilder sb, final String indent, boolean separatorAdded) {
if (!fixTag(VERSION_TAG)) {
return separatorAdded;
}
if (!separatorAdded) {
appendSeparator(sb, indent);
separatorAdded = true;
}
appendDefaultVersionTag(sb, indent);
return separatorAdded;
}
/**
* @param sb not null
* @param indent not null
*/
private void appendDefaultVersionTag(final StringBuilder sb, final String indent) {
if (!fixTag(VERSION_TAG)) {
return;
}
sb.append(indent).append(" * @").append(VERSION_TAG).append(" ");
sb.append(defaultVersion);
sb.append(EOL);
}
/**
* @param sb not null
* @param indent not null
* @param separatorAdded
* @param typeParam not null
* @return true if separator has been added.
*/
private boolean appendDefaultParamTag(
final StringBuilder sb, final String indent, boolean separatorAdded, final JavaParameter typeParam) {
if (!fixTag(PARAM_TAG)) {
return separatorAdded;
}
if (!separatorAdded) {
appendSeparator(sb, indent);
separatorAdded = true;
}
appendDefaultParamTag(sb, indent, typeParam);
return separatorAdded;
}
/**
* @param sb not null
* @param indent not null
* @param separatorAdded
* @param typeParameter not null
* @return true if separator has been added.
*/
private boolean appendDefaultParamTag(
final StringBuilder sb,
final String indent,
boolean separatorAdded,
final JavaTypeVariable<JavaGenericDeclaration> typeParameter) {
if (!fixTag(PARAM_TAG)) {
return separatorAdded;
}
if (!separatorAdded) {
appendSeparator(sb, indent);
separatorAdded = true;
}
appendDefaultParamTag(sb, indent, typeParameter);
return separatorAdded;
}
/**
* @param sb not null
* @param indent not null
* @param typeParam not null
*/
private void appendDefaultParamTag(final StringBuilder sb, final String indent, final JavaParameter typeParam) {
if (!fixTag(PARAM_TAG)) {
return;
}
sb.append(indent).append(" * @").append(PARAM_TAG).append(" ");
sb.append(typeParam.getName());
sb.append(" ");
sb.append(getDefaultJavadocForType(typeParam.getJavaClass()));
sb.append(EOL);
}
/**
* @param sb not null
* @param indent not null
* @param typeParameter not null
*/
private void appendDefaultParamTag(
final StringBuilder sb, final String indent, final JavaTypeVariable<JavaGenericDeclaration> typeParameter) {
if (!fixTag(PARAM_TAG)) {
return;
}
sb.append(indent).append(" * @").append(PARAM_TAG).append(" ");
sb.append("<").append(typeParameter.getName()).append(">");
sb.append(" ");
sb.append(getDefaultJavadocForType(typeParameter));
sb.append(EOL);
}
/**
* @param sb not null
* @param indent not null
* @param separatorAdded
* @param javaMethod not null
* @return true if separator has been added.
*/
private boolean appendDefaultReturnTag(
final StringBuilder sb, final String indent, boolean separatorAdded, final JavaMethod javaMethod) {
if (!fixTag(RETURN_TAG)) {
return separatorAdded;
}
if (!separatorAdded) {
appendSeparator(sb, indent);
separatorAdded = true;
}
appendDefaultReturnTag(sb, indent, javaMethod);
return separatorAdded;
}
/**
* @param sb not null
* @param indent not null
* @param javaMethod not null
*/
private void appendDefaultReturnTag(final StringBuilder sb, final String indent, final JavaMethod javaMethod) {
if (!fixTag(RETURN_TAG)) {
return;
}
sb.append(indent).append(" * @").append(RETURN_TAG).append(" ");
sb.append(getDefaultJavadocForType(javaMethod.getReturns()));
sb.append(EOL);
}
/**
* @param sb not null
* @param indent not null
* @param separatorAdded
* @param exception not null
* @return true if separator has been added.
*/
private boolean appendDefaultThrowsTag(
final StringBuilder sb, final String indent, boolean separatorAdded, final JavaType exception) {
if (!fixTag(THROWS_TAG)) {
return separatorAdded;
}
if (!separatorAdded) {
appendSeparator(sb, indent);
separatorAdded = true;
}
appendDefaultThrowsTag(sb, indent, exception);
return separatorAdded;
}
/**
* @param sb not null
* @param indent not null
* @param exception not null
*/
private void appendDefaultThrowsTag(final StringBuilder sb, final String indent, final JavaType exception) {
if (!fixTag(THROWS_TAG)) {
return;
}
sb.append(indent).append(" * @").append(THROWS_TAG).append(" ");
sb.append(exception.getFullyQualifiedName());
sb.append(" if any.");
sb.append(EOL);
}
/**
* @param sb not null
* @param indent not null
*/
private void appendSeparator(final StringBuilder sb, final String indent) {
sb.append(indent).append(" *");
sb.append(EOL);
}
/**
* Verify if a method has <code>&#64;java.lang.Override()</code> annotation or if it is an inherited method
* from an interface or a super class. The goal is to handle <code>&#123;&#64;inheritDoc&#125;</code> tag.
*
* @param javaMethod not null
* @return <code>true</code> if the method is inherited, <code>false</code> otherwise.
* @throws MojoExecutionException if any
*/
private boolean isInherited(JavaExecutable javaMethod) throws MojoExecutionException {
if (javaMethod.getAnnotations() != null) {
for (JavaAnnotation annotation : javaMethod.getAnnotations()) {
if (annotation.toString().equals("@java.lang.Override()")) {
return true;
}
}
}
Class<?> clazz = getClass(javaMethod.getDeclaringClass().getFullyQualifiedName());
List<Class<?>> interfaces = ClassUtils.getAllInterfaces(clazz);
for (Class<?> intface : interfaces) {
if (isInherited(intface, javaMethod)) {
return true;
}
}
List<Class<?>> classes = ClassUtils.getAllSuperclasses(clazz);
for (Class<?> superClass : classes) {
if (isInherited(superClass, javaMethod)) {
return true;
}
}
return false;
}
/**
* @param clazz the Java class object, not null
* @param javaMethod the QDox JavaMethod object not null
* @return <code>true</code> if <code>javaMethod</code> exists in the given <code>clazz</code>,
* <code>false</code> otherwise.
* @see #isInherited(JavaExecutable)
*/
private boolean isInherited(Class<?> clazz, JavaExecutable javaMethod) {
for (Method method : clazz.getDeclaredMethods()) {
if (!method.getName().equals(javaMethod.getName())) {
continue;
}
if (method.getParameterTypes().length != javaMethod.getParameters().size()) {
continue;
}
boolean found = false;
int j = 0;
for (Class<?> paramType : method.getParameterTypes()) {
String name1 = paramType.getName();
String name2 = javaMethod.getParameters().get(j++).getType().getFullyQualifiedName();
found = name1.equals(name2); // TODO check algo, seems broken (only takes in account the last param)
}
return found;
}
return false;
}
/**
* @param clazz
* @return
*/
private String getDefaultJavadocForType(JavaClass clazz) {
StringBuilder sb = new StringBuilder();
if (!JavaTypeVariable.class.isAssignableFrom(clazz.getClass()) && clazz.isPrimitive()) {
if (clazz.isArray()) {
sb.append("an array of ").append(clazz.getComponentType().getCanonicalName());
} else {
sb.append("a ").append(clazz.getCanonicalName());
}
return sb.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>
* &#47;&#42;&#42; {&#64;inheritDoc} &#42;&#47;
* </pre>
* or
* <pre>
* &#47;&#42;&#42;
* &#32;&#42; {&#64;inheritDoc}
* &#32;&#42;&#47;
* </pre>
*
* @param content not null
* @return <code>true</code> if the content has an inherited tag, <code>false</code> otherwise.
*/
private static boolean hasInheritedTag(final String content) {
final String inheritedTagPattern =
"^\\s*(\\/\\*\\*)?(\\s*(\\*)?)*(\\{)@inheritDoc\\s*(\\})(\\s*(\\*)?)*(\\*\\/)?$";
return Pattern.matches(inheritedTagPattern, StringUtils.removeDuplicateWhitespace(content));
}
/**
* Workaround for QDOX-146 about whitespace.
* Ideally we want to use <code>entity.getComment()</code>
* <br/>
* For instance, with the following snippet:
* <br/>
* <p/>
* <code>
* <font color="#808080">1</font>&nbsp;<font color="#ffffff"></font><br />
* <font color="#808080">2</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;</font>
* <font color="#3f5fbf">&#47;&#42;&#42;</font><br />
* <font color="#808080">3</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</font>
* <font color="#3f5fbf">&#42;&nbsp;Dummy&nbsp;Javadoc&nbsp;comment.</font><br />
* <font color="#808080">4</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</font>
* <font color="#3f5fbf">&#42;&nbsp;</font><font color="#7f9fbf">@param&nbsp;</font>
* <font color="#3f5fbf">s&nbsp;a&nbsp;String</font><br />
* <font color="#808080">5</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</font>
* <font color="#3f5fbf">&#42;&#47;</font><br />
* <font color="#808080">6</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;</font>
* <font color="#7f0055"><b>public&nbsp;</b></font><font color="#7f0055"><b>void&nbsp;</b></font>
* <font color="#000000">dummyMethod</font><font color="#000000">(&nbsp;</font>
* <font color="#000000">String&nbsp;s&nbsp;</font><font color="#000000">){}</font><br />
* </code>
* <p/>
* <br/>
* The return will be:
* <br/>
* <p/>
* <code>
* <font color="#808080">1</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</font>
* <font color="#3f5fbf">&#42;&nbsp;Dummy&nbsp;Javadoc&nbsp;comment.</font><br />
* </code>
*
* @param javaClassContent original class content not null
* @param entity not null
* @return the javadoc comment for the entity without any tags.
* @throws IOException if any
*/
static String getJavadocComment(final String javaClassContent, final JavaAnnotatedElement entity)
throws IOException {
if (entity.getComment() == null) {
return "";
}
String originalJavadoc = extractOriginalJavadocContent(javaClassContent, entity);
StringBuilder sb = new StringBuilder();
BufferedReader lr = new BufferedReader(new StringReader(originalJavadoc));
String line;
while ((line = lr.readLine()) != null) {
String l = StringUtils.removeDuplicateWhitespace(line.trim());
if (l.startsWith("* @") || l.startsWith("*@")) {
break;
}
sb.append(line).append(EOL);
}
return trimRight(sb.toString());
}
/**
* Work around for QDOX-146 about whitespace.
* Ideally we want to use <code>docletTag.getValue()</code>
* <br/>
* For instance, with the following snippet:
* <br/>
* <p/>
* <code>
* <font color="#808080">1</font>&nbsp;<font color="#ffffff"></font><br />
* <font color="#808080">2</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;</font>
* <font color="#3f5fbf">&#47;&#42;&#42;</font><br />
* <font color="#808080">3</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</font>
* <font color="#3f5fbf">&#42;&nbsp;Dummy&nbsp;Javadoc&nbsp;comment.</font><br />
* <font color="#808080">4</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</font>
* <font color="#3f5fbf">&#42;&nbsp;</font><font color="#7f9fbf">@param&nbsp;</font>
* <font color="#3f5fbf">s&nbsp;a&nbsp;String</font><br />
* <font color="#808080">5</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</font>
* <font color="#3f5fbf">&#42;&#47;</font><br />
* <font color="#808080">6</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;</font>
* <font color="#7f0055"><b>public&nbsp;</b></font><font color="#7f0055"><b>void&nbsp;</b></font>
* <font color="#000000">dummyMethod</font><font color="#000000">(&nbsp;</font>
* <font color="#000000">String&nbsp;s&nbsp;</font><font color="#000000">){}</font><br />
* </code>
* <p/>
* <br/>
* The return will be:
* <br/>
* <p/>
* <code>
* <font color="#808080">1</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</font>
* <font color="#3f5fbf">&#42;&nbsp;</font><font color="#7f9fbf">@param&nbsp;</font>
* <font color="#3f5fbf">s&nbsp;a&nbsp;String</font><br />
* </code>
*
* @param javaClassContent original class content not null
* @param entity not null
* @param docletTag not null
* @return the javadoc comment for the entity without Javadoc tags.
* @throws IOException if any
*/
String getJavadocComment(
final String javaClassContent, final JavaAnnotatedElement entity, final DocletTag docletTag)
throws IOException {
if (docletTag.getValue() == null || docletTag.getParameters().isEmpty()) {
return "";
}
String originalJavadoc = extractOriginalJavadocContent(javaClassContent, entity);
StringBuilder sb = new StringBuilder();
BufferedReader lr = new BufferedReader(new StringReader(originalJavadoc));
String line;
boolean found = false;
// matching first line of doclettag
Pattern p = Pattern.compile("(\\s*\\*\\s?@" + docletTag.getName() + ")\\s+" + "(\\Q"
+ docletTag.getValue().split("\r\n|\r|\n")[0] + "\\E)");
while ((line = lr.readLine()) != null) {
Matcher m = p.matcher(line);
if (m.matches()) {
if (fixTag(LINK_TAG)) {
line = replaceLinkTags(line, entity);
}
sb.append(line).append(EOL);
found = true;
} else {
if (line.trim().startsWith("* @") || line.trim().startsWith("*@")) {
found = false;
}
if (found) {
if (fixTag(LINK_TAG)) {
line = replaceLinkTags(line, entity);
}
sb.append(line).append(EOL);
}
}
}
return trimRight(sb.toString());
}
/**
* Extract the original Javadoc and others comments up to {@link #START_JAVADOC} form the entity. This method
* takes care of the Javadoc indentation. All javadoc lines will be trimmed on right.
* <br/>
* For instance, with the following snippet:
* <br/>
* <p/>
* <code>
* <font color="#808080">1</font>&nbsp;<font color="#ffffff"></font><br />
* <font color="#808080">2</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;</font>
* <font color="#3f5fbf">&#47;&#42;&#42;</font><br />
* <font color="#808080">3</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</font>
* <font color="#3f5fbf">&#42;&nbsp;Dummy&nbsp;Javadoc&nbsp;comment.</font><br />
* <font color="#808080">4</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</font>
* <font color="#3f5fbf">&#42;&nbsp;</font><font color="#7f9fbf">@param&nbsp;</font>
* <font color="#3f5fbf">s&nbsp;a&nbsp;String</font><br />
* <font color="#808080">5</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</font>
* <font color="#3f5fbf">&#42;&#47;</font><br />
* <font color="#808080">6</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;</font>
* <font color="#7f0055"><b>public&nbsp;</b></font><font color="#7f0055"><b>void&nbsp;</b></font>
* <font color="#000000">dummyMethod</font><font color="#000000">(&nbsp;</font>
* <font color="#000000">String&nbsp;s&nbsp;</font><font color="#000000">){}</font><br />
* </code>
* <p/>
* <br/>
* The return will be:
* <br/>
* <p/>
* <code>
* <font color="#808080">1</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;</font>
* <font color="#3f5fbf">&#47;&#42;&#42;</font><br />
* <font color="#808080">2</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</font>
* <font color="#3f5fbf">&#42;&nbsp;Dummy&nbsp;Javadoc&nbsp;comment.</font><br />
* <font color="#808080">3</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</font>
* <font color="#3f5fbf">&#42;&nbsp;</font><font color="#7f9fbf">@param&nbsp;</font>
* <font color="#3f5fbf">s&nbsp;a&nbsp;String</font><br />
* <font color="#808080">4</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</font>
* <font color="#3f5fbf">&#42;&#47;</font><br />
* </code>
*
* @param javaClassContent not null
* @param entity not null
* @return return the original javadoc as String for the current entity
* @throws IOException if any
*/
static String extractOriginalJavadoc(final String javaClassContent, final JavaAnnotatedElement entity)
throws IOException {
if (entity.getComment() == null) {
return "";
}
String[] javaClassContentLines = getLines(javaClassContent);
List<String> list = new LinkedList<>();
for (int i = entity.getLineNumber() - 2; i >= 0; i--) {
String line = javaClassContentLines[i];
list.add(trimRight(line));
if (line.trim().startsWith(START_JAVADOC)) {
break;
}
}
Collections.reverse(list);
return StringUtils.join(list.iterator(), EOL);
}
/**
* Extract the Javadoc comment between {@link #START_JAVADOC} and {@link #END_JAVADOC} form the entity. This method
* takes care of the Javadoc indentation. All javadoc lines will be trimmed on right.
* <br/>
* For instance, with the following snippet:
* <br/>
* <p/>
* <code>
* <font color="#808080">1</font>&nbsp;<font color="#ffffff"></font><br />
* <font color="#808080">2</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;</font>
* <font color="#3f5fbf">&#47;&#42;&#42;</font><br />
* <font color="#808080">3</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</font>
* <font color="#3f5fbf">&#42;&nbsp;Dummy&nbsp;Javadoc&nbsp;comment.</font><br />
* <font color="#808080">4</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</font>
* <font color="#3f5fbf">&#42;&nbsp;</font><font color="#7f9fbf">@param&nbsp;</font>
* <font color="#3f5fbf">s&nbsp;a&nbsp;String</font><br />
* <font color="#808080">5</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</font>
* <font color="#3f5fbf">&#42;&#47;</font><br />
* <font color="#808080">6</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;</font>
* <font color="#7f0055"><b>public&nbsp;</b></font><font color="#7f0055"><b>void&nbsp;</b></font>
* <font color="#000000">dummyMethod</font><font color="#000000">(&nbsp;</font>
* <font color="#000000">String&nbsp;s&nbsp;</font><font color="#000000">){}</font><br />
* </code>
* <p/>
* <br/>
* The return will be:
* <br/>
* <p/>
* <code>
* <font color="#808080">1</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</font>
* <font color="#3f5fbf">&#42;&nbsp;Dummy&nbsp;Javadoc&nbsp;comment.</font><br />
* <font color="#808080">2</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</font>
* <font color="#3f5fbf">&#42;&nbsp;</font><font color="#7f9fbf">@param&nbsp;</font>
* <font color="#3f5fbf">s&nbsp;a&nbsp;String</font><br />
* </code>
*
* @param javaClassContent not null
* @param entity not null
* @return return the original javadoc as String for the current entity
* @throws IOException if any
*/
static String extractOriginalJavadocContent(final String javaClassContent, final JavaAnnotatedElement entity)
throws IOException {
if (entity.getComment() == null) {
return "";
}
String originalJavadoc = extractOriginalJavadoc(javaClassContent, entity);
int index = originalJavadoc.indexOf(START_JAVADOC);
if (index != -1) {
originalJavadoc = originalJavadoc.substring(index + START_JAVADOC.length());
}
index = originalJavadoc.indexOf(END_JAVADOC);
if (index != -1) {
originalJavadoc = originalJavadoc.substring(0, index);
}
if (originalJavadoc.startsWith("\r\n")) {
originalJavadoc = originalJavadoc.substring(2);
} else if (originalJavadoc.startsWith("\n") || originalJavadoc.startsWith("\r")) {
originalJavadoc = originalJavadoc.substring(1);
}
return trimRight(originalJavadoc);
}
/**
* @param content not null
* @return the content without last lines containing javadoc separator (ie <code> * </code>)
* @throws IOException if any
* @see #getJavadocComment(String, JavaAnnotatedElement, DocletTag)
*/
private static String removeLastEmptyJavadocLines(final String content) throws IOException {
if (!content.contains(EOL)) {
return content;
}
String[] lines = getLines(content);
if (lines.length == 1) {
return content;
}
List<String> linesList = new LinkedList<>(Arrays.asList(lines));
Collections.reverse(linesList);
for (Iterator<String> it = linesList.iterator(); it.hasNext(); ) {
String line = it.next();
if (line.trim().equals("*")) {
it.remove();
} else {
break;
}
}
Collections.reverse(linesList);
return StringUtils.join(linesList.iterator(), EOL);
}
/**
* @param content not null
* @return the javadoc comment with the given indentation
* @throws IOException if any
* @see #getJavadocComment(String, JavaAnnotatedElement, DocletTag)
*/
private static String alignIndentationJavadocLines(final String content, final String indent) throws IOException {
StringBuilder sb = new StringBuilder();
for (String line : getLines(content)) {
if (sb.length() > 0) {
sb.append(EOL);
}
if (!line.trim().startsWith("*")) {
line = "*" + line;
}
sb.append(indent).append(" ").append(trimLeft(line));
}
return sb.toString();
}
/**
* Autodetect the indentation of a given line:
* <pre>
* autodetectIndentation( null ) = "";
* autodetectIndentation( "a" ) = "";
* autodetectIndentation( " a" ) = " ";
* autodetectIndentation( "\ta" ) = "\t";
* </pre>
*
* @param line not null
* @return the indentation for the given line.
*/
private static String autodetectIndentation(final String line) {
if (line == null || line.isEmpty()) {
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 ((text == null || text.isEmpty()) || 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 ((text == null || text.isEmpty()) || 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();
}
}
}