View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.plugins.javadoc;
20  
21  import java.io.BufferedReader;
22  import java.io.File;
23  import java.io.IOException;
24  import java.io.InputStream;
25  import java.io.StringReader;
26  import java.io.StringWriter;
27  import java.lang.reflect.Method;
28  import java.net.MalformedURLException;
29  import java.net.URISyntaxException;
30  import java.net.URL;
31  import java.net.URLClassLoader;
32  import java.nio.file.Paths;
33  import java.util.ArrayList;
34  import java.util.Arrays;
35  import java.util.Collection;
36  import java.util.Collections;
37  import java.util.HashSet;
38  import java.util.Iterator;
39  import java.util.LinkedHashMap;
40  import java.util.LinkedList;
41  import java.util.List;
42  import java.util.Locale;
43  import java.util.Map;
44  import java.util.Properties;
45  import java.util.Set;
46  import java.util.StringTokenizer;
47  import java.util.regex.Matcher;
48  import java.util.regex.Pattern;
49  
50  import com.thoughtworks.qdox.JavaProjectBuilder;
51  import com.thoughtworks.qdox.library.ClassLibraryBuilder;
52  import com.thoughtworks.qdox.library.OrderedClassLibraryBuilder;
53  import com.thoughtworks.qdox.model.DocletTag;
54  import com.thoughtworks.qdox.model.JavaAnnotatedElement;
55  import com.thoughtworks.qdox.model.JavaAnnotation;
56  import com.thoughtworks.qdox.model.JavaClass;
57  import com.thoughtworks.qdox.model.JavaConstructor;
58  import com.thoughtworks.qdox.model.JavaExecutable;
59  import com.thoughtworks.qdox.model.JavaField;
60  import com.thoughtworks.qdox.model.JavaGenericDeclaration;
61  import com.thoughtworks.qdox.model.JavaMember;
62  import com.thoughtworks.qdox.model.JavaMethod;
63  import com.thoughtworks.qdox.model.JavaParameter;
64  import com.thoughtworks.qdox.model.JavaType;
65  import com.thoughtworks.qdox.model.JavaTypeVariable;
66  import com.thoughtworks.qdox.parser.ParseException;
67  import com.thoughtworks.qdox.type.TypeResolver;
68  import org.apache.commons.lang3.ClassUtils;
69  import org.apache.commons.text.StringEscapeUtils;
70  import org.apache.maven.artifact.Artifact;
71  import org.apache.maven.artifact.DependencyResolutionRequiredException;
72  import org.apache.maven.artifact.repository.ArtifactRepository;
73  import org.apache.maven.execution.MavenSession;
74  import org.apache.maven.plugin.AbstractMojo;
75  import org.apache.maven.plugin.MojoExecutionException;
76  import org.apache.maven.plugin.MojoFailureException;
77  import org.apache.maven.plugins.annotations.Component;
78  import org.apache.maven.plugins.annotations.Parameter;
79  import org.apache.maven.project.MavenProject;
80  import org.apache.maven.settings.Settings;
81  import org.apache.maven.shared.invoker.MavenInvocationException;
82  import org.codehaus.plexus.components.interactivity.InputHandler;
83  import org.codehaus.plexus.languages.java.version.JavaVersion;
84  import org.codehaus.plexus.util.FileUtils;
85  import org.codehaus.plexus.util.ReaderFactory;
86  import org.codehaus.plexus.util.StringUtils;
87  
88  /**
89   * Abstract class to fix Javadoc documentation and tags in source files.
90   * @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
91   * Can Be Used</a>.
92   * @author <a href="mailto:vincent.siveton@gmail.com">Vincent Siveton</a>
93   * @since 2.6
94   */
95  public abstract class AbstractFixJavadocMojo extends AbstractMojo {
96      /**
97       * The vm line separator
98       */
99      private static final String EOL = System.getProperty("line.separator");
100 
101     /**
102      * Tag name for &#64;author *
103      */
104     private static final String AUTHOR_TAG = "author";
105 
106     /**
107      * Tag name for &#64;version *
108      */
109     private static final String VERSION_TAG = "version";
110 
111     /**
112      * Tag name for &#64;since *
113      */
114     private static final String SINCE_TAG = "since";
115 
116     /**
117      * Tag name for &#64;param *
118      */
119     private static final String PARAM_TAG = "param";
120 
121     /**
122      * Tag name for &#64;return *
123      */
124     private static final String RETURN_TAG = "return";
125 
126     /**
127      * Tag name for &#64;throws *
128      */
129     private static final String THROWS_TAG = "throws";
130 
131     /**
132      * Tag name for &#64;link *
133      */
134     private static final String LINK_TAG = "link";
135 
136     /**
137      * Tag name for {&#64;inheritDoc} *
138      */
139     private static final String INHERITED_TAG = "{@inheritDoc}";
140 
141     /**
142      * Start Javadoc String i.e. <code>&#47;&#42;&#42;</code> *
143      */
144     private static final String START_JAVADOC = "/**";
145 
146     /**
147      * End Javadoc String i.e. <code>&#42;&#47;</code> *
148      */
149     private static final String END_JAVADOC = "*/";
150 
151     /**
152      * Javadoc Separator i.e. <code> &#42; </code> *
153      */
154     private static final String SEPARATOR_JAVADOC = " * ";
155 
156     /**
157      * Inherited Javadoc i.e. <code>&#47;&#42;&#42; {&#64;inheritDoc} &#42;&#47;</code> *
158      */
159     private static final String INHERITED_JAVADOC = START_JAVADOC + " " + INHERITED_TAG + " " + END_JAVADOC;
160 
161     /**
162      * <code>all</code> parameter used by {@link #fixTags} *
163      */
164     private static final String FIX_TAGS_ALL = "all";
165 
166     /**
167      * <code>public</code> parameter used by {@link #level} *
168      */
169     private static final String LEVEL_PUBLIC = "public";
170 
171     /**
172      * <code>protected</code> parameter used by {@link #level} *
173      */
174     private static final String LEVEL_PROTECTED = "protected";
175 
176     /**
177      * <code>package</code> parameter used by {@link #level} *
178      */
179     private static final String LEVEL_PACKAGE = "package";
180 
181     /**
182      * <code>private</code> parameter used by {@link #level} *
183      */
184     private static final String LEVEL_PRIVATE = "private";
185 
186     /**
187      * The Clirr Maven plugin groupId <code>org.codehaus.mojo</code> *
188      */
189     private static final String CLIRR_MAVEN_PLUGIN_GROUPID = "org.codehaus.mojo";
190 
191     /**
192      * The Clirr Maven plugin artifactId <code>clirr-maven-plugin</code> *
193      */
194     private static final String CLIRR_MAVEN_PLUGIN_ARTIFACTID = "clirr-maven-plugin";
195 
196     /**
197      * The latest Clirr Maven plugin version <code>2.2.2</code> *
198      */
199     private static final String CLIRR_MAVEN_PLUGIN_VERSION = "2.2.2";
200 
201     /**
202      * The Clirr Maven plugin goal <code>check</code> *
203      */
204     private static final String CLIRR_MAVEN_PLUGIN_GOAL = "check";
205 
206     /**
207      * Java Files Pattern.
208      */
209     public static final String JAVA_FILES = "**\\/*.java";
210 
211     /**
212      * Default version value.
213      */
214     public static final String DEFAULT_VERSION_VALUE = "\u0024Id: \u0024Id";
215 
216     // ----------------------------------------------------------------------
217     // Mojo components
218     // ----------------------------------------------------------------------
219 
220     /**
221      * Input handler, needed for command line handling.
222      */
223     @Component
224     private InputHandler inputHandler;
225 
226     // ----------------------------------------------------------------------
227     // Mojo parameters
228     // ----------------------------------------------------------------------
229 
230     /**
231      * Version to compare the current code against using the
232      * <a href="http://mojo.codehaus.org/clirr-maven-plugin/">Clirr Maven Plugin</a>.
233      * <br/>
234      * See <a href="#defaultSince">defaultSince</a>.
235      */
236     @Parameter(property = "comparisonVersion", defaultValue = "(,${project.version})")
237     private String comparisonVersion;
238 
239     /**
240      * Default value for the Javadoc tag <code>&#64;author</code>.
241      * <br/>
242      * If not specified, the <code>user.name</code> defined in the System properties will be used.
243      */
244     @Parameter(property = "defaultAuthor")
245     private String defaultAuthor;
246 
247     /**
248      * Default value for the Javadoc tag <code>&#64;since</code>.
249      */
250     @Parameter(property = "defaultSince", defaultValue = "${project.version}")
251     private String defaultSince;
252 
253     /**
254      * Default value for the Javadoc tag <code>&#64;version</code>.
255      * <br/>
256      * By default, it is <code>&#36;Id:&#36;</code>, corresponding to a
257      * <a href="http://svnbook.red-bean.com/en/1.1/ch07s02.html#svn-ch-7-sect-2.3.4">SVN keyword</a>.
258      * Refer to your SCM to use an other SCM keyword.
259      */
260     @Parameter(property = "defaultVersion", defaultValue = DEFAULT_VERSION_VALUE)
261     private String defaultVersion = "\u0024Id: \u0024"; // can't use default-value="\u0024Id: \u0024"
262 
263     /**
264      * The file encoding to use when reading the source files. If the property
265      * <code>project.build.sourceEncoding</code> is not set, the platform default encoding is used.
266      */
267     @Parameter(property = "encoding", defaultValue = "${project.build.sourceEncoding}")
268     private String encoding;
269 
270     /**
271      * Comma separated excludes Java files, i.e. <code>&#42;&#42;/&#42;Test.java</code>.
272      */
273     @Parameter(property = "excludes")
274     private String excludes;
275 
276     /**
277      * Comma separated tags to fix in classes, interfaces or methods Javadoc comments.
278      * Possible values are:
279      * <ul>
280      * <li>all (fix all Javadoc tags)</li>
281      * <li>author (fix only &#64;author tag)</li>
282      * <li>version (fix only &#64;version tag)</li>
283      * <li>since (fix only &#64;since tag)</li>
284      * <li>param (fix only &#64;param tag)</li>
285      * <li>return (fix only &#64;return tag)</li>
286      * <li>throws (fix only &#64;throws tag)</li>
287      * <li>link (fix only &#64;link tag)</li>
288      * </ul>
289      */
290     @Parameter(property = "fixTags", defaultValue = "all")
291     private String fixTags;
292 
293     /**
294      * Flag to fix the classes or interfaces Javadoc comments according the <code>level</code>.
295      */
296     @Parameter(property = "fixClassComment", defaultValue = "true")
297     private boolean fixClassComment;
298 
299     /**
300      * Flag to fix the fields Javadoc comments according the <code>level</code>.
301      */
302     @Parameter(property = "fixFieldComment", defaultValue = "true")
303     private boolean fixFieldComment;
304 
305     /**
306      * Flag to fix the methods Javadoc comments according the <code>level</code>.
307      */
308     @Parameter(property = "fixMethodComment", defaultValue = "true")
309     private boolean fixMethodComment;
310 
311     /**
312      * <p>Flag to remove throws tags from unknown classes.</p>
313      * <p><strong>NOTE:</strong>Since 3.1.0 the default value has been changed to {@code true},
314      * due to JavaDoc 8 strictness.
315      */
316     @Parameter(property = "removeUnknownThrows", defaultValue = "true")
317     private boolean removeUnknownThrows;
318 
319     /**
320      * Forcing the goal execution i.e. skip warranty messages (not recommended).
321      */
322     @Parameter(property = "force")
323     private boolean force;
324 
325     /**
326      * Flag to ignore or not Clirr.
327      */
328     @Parameter(property = "ignoreClirr", defaultValue = "false")
329     protected boolean ignoreClirr;
330 
331     /**
332      * Comma separated includes Java files, i.e. <code>&#42;&#42;/&#42;Test.java</code>.
333      * <p/>
334      * <strong>Note:</strong> default value is {@code **\/*.java}.
335      */
336     @Parameter(property = "includes", defaultValue = JAVA_FILES)
337     private String includes;
338 
339     /**
340      * Specifies the access level for classes and members to show in the Javadocs.
341      * Possible values are:
342      * <ul>
343      * <li>public (shows only public classes and members)</li>
344      * <li>protected (shows only public and protected classes and members)</li>
345      * <li>package (shows all classes and members not marked private)</li>
346      * <li>private (shows all classes and members)</li>
347      * </ul>
348      * @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>
349      */
350     @Parameter(property = "level", defaultValue = "protected")
351     private String level;
352 
353     /**
354      * The local repository where the artifacts are located, used by the tests.
355      */
356     @Parameter(property = "localRepository")
357     private ArtifactRepository localRepository;
358 
359     /**
360      * Output directory where Java classes will be rewritten.
361      */
362     @Parameter(property = "outputDirectory", defaultValue = "${project.build.sourceDirectory}")
363     private File outputDirectory;
364 
365     /**
366      * The Maven Project Object.
367      */
368     @Parameter(defaultValue = "${project}", readonly = true, required = true)
369     private MavenProject project;
370 
371     @Parameter(defaultValue = "${session}", readonly = true, required = true)
372     private MavenSession session;
373 
374     /**
375      * The current user system settings for use in Maven.
376      */
377     @Parameter(defaultValue = "${settings}", readonly = true, required = true)
378     private Settings settings;
379 
380     // ----------------------------------------------------------------------
381     // Internal fields
382     // ----------------------------------------------------------------------
383 
384     /**
385      * The current project class loader.
386      */
387     private ClassLoader projectClassLoader;
388 
389     /**
390      * Split {@link #fixTags} by comma.
391      *
392      * @see #init()
393      */
394     private String[] fixTagsSplitted;
395 
396     /**
397      * New classes found by Clirr.
398      */
399     private List<String> clirrNewClasses;
400 
401     /**
402      * New Methods in a Class (the key) found by Clirr.
403      */
404     private Map<String, List<String>> clirrNewMethods;
405 
406     /**
407      * List of classes where <code>&#42;since</code> is added. Will be used to add or not this tag in the methods.
408      */
409     private List<String> sinceClasses;
410 
411     /**
412      * {@inheritDoc}
413      */
414     @Override
415     public void execute() throws MojoExecutionException, MojoFailureException {
416         if (!fixClassComment && !fixFieldComment && !fixMethodComment) {
417             getLog().info("Specified to NOT fix classes, fields and methods. Nothing to do.");
418             return;
419         }
420 
421         // verify goal params
422         init();
423 
424         if (fixTagsSplitted.length == 0) {
425             getLog().info("No fix tag specified. Nothing to do.");
426             return;
427         }
428 
429         // add warranty msg
430         if (!preCheck()) {
431             return;
432         }
433 
434         // run clirr
435         try {
436             executeClirr();
437         } catch (MavenInvocationException e) {
438             if (getLog().isDebugEnabled()) {
439                 getLog().error("MavenInvocationException: " + e.getMessage(), e);
440             } else {
441                 getLog().error("MavenInvocationException: " + e.getMessage());
442             }
443             getLog().info("Clirr is ignored.");
444         }
445 
446         // run qdox and process
447         try {
448             Collection<JavaClass> javaClasses = getQdoxClasses();
449 
450             if (javaClasses != null) {
451                 for (JavaClass javaClass : javaClasses) {
452                     processFix(javaClass);
453                 }
454             }
455         } catch (IOException e) {
456             throw new MojoExecutionException("IOException: " + e.getMessage(), e);
457         }
458     }
459 
460     // ----------------------------------------------------------------------
461     // protected methods
462     // ----------------------------------------------------------------------
463 
464     protected final MavenProject getProject() {
465         return project;
466     }
467 
468     /**
469      * @param p not null maven project.
470      * @return the artifact type.
471      */
472     protected String getArtifactType(MavenProject p) {
473         return p.getArtifact().getType();
474     }
475 
476     /**
477      * @param p not null maven project.
478      * @return the list of source paths for the given project.
479      */
480     protected List<String> getProjectSourceRoots(MavenProject p) {
481         return (p.getCompileSourceRoots() == null
482                 ? Collections.<String>emptyList()
483                 : new LinkedList<>(p.getCompileSourceRoots()));
484     }
485 
486     /**
487      * @param p not null
488      * @return the compile classpath elements
489      * @throws DependencyResolutionRequiredException
490      *          if any
491      */
492     protected List<String> getCompileClasspathElements(MavenProject p) throws DependencyResolutionRequiredException {
493         return (p.getCompileClasspathElements() == null
494                 ? Collections.<String>emptyList()
495                 : new LinkedList<>(p.getCompileClasspathElements()));
496     }
497 
498     /**
499      * @param javaExecutable not null
500      * @return the fully qualify name of javaMethod with signature
501      */
502     protected static String getJavaMethodAsString(JavaExecutable javaExecutable) {
503         return javaExecutable.getDeclaringClass().getFullyQualifiedName() + "#" + javaExecutable.getCallSignature();
504     }
505 
506     // ----------------------------------------------------------------------
507     // private methods
508     // ----------------------------------------------------------------------
509 
510     /**
511      * Init goal parameters.
512      */
513     private void init() {
514         // defaultAuthor
515         if (StringUtils.isEmpty(defaultAuthor)) {
516             defaultAuthor = System.getProperty("user.name");
517         }
518 
519         // defaultSince
520         int i = defaultSince.indexOf("-" + Artifact.SNAPSHOT_VERSION);
521         if (i != -1) {
522             defaultSince = defaultSince.substring(0, i);
523         }
524 
525         // fixTags
526         if (!FIX_TAGS_ALL.equalsIgnoreCase(fixTags.trim())) {
527             String[] split = StringUtils.split(fixTags, ",");
528             List<String> filtered = new LinkedList<>();
529             for (String aSplit : split) {
530                 String s = aSplit.trim();
531                 if (JavadocUtil.equalsIgnoreCase(
532                         s,
533                         FIX_TAGS_ALL,
534                         AUTHOR_TAG,
535                         VERSION_TAG,
536                         SINCE_TAG,
537                         PARAM_TAG,
538                         THROWS_TAG,
539                         LINK_TAG,
540                         RETURN_TAG)) {
541                     filtered.add(s);
542                 } else {
543                     if (getLog().isWarnEnabled()) {
544                         getLog().warn("Unrecognized '" + s + "' for fixTags parameter. Ignored it!");
545                     }
546                 }
547             }
548             fixTags = StringUtils.join(filtered.iterator(), ",");
549         }
550         fixTagsSplitted = StringUtils.split(fixTags, ",");
551 
552         // encoding
553         if (StringUtils.isEmpty(encoding)) {
554             if (getLog().isWarnEnabled()) {
555                 getLog().warn("File encoding has not been set, using platform encoding " + ReaderFactory.FILE_ENCODING
556                         + ", i.e. build is platform dependent!");
557             }
558             encoding = ReaderFactory.FILE_ENCODING;
559         }
560 
561         // level
562         level = level.trim();
563         if (!JavadocUtil.equalsIgnoreCase(level, LEVEL_PUBLIC, LEVEL_PROTECTED, LEVEL_PACKAGE, LEVEL_PRIVATE)) {
564             if (getLog().isWarnEnabled()) {
565                 getLog().warn("Unrecognized '" + level + "' for level parameter, using 'protected' level.");
566             }
567             level = "protected";
568         }
569     }
570 
571     /**
572      * @return <code>true</code> if the user wants to proceed, <code>false</code> otherwise.
573      * @throws MojoExecutionException if any
574      */
575     private boolean preCheck() throws MojoExecutionException {
576         if (force) {
577             return true;
578         }
579 
580         if (outputDirectory != null
581                 && !outputDirectory
582                         .getAbsolutePath()
583                         .equals(getProjectSourceDirectory().getAbsolutePath())) {
584             return true;
585         }
586 
587         if (!settings.isInteractiveMode()) {
588             getLog().error("Maven is not attempt to interact with the user for input. "
589                     + "Verify the <interactiveMode/> configuration in your settings.");
590             return false;
591         }
592 
593         getLog().warn("");
594         getLog().warn("    WARRANTY DISCLAIMER");
595         getLog().warn("");
596         getLog().warn("All warranties with regard to this Maven goal are disclaimed!");
597         getLog().warn("The changes will be done directly in the source code.");
598         getLog().warn("The Maven Team strongly recommends the use of a SCM software BEFORE executing this goal.");
599         getLog().warn("");
600 
601         while (true) {
602             getLog().info("Are you sure to proceed? [Y]es [N]o");
603 
604             try {
605                 String userExpression = inputHandler.readLine();
606                 if (userExpression == null || JavadocUtil.equalsIgnoreCase(userExpression, "Y", "Yes")) {
607                     getLog().info("OK, let's proceed...");
608                     break;
609                 }
610                 if (JavadocUtil.equalsIgnoreCase(userExpression, "N", "No")) {
611                     getLog().info("No changes in your sources occur.");
612                     return false;
613                 }
614             } catch (IOException e) {
615                 throw new MojoExecutionException("Unable to read from standard input.", e);
616             }
617         }
618 
619         return true;
620     }
621 
622     /**
623      * @return the source dir as File for the given project
624      */
625     private File getProjectSourceDirectory() {
626         return new File(project.getBuild().getSourceDirectory());
627     }
628 
629     /**
630      * Invoke Maven to run clirr-maven-plugin to find API differences.
631      *
632      * @throws MavenInvocationException if any
633      */
634     private void executeClirr() throws MavenInvocationException {
635         if (ignoreClirr) {
636             getLog().info("Clirr is ignored.");
637             return;
638         }
639 
640         String clirrGoal = getFullClirrGoal();
641 
642         // http://mojo.codehaus.org/clirr-maven-plugin/check-mojo.html
643         File clirrTextOutputFile = FileUtils.createTempFile(
644                 "clirr", ".txt", new File(project.getBuild().getDirectory()));
645         Properties properties = new Properties();
646         properties.put("textOutputFile", clirrTextOutputFile.getAbsolutePath());
647         properties.put("comparisonVersion", comparisonVersion);
648         properties.put("failOnError", "false");
649         if (JavaVersion.JAVA_SPECIFICATION_VERSION.isBefore("8")) {
650             // ensure that Java7 picks up TLSv1.2 when connecting with Central
651             properties.put("https.protocols", "TLSv1.2");
652         }
653 
654         File invokerDir = new File(project.getBuild().getDirectory(), "invoker");
655         invokerDir.mkdirs();
656         File invokerLogFile = FileUtils.createTempFile("clirr-maven-plugin", ".txt", invokerDir);
657 
658         JavadocUtil.invokeMaven(
659                 getLog(),
660                 new File(localRepository.getBasedir()),
661                 project.getFile(),
662                 Collections.singletonList(clirrGoal),
663                 properties,
664                 invokerLogFile,
665                 session.getRequest().getGlobalSettingsFile());
666 
667         try {
668             if (invokerLogFile.exists()) {
669                 String invokerLogContent = StringUtils.unifyLineSeparators(FileUtils.fileRead(invokerLogFile, "UTF-8"));
670                 // see org.codehaus.mojo.clirr.AbstractClirrMojo#getComparisonArtifact()
671                 final String artifactNotFoundMsg = "Unable to find a previous version of the project in the repository";
672                 if (invokerLogContent.contains(artifactNotFoundMsg)) {
673                     getLog().warn("No previous artifact has been deployed, Clirr is ignored.");
674                     return;
675                 }
676             }
677         } catch (IOException e) {
678             getLog().debug("IOException: " + e.getMessage());
679         }
680 
681         try {
682             parseClirrTextOutputFile(clirrTextOutputFile);
683         } catch (IOException e) {
684             if (getLog().isDebugEnabled()) {
685                 getLog().debug("IOException: " + e.getMessage(), e);
686             }
687             getLog().info("IOException when parsing Clirr output '" + clirrTextOutputFile.getAbsolutePath()
688                     + "', Clirr is ignored.");
689         }
690     }
691 
692     /**
693      * @param clirrTextOutputFile not null
694      * @throws IOException if any
695      */
696     private void parseClirrTextOutputFile(File clirrTextOutputFile) throws IOException {
697         if (!clirrTextOutputFile.exists()) {
698             if (getLog().isInfoEnabled()) {
699                 getLog().info("No Clirr output file '" + clirrTextOutputFile.getAbsolutePath()
700                         + "' exists, Clirr is ignored.");
701             }
702             return;
703         }
704 
705         if (getLog().isInfoEnabled()) {
706             getLog().info("Clirr output file was created: " + clirrTextOutputFile.getAbsolutePath());
707         }
708 
709         clirrNewClasses = new LinkedList<>();
710         clirrNewMethods = new LinkedHashMap<>();
711 
712         try (BufferedReader reader = new BufferedReader(ReaderFactory.newReader(clirrTextOutputFile, "UTF-8"))) {
713 
714             for (String line = reader.readLine(); line != null; line = reader.readLine()) {
715                 String[] split = StringUtils.split(line, ":");
716                 if (split.length != 4) {
717                     if (getLog().isDebugEnabled()) {
718                         getLog().debug("Unable to parse the clirr line: " + line);
719                     }
720                     continue;
721                 }
722 
723                 int code;
724                 try {
725                     code = Integer.parseInt(split[1].trim());
726                 } catch (NumberFormatException e) {
727                     if (getLog().isDebugEnabled()) {
728                         getLog().debug("Unable to parse the clirr line: " + line);
729                     }
730                     continue;
731                 }
732 
733                 // http://clirr.sourceforge.net/clirr-core/exegesis.html
734                 // 7011 - Method Added
735                 // 7012 - Method Added to Interface
736                 // 8000 - Class Added
737 
738                 // CHECKSTYLE_OFF: MagicNumber
739                 switch (code) {
740                     case 7011:
741                         methodAdded(split);
742                         break;
743                     case 7012:
744                         methodAdded(split);
745                         break;
746                     case 8000:
747                         clirrNewClasses.add(split[2].trim());
748                         break;
749                     default:
750                         break;
751                 }
752                 // CHECKSTYLE_ON: MagicNumber
753             }
754         }
755         if (clirrNewClasses.isEmpty() && clirrNewMethods.isEmpty()) {
756             getLog().info("Clirr NOT found API differences.");
757         } else {
758             getLog().info("Clirr found API differences, i.e. new classes/interfaces or methods.");
759         }
760     }
761 
762     private void methodAdded(String[] split) {
763         List<String> list = clirrNewMethods.get(split[2].trim());
764         if (list == null) {
765             list = new ArrayList<>();
766         }
767         String[] splits2 = StringUtils.split(split[3].trim(), "'");
768         if (splits2.length != 3) {
769             return;
770         }
771         list.add(splits2[1].trim());
772         clirrNewMethods.put(split[2].trim(), list);
773     }
774 
775     /**
776      * @param tag not null
777      * @return <code>true</code> if <code>tag</code> is defined in {@link #fixTags}.
778      */
779     private boolean fixTag(String tag) {
780         if (fixTagsSplitted.length == 1 && fixTagsSplitted[0].equals(FIX_TAGS_ALL)) {
781             return true;
782         }
783 
784         for (String aFixTagsSplitted : fixTagsSplitted) {
785             if (aFixTagsSplitted.trim().equals(tag)) {
786                 return true;
787             }
788         }
789 
790         return false;
791     }
792 
793     /**
794      * Calling Qdox to find {@link JavaClass} objects from the Maven project sources.
795      * Ignore java class if Qdox has parsing errors.
796      *
797      * @return an array of {@link JavaClass} found by QDox
798      * @throws IOException            if any
799      * @throws MojoExecutionException if any
800      */
801     private Collection<JavaClass> getQdoxClasses() throws IOException, MojoExecutionException {
802         if ("pom".equalsIgnoreCase(project.getPackaging())) {
803             getLog().warn("This project has 'pom' packaging, no Java sources is available.");
804             return null;
805         }
806 
807         List<File> javaFiles = new LinkedList<>();
808         for (String sourceRoot : getProjectSourceRoots(project)) {
809             File f = new File(sourceRoot);
810             if (f.isDirectory()) {
811                 javaFiles.addAll(FileUtils.getFiles(f, includes, excludes, true));
812             } else {
813                 if (getLog().isWarnEnabled()) {
814                     getLog().warn(f + " doesn't exist. Ignored it.");
815                 }
816             }
817         }
818 
819         ClassLibraryBuilder classLibraryBuilder = new OrderedClassLibraryBuilder();
820         classLibraryBuilder.appendClassLoader(getProjectClassLoader());
821 
822         JavaProjectBuilder builder = new JavaProjectBuilder(classLibraryBuilder);
823         builder.setEncoding(encoding);
824         for (File f : javaFiles) {
825             if (!f.getAbsolutePath().toLowerCase(Locale.ENGLISH).endsWith(".java") && getLog().isWarnEnabled()) {
826                 getLog().warn("'" + f + "' is not a Java file. Ignored it.");
827                 continue;
828             }
829 
830             try {
831                 builder.addSource(f);
832             } catch (ParseException e) {
833                 if (getLog().isWarnEnabled()) {
834                     getLog().warn("QDOX ParseException: " + e.getMessage() + ". Can't fix it.");
835                 }
836             }
837         }
838 
839         return builder.getClasses();
840     }
841 
842     /**
843      * @return the classLoader for the given project using lazy instantiation.
844      * @throws MojoExecutionException if any
845      */
846     private ClassLoader getProjectClassLoader() throws MojoExecutionException {
847         if (projectClassLoader == null) {
848             List<String> classPath;
849             try {
850                 classPath = getCompileClasspathElements(project);
851             } catch (DependencyResolutionRequiredException e) {
852                 throw new MojoExecutionException("DependencyResolutionRequiredException: " + e.getMessage(), e);
853             }
854 
855             List<URL> urls = new ArrayList<>(classPath.size());
856             for (String filename : classPath) {
857                 try {
858                     urls.add(new File(filename).toURI().toURL());
859                 } catch (MalformedURLException e) {
860                     throw new MojoExecutionException("MalformedURLException: " + e.getMessage(), e);
861                 }
862             }
863 
864             projectClassLoader = new URLClassLoader(urls.toArray(new URL[urls.size()]), null);
865         }
866 
867         return projectClassLoader;
868     }
869 
870     /**
871      * Process the given {@link JavaClass}, ie add missing javadoc tags depending user parameters.
872      *
873      * @param javaClass not null
874      * @throws IOException            if any
875      * @throws MojoExecutionException if any
876      */
877     private void processFix(JavaClass javaClass) throws IOException, MojoExecutionException {
878         // Skipping inner classes
879         if (javaClass.isInner()) {
880             return;
881         }
882 
883         File javaFile;
884         try {
885             javaFile = Paths.get(javaClass.getSource().getURL().toURI()).toFile();
886         } catch (URISyntaxException e) {
887             throw new MojoExecutionException(e.getMessage());
888         }
889 
890         // the original java content in memory
891         final String originalContent = StringUtils.unifyLineSeparators(FileUtils.fileRead(javaFile, encoding));
892 
893         if (getLog().isDebugEnabled()) {
894             getLog().debug("Analyzing " + javaClass.getFullyQualifiedName());
895         }
896 
897         final StringWriter stringWriter = new StringWriter();
898         boolean changeDetected = false;
899         try (BufferedReader reader = new BufferedReader(new StringReader(originalContent))) {
900 
901             int lineNumber = 0;
902             for (String line = reader.readLine(); line != null; line = reader.readLine()) {
903                 lineNumber++;
904                 final String indent = autodetectIndentation(line);
905 
906                 // fixing classes
907                 if (javaClass.getComment() == null
908                         && javaClass.getAnnotations() != null
909                         && !javaClass.getAnnotations().isEmpty()) {
910                     if (lineNumber == javaClass.getAnnotations().get(0).getLineNumber()) {
911                         changeDetected |= fixClassComment(stringWriter, originalContent, javaClass, indent);
912 
913                         takeCareSingleComment(stringWriter, originalContent, javaClass);
914                     }
915                 } else if (lineNumber == javaClass.getLineNumber()) {
916                     changeDetected |= fixClassComment(stringWriter, originalContent, javaClass, indent);
917 
918                     takeCareSingleComment(stringWriter, originalContent, javaClass);
919                 }
920 
921                 // fixing fields
922                 if (javaClass.getFields() != null) {
923                     for (JavaField field : javaClass.getFields()) {
924                         if (lineNumber == field.getLineNumber()) {
925                             changeDetected |= fixFieldComment(stringWriter, javaClass, field, indent);
926                         }
927                     }
928                 }
929 
930                 // fixing methods
931                 if (javaClass.getConstructors() != null) {
932                     for (JavaConstructor method : javaClass.getConstructors()) {
933                         if (lineNumber == method.getLineNumber()) {
934                             final boolean commentUpdated =
935                                     fixMethodComment(stringWriter, originalContent, method, indent);
936                             if (commentUpdated) {
937                                 takeCareSingleComment(stringWriter, originalContent, method);
938                             }
939                             changeDetected |= commentUpdated;
940                         }
941                     }
942                 }
943 
944                 // fixing methods
945                 for (JavaMethod method : javaClass.getMethods()) {
946                     int methodLineNumber;
947                     if (method.getComment() == null && !method.getAnnotations().isEmpty()) {
948                         methodLineNumber = method.getAnnotations().get(0).getLineNumber();
949                     } else {
950                         methodLineNumber = method.getLineNumber();
951                     }
952 
953                     if (lineNumber == methodLineNumber) {
954                         final boolean commentUpdated = fixMethodComment(stringWriter, originalContent, method, indent);
955                         if (commentUpdated) {
956                             takeCareSingleComment(stringWriter, originalContent, method);
957                         }
958                         changeDetected |= commentUpdated;
959                     }
960                 }
961 
962                 stringWriter.write(line);
963                 stringWriter.write(EOL);
964             }
965         }
966 
967         if (changeDetected) {
968             if (getLog().isInfoEnabled()) {
969                 getLog().info("Saving changes to " + javaClass.getFullyQualifiedName());
970             }
971 
972             if (outputDirectory != null
973                     && !outputDirectory
974                             .getAbsolutePath()
975                             .equals(getProjectSourceDirectory().getAbsolutePath())) {
976                 String path = StringUtils.replace(
977                         javaFile.getAbsolutePath().replaceAll("\\\\", "/"),
978                         project.getBuild().getSourceDirectory().replaceAll("\\\\", "/"),
979                         "");
980                 javaFile = new File(outputDirectory, path);
981                 javaFile.getParentFile().mkdirs();
982             }
983             writeFile(javaFile, encoding, stringWriter.toString());
984         } else {
985             if (getLog().isDebugEnabled()) {
986                 getLog().debug("No changes made to " + javaClass.getFullyQualifiedName());
987             }
988         }
989     }
990 
991     /**
992      * Take care of block or single comments between Javadoc comment and entity declaration ie:
993      * <br/>
994      * <code>
995      * <font color="#808080">1</font>&nbsp;<font color="#ffffff">&nbsp;</font>
996      * <font color="#3f5fbf">&#47;&#42;&#42;</font><br />
997      * <font color="#808080">2</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;</font>
998      * <font color="#3f5fbf">&#42;&nbsp;{Javadoc&nbsp;Comment}</font><br />
999      * <font color="#808080">3</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;</font>
1000      * <font color="#3f5fbf">&#42;&#47;</font><br />
1001      * <font color="#808080">4</font>&nbsp;<font color="#ffffff">&nbsp;</font>
1002      * <font color="#3f7f5f">&#47;&#42;</font><br />
1003      * <font color="#808080">5</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;</font>
1004      * <font color="#3f7f5f">&#42;&nbsp;{Block&nbsp;Comment}</font><br />
1005      * <font color="#808080">6</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;</font>
1006      * <font color="#3f7f5f">&#42;&#47;</font><br />
1007      * <font color="#808080">7</font>&nbsp;<font color="#ffffff">&nbsp;</font>
1008      * <font color="#3f7f5f">&#47;&#47;&nbsp;{Single&nbsp;comment}</font><br />
1009      * <font color="#808080">8</font>&nbsp;<font color="#ffffff">&nbsp;</font>
1010      * <font color="#7f0055"><b>public&nbsp;</b></font><font color="#7f0055"><b>void&nbsp;</b></font>
1011      * <font color="#000000">dummyMethod</font><font color="#000000">(&nbsp;</font>
1012      * <font color="#000000">String&nbsp;s&nbsp;</font><font color="#000000">){}</font>
1013      * </code>
1014      *
1015      * @param stringWriter    not null
1016      * @param originalContent not null
1017      * @param entity          not null
1018      * @throws IOException if any
1019      * @see #extractOriginalJavadoc
1020      */
1021     private void takeCareSingleComment(
1022             final StringWriter stringWriter, final String originalContent, final JavaAnnotatedElement entity)
1023             throws IOException {
1024         if (entity.getComment() == null) {
1025             return;
1026         }
1027 
1028         String javadocComment = trimRight(extractOriginalJavadoc(originalContent, entity));
1029         String extraComment = javadocComment.substring(javadocComment.indexOf(END_JAVADOC) + END_JAVADOC.length());
1030         if (StringUtils.isNotEmpty(extraComment)) {
1031             if (extraComment.contains(EOL)) {
1032                 stringWriter.write(extraComment.substring(extraComment.indexOf(EOL) + EOL.length()));
1033             } else {
1034                 stringWriter.write(extraComment);
1035             }
1036             stringWriter.write(EOL);
1037         }
1038     }
1039 
1040     /**
1041      * Add/update Javadoc class comment.
1042      *
1043      * @param stringWriter
1044      * @param originalContent
1045      * @param javaClass
1046      * @param indent
1047      * @return {@code true} if the comment is updated, otherwise {@code false}
1048      * @throws MojoExecutionException
1049      * @throws IOException
1050      */
1051     private boolean fixClassComment(
1052             final StringWriter stringWriter,
1053             final String originalContent,
1054             final JavaClass javaClass,
1055             final String indent)
1056             throws MojoExecutionException, IOException {
1057         if (!fixClassComment) {
1058             return false;
1059         }
1060 
1061         if (!isInLevel(javaClass.getModifiers())) {
1062             return false;
1063         }
1064 
1065         // add
1066         if (javaClass.getComment() == null) {
1067             addDefaultClassComment(stringWriter, javaClass, indent);
1068             return true;
1069         }
1070 
1071         // update
1072         return updateEntityComment(stringWriter, originalContent, javaClass, indent);
1073     }
1074 
1075     /**
1076      * @param modifiers list of modifiers (public, private, protected, package)
1077      * @return <code>true</code> if modifier is align with <code>level</code>.
1078      */
1079     private boolean isInLevel(List<String> modifiers) {
1080         if (LEVEL_PUBLIC.equalsIgnoreCase(level.trim())) {
1081             return modifiers.contains(LEVEL_PUBLIC);
1082         }
1083 
1084         if (LEVEL_PROTECTED.equalsIgnoreCase(level.trim())) {
1085             return (modifiers.contains(LEVEL_PUBLIC) || modifiers.contains(LEVEL_PROTECTED));
1086         }
1087 
1088         if (LEVEL_PACKAGE.equalsIgnoreCase(level.trim())) {
1089             return !modifiers.contains(LEVEL_PRIVATE);
1090         }
1091 
1092         // should be private (shows all classes and members)
1093         return true;
1094     }
1095 
1096     /**
1097      * Add a default Javadoc for the given class, i.e.:
1098      * <br/>
1099      * <code>
1100      * <font color="#808080">1</font>&nbsp;<font color="#ffffff">&nbsp;</font>
1101      * <font color="#3f5fbf">&#47;&#42;&#42;</font><br />
1102      * <font color="#808080">2</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;</font>
1103      * <font color="#3f5fbf">&#42;&nbsp;{Comment&nbsp;based&nbsp;on&nbsp;the&nbsp;class&nbsp;name}</font><br />
1104      * <font color="#808080">3</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;</font>
1105      * <font color="#3f5fbf">&#42;</font><br />
1106      * <font color="#808080">4</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;</font>
1107      * <font color="#3f5fbf">&#42;&nbsp;</font><font color="#7f9fbf">@author&nbsp;</font>
1108      * <font color="#3f5fbf">X&nbsp;{added&nbsp;if&nbsp;addMissingAuthor}</font><br />
1109      * <font color="#808080">5</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;</font>
1110      * <font color="#3f5fbf">&#42;&nbsp;</font><font color="#7f9fbf">@version&nbsp;</font>
1111      * <font color="#3f5fbf">X&nbsp;{added&nbsp;if&nbsp;addMissingVersion}</font><br />
1112      * <font color="#808080">6</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;</font>
1113      * <font color="#3f5fbf">&#42;&nbsp;</font><font color="#7f9fbf">@since&nbsp;</font>
1114      * <font color="#3f5fbf">X&nbsp;{added&nbsp;if&nbsp;addMissingSince&nbsp;and&nbsp;new&nbsp;classes
1115      * from&nbsp;previous&nbsp;version}</font><br />
1116      * <font color="#808080">7</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;</font>
1117      * <font color="#3f5fbf">&#42;&#47;</font><br />
1118      * <font color="#808080">8</font>&nbsp;<font color="#7f0055"><b>public&nbsp;class&nbsp;</b></font>
1119      * <font color="#000000">DummyClass&nbsp;</font><font color="#000000">{}</font></code>
1120      * </code>
1121      *
1122      * @param stringWriter not null
1123      * @param javaClass    not null
1124      * @param indent       not null
1125      * @see #getDefaultClassJavadocComment(JavaClass)
1126      * @see #appendDefaultAuthorTag(StringBuilder, String)
1127      * @see #appendDefaultSinceTag(StringBuilder, String)
1128      * @see #appendDefaultVersionTag(StringBuilder, String)
1129      */
1130     private void addDefaultClassComment(
1131             final StringWriter stringWriter, final JavaClass javaClass, final String indent) {
1132         StringBuilder sb = new StringBuilder();
1133 
1134         sb.append(indent).append(START_JAVADOC);
1135         sb.append(EOL);
1136         sb.append(indent).append(SEPARATOR_JAVADOC);
1137         sb.append(getDefaultClassJavadocComment(javaClass));
1138         sb.append(EOL);
1139 
1140         appendSeparator(sb, indent);
1141 
1142         appendDefaultAuthorTag(sb, indent);
1143 
1144         appendDefaultVersionTag(sb, indent);
1145 
1146         if (fixTag(SINCE_TAG)) {
1147             if (!ignoreClirr) {
1148                 if (isNewClassFromLastVersion(javaClass)) {
1149                     appendDefaultSinceTag(sb, indent);
1150                 }
1151             } else {
1152                 appendDefaultSinceTag(sb, indent);
1153                 addSinceClasses(javaClass);
1154             }
1155         }
1156 
1157         sb.append(indent).append(" ").append(END_JAVADOC);
1158         sb.append(EOL);
1159 
1160         stringWriter.write(sb.toString());
1161     }
1162 
1163     /**
1164      * Add Javadoc field comment, only for static fields or interface fields.
1165      *
1166      * @param stringWriter not null
1167      * @param javaClass    not null
1168      * @param field        not null
1169      * @param indent       not null
1170      * @return {@code true} if comment was updated, otherwise {@code false}
1171      * @throws IOException if any
1172      */
1173     private boolean fixFieldComment(
1174             final StringWriter stringWriter, final JavaClass javaClass, final JavaField field, final String indent)
1175             throws IOException {
1176         if (!fixFieldComment) {
1177             return false;
1178         }
1179 
1180         if (!javaClass.isInterface() && (!isInLevel(field.getModifiers()) || !field.isStatic())) {
1181             return false;
1182         }
1183 
1184         // add
1185         if (field.getComment() == null) {
1186             addDefaultFieldComment(stringWriter, field, indent);
1187             return true;
1188         }
1189 
1190         // no update
1191         return false;
1192     }
1193 
1194     /**
1195      * Add a default Javadoc for the given field, i.e.:
1196      * <br/>
1197      * <code>
1198      * <font color="#808080">1</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;</font>
1199      * <font color="#3f5fbf">&#47;&#42;&#42;&nbsp;Constant&nbsp;</font><font color="#7f7f9f">&lt;code&gt;</font>
1200      * <font color="#3f5fbf">MY_STRING_CONSTANT=&#34;value&#34;</font>
1201      * <font color="#7f7f9f">&lt;/code&gt;&nbsp;</font><font color="#3f5fbf">&#42;&#47;</font><br />
1202      * <font color="#808080">2</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;</font>
1203      * <font color="#7f0055"><b>public&nbsp;static&nbsp;final&nbsp;</b></font>
1204      * <font color="#000000">String&nbsp;MY_STRING_CONSTANT&nbsp;=&nbsp;</font>
1205      * <font color="#2a00ff">&#34;value&#34;</font><font color="#000000">;</font>
1206      * </code>
1207      *
1208      * @param stringWriter not null
1209      * @param field        not null
1210      * @param indent       not null
1211      * @throws IOException if any
1212      */
1213     private void addDefaultFieldComment(final StringWriter stringWriter, final JavaField field, final String indent)
1214             throws IOException {
1215         StringBuilder sb = new StringBuilder();
1216 
1217         sb.append(indent).append(START_JAVADOC).append(" ");
1218         sb.append("Constant <code>").append(field.getName());
1219 
1220         if (StringUtils.isNotEmpty(field.getInitializationExpression())) {
1221             String qualifiedName = field.getType().getFullyQualifiedName();
1222 
1223             if (qualifiedName.equals(Byte.TYPE.toString())
1224                     || qualifiedName.equals(Short.TYPE.toString())
1225                     || qualifiedName.equals(Integer.TYPE.toString())
1226                     || qualifiedName.equals(Long.TYPE.toString())
1227                     || qualifiedName.equals(Float.TYPE.toString())
1228                     || qualifiedName.equals(Double.TYPE.toString())
1229                     || qualifiedName.equals(Boolean.TYPE.toString())
1230                     || qualifiedName.equals(Character.TYPE.toString())) {
1231                 sb.append("=");
1232                 sb.append(StringEscapeUtils.escapeHtml4(
1233                         field.getInitializationExpression().trim()));
1234             }
1235 
1236             if (qualifiedName.equals(String.class.getName())) {
1237                 StringBuilder value = new StringBuilder();
1238                 String[] lines = getLines(field.getInitializationExpression());
1239                 for (String line : lines) {
1240                     StringTokenizer token = new StringTokenizer(line.trim(), "\"\n\r");
1241                     while (token.hasMoreTokens()) {
1242                         String s = token.nextToken();
1243 
1244                         if (s.trim().equals("+")) {
1245                             continue;
1246                         }
1247                         if (s.trim().endsWith("\\")) {
1248                             s += "\"";
1249                         }
1250                         value.append(s);
1251                     }
1252                 }
1253 
1254                 sb.append("=\"");
1255                 String escapedValue = StringEscapeUtils.escapeHtml4(value.toString());
1256                 // reduce the size
1257                 // CHECKSTYLE_OFF: MagicNumber
1258                 if (escapedValue.length() < 40) {
1259                     sb.append(escapedValue).append("\"");
1260                 } else {
1261                     sb.append(escapedValue, 0, 39).append("\"{trunked}");
1262                 }
1263                 // CHECKSTYLE_ON: MagicNumber
1264             }
1265         }
1266 
1267         sb.append("</code> ").append(END_JAVADOC);
1268         sb.append(EOL);
1269 
1270         stringWriter.write(sb.toString());
1271     }
1272 
1273     /**
1274      * Add/update Javadoc method comment.
1275      *
1276      * @param stringWriter    not null
1277      * @param originalContent not null
1278      * @param javaExecutable      not null
1279      * @param indent          not null
1280      * @return {@code true} if comment was updated, otherwise {@code false}
1281      * @throws MojoExecutionException if any
1282      * @throws IOException            if any
1283      */
1284     private boolean fixMethodComment(
1285             final StringWriter stringWriter,
1286             final String originalContent,
1287             final JavaExecutable javaExecutable,
1288             final String indent)
1289             throws MojoExecutionException, IOException {
1290         if (!fixMethodComment) {
1291             return false;
1292         }
1293 
1294         if (!javaExecutable.getDeclaringClass().isInterface() && !isInLevel(javaExecutable.getModifiers())) {
1295             return false;
1296         }
1297 
1298         // add
1299         if (javaExecutable.getComment() == null) {
1300             addDefaultMethodComment(stringWriter, javaExecutable, indent);
1301             return true;
1302         }
1303 
1304         // update
1305         return updateEntityComment(stringWriter, originalContent, javaExecutable, indent);
1306     }
1307 
1308     /**
1309      * Add in the buffer a default Javadoc for the given class:
1310      * <br/>
1311      * <code>
1312      * <font color="#808080">1</font>&nbsp;<font color="#ffffff">&nbsp;</font>
1313      * <font color="#3f5fbf">&#47;&#42;&#42;</font><br />
1314      * <font color="#808080">2</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;</font>
1315      * <font color="#3f5fbf">&#42;&nbsp;{Comment&nbsp;based&nbsp;on&nbsp;the&nbsp;method&nbsp;name}</font><br />
1316      * <font color="#808080">3</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;</font>
1317      * <font color="#3f5fbf">&#42;</font><br />
1318      * <font color="#808080">4</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;</font>
1319      * <font color="#3f5fbf">&#42;&nbsp;</font><font color="#7f9fbf">@param&nbsp;</font>
1320      * <font color="#3f5fbf">X&nbsp;{added&nbsp;if&nbsp;addMissingParam}</font><br />
1321      * <font color="#808080">5</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;</font>
1322      * <font color="#3f5fbf">&#42;&nbsp;</font><font color="#7f9fbf">@return&nbsp;</font>
1323      * <font color="#3f5fbf">X&nbsp;{added&nbsp;if&nbsp;addMissingReturn}</font><br />
1324      * <font color="#808080">6</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;</font>
1325      * <font color="#3f5fbf">&#42;&nbsp;</font><font color="#7f9fbf">@throws&nbsp;</font>
1326      * <font color="#3f5fbf">X&nbsp;{added&nbsp;if&nbsp;addMissingThrows}</font><br />
1327      * <font color="#808080">7</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;</font>
1328      * <font color="#3f5fbf">&#42;&nbsp;</font><font color="#7f9fbf">@since&nbsp;</font>
1329      * <font color="#3f5fbf">X&nbsp;{added&nbsp;if&nbsp;addMissingSince&nbsp;and&nbsp;new&nbsp;classes
1330      * from&nbsp;previous&nbsp;version}</font><br />
1331      * <font color="#808080">8</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;</font>
1332      * <font color="#3f5fbf">&#42;&#47;</font><br />
1333      * <font color="#808080">9</font>&nbsp;<font color="#7f0055"><b>public&nbsp;</b></font>
1334      * <font color="#7f0055"><b>void&nbsp;</b></font><font color="#000000">dummyMethod</font>
1335      * <font color="#000000">(&nbsp;</font><font color="#000000">String&nbsp;s&nbsp;</font>
1336      * <font color="#000000">){}</font>
1337      * </code>
1338      *
1339      * @param stringWriter   not null
1340      * @param javaExecutable not null
1341      * @param indent         not null
1342      * @throws MojoExecutionException if any
1343      * @see #getDefaultMethodJavadocComment
1344      * @see #appendDefaultSinceTag(StringBuilder, String)
1345      */
1346     private void addDefaultMethodComment(
1347             final StringWriter stringWriter, final JavaExecutable javaExecutable, final String indent)
1348             throws MojoExecutionException {
1349         StringBuilder sb = new StringBuilder();
1350 
1351         // special case
1352         if (isInherited(javaExecutable)) {
1353             sb.append(indent).append(INHERITED_JAVADOC);
1354             sb.append(EOL);
1355 
1356             stringWriter.write(sb.toString());
1357             return;
1358         }
1359 
1360         sb.append(indent).append(START_JAVADOC);
1361         sb.append(EOL);
1362         sb.append(indent).append(SEPARATOR_JAVADOC);
1363         sb.append(getDefaultMethodJavadocComment(javaExecutable));
1364         sb.append(EOL);
1365 
1366         boolean separatorAdded = false;
1367         if (fixTag(PARAM_TAG)) {
1368             if (javaExecutable.getParameters() != null) {
1369                 for (JavaParameter javaParameter : javaExecutable.getParameters()) {
1370                     separatorAdded = appendDefaultParamTag(sb, indent, separatorAdded, javaParameter);
1371                 }
1372             }
1373             // is generic?
1374             if (javaExecutable.getTypeParameters() != null) {
1375                 for (JavaTypeVariable<JavaGenericDeclaration> typeParam : javaExecutable.getTypeParameters()) {
1376                     separatorAdded = appendDefaultParamTag(sb, indent, separatorAdded, typeParam);
1377                 }
1378             }
1379         }
1380         if (javaExecutable instanceof JavaMethod) {
1381             JavaMethod javaMethod = (JavaMethod) javaExecutable;
1382             if (fixTag(RETURN_TAG)
1383                     && javaMethod.getReturns() != null
1384                     && !javaMethod.getReturns().isVoid()) {
1385                 separatorAdded = appendDefaultReturnTag(sb, indent, separatorAdded, javaMethod);
1386             }
1387         }
1388         if (fixTag(THROWS_TAG) && javaExecutable.getExceptions() != null) {
1389             for (JavaType exception : javaExecutable.getExceptions()) {
1390                 separatorAdded = appendDefaultThrowsTag(sb, indent, separatorAdded, exception);
1391             }
1392         }
1393         if (fixTag(SINCE_TAG) && isNewMethodFromLastRevision(javaExecutable)) {
1394             separatorAdded = appendDefaultSinceTag(sb, indent, separatorAdded);
1395         }
1396 
1397         sb.append(indent).append(" ").append(END_JAVADOC);
1398         sb.append(EOL);
1399 
1400         stringWriter.write(sb.toString());
1401     }
1402 
1403     /**
1404      * @param stringWriter    not null
1405      * @param originalContent not null
1406      * @param entity          not null
1407      * @param indent          not null
1408      * @return the updated changeDetected flag
1409      * @throws MojoExecutionException if any
1410      * @throws IOException            if any
1411      */
1412     private boolean updateEntityComment(
1413             final StringWriter stringWriter,
1414             final String originalContent,
1415             final JavaAnnotatedElement entity,
1416             final String indent)
1417             throws MojoExecutionException, IOException {
1418         boolean changeDetected = false;
1419 
1420         String old = null;
1421         String s = stringWriter.toString();
1422         int i = s.lastIndexOf(START_JAVADOC);
1423         if (i != -1) {
1424             String tmp = s.substring(0, i);
1425             if (tmp.lastIndexOf(EOL) != -1) {
1426                 tmp = tmp.substring(0, tmp.lastIndexOf(EOL));
1427             }
1428 
1429             old = stringWriter.getBuffer().substring(i);
1430 
1431             stringWriter.getBuffer().delete(0, stringWriter.getBuffer().length());
1432             stringWriter.write(tmp);
1433             stringWriter.write(EOL);
1434         } else {
1435             changeDetected = true;
1436         }
1437 
1438         updateJavadocComment(stringWriter, originalContent, entity, indent);
1439 
1440         if (changeDetected) {
1441             return true; // return now if we already know there's a change
1442         }
1443 
1444         return !stringWriter.getBuffer().substring(i).equals(old);
1445     }
1446 
1447     /**
1448      * @param stringWriter    not null
1449      * @param originalContent not null
1450      * @param entity          not null
1451      * @param indent          not null
1452      * @throws MojoExecutionException if any
1453      * @throws IOException            if any
1454      */
1455     private void updateJavadocComment(
1456             final StringWriter stringWriter,
1457             final String originalContent,
1458             final JavaAnnotatedElement entity,
1459             final String indent)
1460             throws MojoExecutionException, IOException {
1461         if (entity.getComment() == null
1462                 && (entity.getTags() == null || entity.getTags().isEmpty())) {
1463             return;
1464         }
1465 
1466         boolean isJavaExecutable = entity instanceof JavaExecutable;
1467 
1468         StringBuilder sb = new StringBuilder();
1469 
1470         // special case for inherited method
1471         if (isJavaExecutable) {
1472             JavaExecutable javaMethod = (JavaExecutable) entity;
1473 
1474             if (isInherited(javaMethod)) {
1475                 // QDOX-154 could be empty
1476                 if (StringUtils.isEmpty(javaMethod.getComment())) {
1477                     sb.append(indent).append(INHERITED_JAVADOC);
1478                     sb.append(EOL);
1479                     stringWriter.write(sb.toString());
1480                     return;
1481                 }
1482 
1483                 String javadoc = getJavadocComment(originalContent, javaMethod);
1484 
1485                 // case: /** {@inheritDoc} */ or no tags
1486                 if (hasInheritedTag(javadoc)
1487                         && (javaMethod.getTags() == null || javaMethod.getTags().isEmpty())) {
1488                     sb.append(indent).append(INHERITED_JAVADOC);
1489                     sb.append(EOL);
1490                     stringWriter.write(sb.toString());
1491                     return;
1492                 }
1493 
1494                 if (javadoc.contains(START_JAVADOC)) {
1495                     javadoc = javadoc.substring(javadoc.indexOf(START_JAVADOC) + START_JAVADOC.length());
1496                 }
1497                 if (javadoc.contains(END_JAVADOC)) {
1498                     javadoc = javadoc.substring(0, javadoc.indexOf(END_JAVADOC));
1499                 }
1500 
1501                 sb.append(indent).append(START_JAVADOC);
1502                 sb.append(EOL);
1503                 if (!javadoc.contains(INHERITED_TAG)) {
1504                     sb.append(indent).append(SEPARATOR_JAVADOC).append(INHERITED_TAG);
1505                     sb.append(EOL);
1506                     appendSeparator(sb, indent);
1507                 }
1508                 javadoc = removeLastEmptyJavadocLines(javadoc);
1509                 javadoc = alignIndentationJavadocLines(javadoc, indent);
1510                 sb.append(javadoc);
1511                 sb.append(EOL);
1512                 if (javaMethod.getTags() != null) {
1513                     for (DocletTag docletTag : javaMethod.getTags()) {
1514                         // Voluntary ignore these tags
1515                         if (JavadocUtil.equals(docletTag.getName(), PARAM_TAG, RETURN_TAG, THROWS_TAG)) {
1516                             continue;
1517                         }
1518 
1519                         String s = getJavadocComment(originalContent, entity, docletTag);
1520                         s = removeLastEmptyJavadocLines(s);
1521                         s = alignIndentationJavadocLines(s, indent);
1522                         sb.append(s);
1523                         sb.append(EOL);
1524                     }
1525                 }
1526                 sb.append(indent).append(" ").append(END_JAVADOC);
1527                 sb.append(EOL);
1528 
1529                 if (hasInheritedTag(sb.toString().trim())) {
1530                     sb = new StringBuilder();
1531                     sb.append(indent).append(INHERITED_JAVADOC);
1532                     sb.append(EOL);
1533                     stringWriter.write(sb.toString());
1534                     return;
1535                 }
1536 
1537                 stringWriter.write(sb.toString());
1538                 return;
1539             }
1540         }
1541 
1542         sb.append(indent).append(START_JAVADOC);
1543         sb.append(EOL);
1544 
1545         // comment
1546         if (StringUtils.isNotEmpty(entity.getComment())) {
1547             updateJavadocComment(sb, originalContent, entity, indent);
1548         } else {
1549             addDefaultJavadocComment(sb, entity, indent, isJavaExecutable);
1550         }
1551 
1552         // tags
1553         updateJavadocTags(sb, originalContent, entity, indent, isJavaExecutable);
1554 
1555         sb = new StringBuilder(removeLastEmptyJavadocLines(sb.toString())).append(EOL);
1556 
1557         sb.append(indent).append(" ").append(END_JAVADOC);
1558         sb.append(EOL);
1559 
1560         stringWriter.write(sb.toString());
1561     }
1562 
1563     /**
1564      * @param sb              not null
1565      * @param originalContent not null
1566      * @param entity          not null
1567      * @param indent          not null
1568      * @throws IOException if any
1569      */
1570     private void updateJavadocComment(
1571             final StringBuilder sb,
1572             final String originalContent,
1573             final JavaAnnotatedElement entity,
1574             final String indent)
1575             throws IOException {
1576         String comment = getJavadocComment(originalContent, entity);
1577         comment = removeLastEmptyJavadocLines(comment);
1578         comment = alignIndentationJavadocLines(comment, indent);
1579 
1580         if (comment.contains(START_JAVADOC)) {
1581             comment = comment.substring(comment.indexOf(START_JAVADOC) + START_JAVADOC.length());
1582             comment = indent + SEPARATOR_JAVADOC + comment.trim();
1583         }
1584         if (comment.contains(END_JAVADOC)) {
1585             comment = comment.substring(0, comment.indexOf(END_JAVADOC));
1586         }
1587 
1588         if (fixTag(LINK_TAG)) {
1589             comment = replaceLinkTags(comment, entity);
1590         }
1591 
1592         String[] lines = getLines(comment);
1593         for (String line : lines) {
1594             sb.append(indent).append(" ").append(line.trim());
1595             sb.append(EOL);
1596         }
1597     }
1598 
1599     private static final Pattern REPLACE_LINK_TAGS_PATTERN = Pattern.compile("\\{@link\\s");
1600 
1601     static String replaceLinkTags(String comment, JavaAnnotatedElement entity) {
1602         StringBuilder resolvedComment = new StringBuilder();
1603         // scan comment for {@link someClassName} and try to resolve this
1604         Matcher linktagMatcher = REPLACE_LINK_TAGS_PATTERN.matcher(comment);
1605         int startIndex = 0;
1606         while (linktagMatcher.find()) {
1607             int startName = linktagMatcher.end();
1608             resolvedComment.append(comment, startIndex, startName);
1609             int endName = comment.indexOf("}", startName);
1610             if (endName >= 0) {
1611                 String name;
1612                 String link = comment.substring(startName, endName);
1613                 int hashIndex = link.indexOf('#');
1614                 if (hashIndex >= 0) {
1615                     name = link.substring(0, hashIndex);
1616                 } else {
1617                     name = link;
1618                 }
1619                 if (StringUtils.isNotBlank(name)) {
1620                     String typeName;
1621                     if (entity instanceof JavaClass) {
1622                         JavaClass clazz = (JavaClass) entity;
1623                         typeName = TypeResolver.byClassName(
1624                                         clazz.getBinaryName(),
1625                                         clazz.getJavaClassLibrary(),
1626                                         clazz.getSource().getImports())
1627                                 .resolveType(name.trim());
1628                     } else if (entity instanceof JavaMember) {
1629                         JavaClass clazz = ((JavaMember) entity).getDeclaringClass();
1630                         typeName = TypeResolver.byClassName(
1631                                         clazz.getBinaryName(),
1632                                         clazz.getJavaClassLibrary(),
1633                                         clazz.getSource().getImports())
1634                                 .resolveType(name.trim());
1635                     } else {
1636                         typeName = null;
1637                     }
1638 
1639                     if (typeName == null) {
1640                         typeName = name.trim();
1641                     } else {
1642                         typeName = typeName.replaceAll("\\$", ".");
1643                     }
1644                     // adjust name for inner classes
1645                     resolvedComment.append(typeName);
1646                 }
1647                 if (hashIndex >= 0) {
1648                     resolvedComment.append(link.substring(hashIndex).trim());
1649                 }
1650                 startIndex = endName;
1651             } else {
1652                 startIndex = startName;
1653             }
1654         }
1655         resolvedComment.append(comment.substring(startIndex));
1656         return resolvedComment.toString();
1657     }
1658 
1659     /**
1660      * @param sb           not null
1661      * @param entity       not null
1662      * @param indent       not null
1663      * @param isJavaExecutable
1664      */
1665     private void addDefaultJavadocComment(
1666             final StringBuilder sb,
1667             final JavaAnnotatedElement entity,
1668             final String indent,
1669             final boolean isJavaExecutable) {
1670         sb.append(indent).append(SEPARATOR_JAVADOC);
1671         if (isJavaExecutable) {
1672             sb.append(getDefaultMethodJavadocComment((JavaExecutable) entity));
1673         } else {
1674             sb.append(getDefaultClassJavadocComment((JavaClass) entity));
1675         }
1676         sb.append(EOL);
1677     }
1678 
1679     /**
1680      * @param sb              not null
1681      * @param originalContent not null
1682      * @param entity          not null
1683      * @param indent          not null
1684      * @param isJavaExecutable
1685      * @throws IOException            if any
1686      * @throws MojoExecutionException if any
1687      */
1688     private void updateJavadocTags(
1689             final StringBuilder sb,
1690             final String originalContent,
1691             final JavaAnnotatedElement entity,
1692             final String indent,
1693             final boolean isJavaExecutable)
1694             throws IOException, MojoExecutionException {
1695         appendSeparator(sb, indent);
1696 
1697         // parse tags
1698         JavaEntityTags javaEntityTags = parseJavadocTags(originalContent, entity, indent, isJavaExecutable);
1699 
1700         // update and write tags
1701         updateJavadocTags(sb, entity, isJavaExecutable, javaEntityTags);
1702 
1703         // add missing tags...
1704         addMissingJavadocTags(sb, entity, indent, isJavaExecutable, javaEntityTags);
1705     }
1706 
1707     /**
1708      * Parse entity tags
1709      *
1710      * @param originalContent not null
1711      * @param entity          not null
1712      * @param indent          not null
1713      * @param isJavaMethod
1714      * @return an instance of {@link JavaEntityTags}
1715      * @throws IOException if any
1716      */
1717     JavaEntityTags parseJavadocTags(
1718             final String originalContent,
1719             final JavaAnnotatedElement entity,
1720             final String indent,
1721             final boolean isJavaMethod)
1722             throws IOException {
1723         JavaEntityTags javaEntityTags = new JavaEntityTags(entity, isJavaMethod);
1724         for (DocletTag docletTag : entity.getTags()) {
1725             String originalJavadocTag = getJavadocComment(originalContent, entity, docletTag);
1726             originalJavadocTag = removeLastEmptyJavadocLines(originalJavadocTag);
1727             originalJavadocTag = alignIndentationJavadocLines(originalJavadocTag, indent);
1728 
1729             javaEntityTags.getNamesTags().add(docletTag.getName());
1730 
1731             if (isJavaMethod) {
1732                 List<String> params = docletTag.getParameters();
1733                 if (params.size() < 1) {
1734                     continue;
1735                 }
1736 
1737                 String paramName = params.get(0);
1738                 switch (docletTag.getName()) {
1739                     case PARAM_TAG:
1740                         javaEntityTags.putJavadocParamTag(paramName, docletTag.getValue(), originalJavadocTag);
1741                         break;
1742                     case RETURN_TAG:
1743                         javaEntityTags.setJavadocReturnTag(originalJavadocTag);
1744                         break;
1745                     case THROWS_TAG:
1746                         javaEntityTags.putJavadocThrowsTag(paramName, originalJavadocTag);
1747                         break;
1748                     default:
1749                         javaEntityTags.getUnknownTags().add(originalJavadocTag);
1750                         break;
1751                 }
1752             } else {
1753                 javaEntityTags.getUnknownTags().add(originalJavadocTag);
1754             }
1755         }
1756 
1757         return javaEntityTags;
1758     }
1759 
1760     /**
1761      * Write tags according javaEntityTags.
1762      *
1763      * @param sb             not null
1764      * @param entity         not null
1765      * @param isJavaExecutable
1766      * @param javaEntityTags not null
1767      */
1768     private void updateJavadocTags(
1769             final StringBuilder sb,
1770             final JavaAnnotatedElement entity,
1771             final boolean isJavaExecutable,
1772             final JavaEntityTags javaEntityTags) {
1773         for (DocletTag docletTag : entity.getTags()) {
1774             if (isJavaExecutable) {
1775                 JavaExecutable javaExecutable = (JavaExecutable) entity;
1776 
1777                 List<String> params = docletTag.getParameters();
1778                 if (params.size() < 1) {
1779                     continue;
1780                 }
1781 
1782                 if (docletTag.getName().equals(PARAM_TAG)) {
1783                     writeParamTag(sb, javaExecutable, javaEntityTags, params.get(0), docletTag.getValue());
1784                 } else if (docletTag.getName().equals(RETURN_TAG) && javaExecutable instanceof JavaMethod) {
1785                     writeReturnTag(sb, (JavaMethod) javaExecutable, javaEntityTags);
1786                 } else if (docletTag.getName().equals(THROWS_TAG)) {
1787                     writeThrowsTag(sb, javaExecutable, javaEntityTags, params);
1788                 } else {
1789                     // write unknown tags
1790                     for (Iterator<String> it = javaEntityTags.getUnknownTags().iterator(); it.hasNext(); ) {
1791                         String originalJavadocTag = it.next();
1792                         String simplified = StringUtils.removeDuplicateWhitespace(originalJavadocTag)
1793                                 .trim();
1794 
1795                         if (simplified.contains("@" + docletTag.getName())) {
1796                             it.remove();
1797                             sb.append(originalJavadocTag);
1798                             sb.append(EOL);
1799                         }
1800                     }
1801                 }
1802             } else {
1803                 for (Iterator<String> it = javaEntityTags.getUnknownTags().iterator(); it.hasNext(); ) {
1804                     String originalJavadocTag = it.next();
1805                     String simplified = StringUtils.removeDuplicateWhitespace(originalJavadocTag)
1806                             .trim();
1807 
1808                     if (simplified.contains("@" + docletTag.getName())) {
1809                         it.remove();
1810                         sb.append(originalJavadocTag);
1811                         sb.append(EOL);
1812                     }
1813                 }
1814             }
1815 
1816             if (sb.toString().endsWith(EOL)) {
1817                 sb.delete(sb.toString().lastIndexOf(EOL), sb.toString().length());
1818             }
1819 
1820             sb.append(EOL);
1821         }
1822     }
1823 
1824     private void writeParamTag(
1825             final StringBuilder sb,
1826             final JavaExecutable javaExecutable,
1827             final JavaEntityTags javaEntityTags,
1828             String paramName,
1829             String paramValue) {
1830         if (!fixTag(PARAM_TAG)) {
1831             // write original param tag if found
1832             String originalJavadocTag = javaEntityTags.getJavadocParamTag(paramValue);
1833             if (originalJavadocTag != null) {
1834                 sb.append(originalJavadocTag);
1835             }
1836             return;
1837         }
1838 
1839         boolean found = false;
1840         JavaParameter javaParam = javaExecutable.getParameterByName(paramName);
1841         if (javaParam == null) {
1842             // is generic?
1843             List<JavaTypeVariable<JavaGenericDeclaration>> typeParams = javaExecutable.getTypeParameters();
1844             for (JavaTypeVariable<JavaGenericDeclaration> typeParam : typeParams) {
1845                 if (("<" + typeParam.getName() + ">").equals(paramName)) {
1846                     found = true;
1847                 }
1848             }
1849         } else {
1850             found = true;
1851         }
1852 
1853         if (!found) {
1854             if (getLog().isWarnEnabled()) {
1855                 getLog().warn("Fixed unknown param '" + paramName + "' defined in "
1856                         + getJavaMethodAsString(javaExecutable));
1857             }
1858 
1859             if (sb.toString().endsWith(EOL)) {
1860                 sb.delete(sb.toString().lastIndexOf(EOL), sb.toString().length());
1861             }
1862         } else {
1863             String originalJavadocTag = javaEntityTags.getJavadocParamTag(paramValue);
1864             if (originalJavadocTag != null) {
1865                 sb.append(originalJavadocTag);
1866                 String s = "@" + PARAM_TAG + " " + paramName;
1867                 if (StringUtils.removeDuplicateWhitespace(originalJavadocTag)
1868                         .trim()
1869                         .endsWith(s)) {
1870                     sb.append(" ");
1871                     sb.append(getDefaultJavadocForType(javaParam.getJavaClass()));
1872                 }
1873             }
1874         }
1875     }
1876 
1877     private void writeReturnTag(
1878             final StringBuilder sb, final JavaMethod javaMethod, final JavaEntityTags javaEntityTags) {
1879         String originalJavadocTag = javaEntityTags.getJavadocReturnTag();
1880         if (originalJavadocTag == null) {
1881             return;
1882         }
1883 
1884         if (!fixTag(RETURN_TAG)) {
1885             // write original param tag if found
1886             sb.append(originalJavadocTag);
1887             return;
1888         }
1889 
1890         if (StringUtils.isNotEmpty(originalJavadocTag)
1891                 && javaMethod.getReturns() != null
1892                 && !javaMethod.getReturns().isVoid()) {
1893             sb.append(originalJavadocTag);
1894             if (originalJavadocTag.trim().endsWith("@" + RETURN_TAG)) {
1895                 sb.append(" ");
1896                 sb.append(getDefaultJavadocForType(javaMethod.getReturns()));
1897             }
1898         }
1899     }
1900 
1901     void writeThrowsTag(
1902             final StringBuilder sb,
1903             final JavaExecutable javaExecutable,
1904             final JavaEntityTags javaEntityTags,
1905             final List<String> params) {
1906         String exceptionClassName = params.get(0);
1907 
1908         String originalJavadocTag = javaEntityTags.getJavadocThrowsTag(exceptionClassName);
1909         if (originalJavadocTag == null) {
1910             return;
1911         }
1912 
1913         if (!fixTag(THROWS_TAG)) {
1914             // write original param tag if found
1915             sb.append(originalJavadocTag);
1916             return;
1917         }
1918 
1919         if (javaExecutable.getExceptions() != null) {
1920             for (JavaType exception : javaExecutable.getExceptions()) {
1921                 if (exception.getFullyQualifiedName().endsWith(exceptionClassName)) {
1922                     originalJavadocTag = StringUtils.replace(
1923                             originalJavadocTag, exceptionClassName, exception.getFullyQualifiedName());
1924                     if (StringUtils.removeDuplicateWhitespace(originalJavadocTag)
1925                             .trim()
1926                             .endsWith("@" + THROWS_TAG + " " + exception.getValue())) {
1927                         originalJavadocTag += " if any.";
1928                     }
1929 
1930                     sb.append(originalJavadocTag);
1931 
1932                     // added qualified name
1933                     javaEntityTags.putJavadocThrowsTag(exception.getValue(), originalJavadocTag);
1934 
1935                     return;
1936                 }
1937             }
1938         }
1939 
1940         Class<?> clazz = getClass(javaExecutable.getDeclaringClass(), exceptionClassName);
1941 
1942         if (clazz != null) {
1943             if (RuntimeException.class.isAssignableFrom(clazz)) {
1944                 sb.append(StringUtils.replace(originalJavadocTag, exceptionClassName, clazz.getName()));
1945 
1946                 // added qualified name
1947                 javaEntityTags.putJavadocThrowsTag(clazz.getName(), originalJavadocTag);
1948             } else if (Throwable.class.isAssignableFrom(clazz)) {
1949                 getLog().debug("Removing '" + originalJavadocTag + "'; Throwable not specified by "
1950                         + getJavaMethodAsString(javaExecutable) + " and it is not a RuntimeException.");
1951             } else {
1952                 getLog().debug("Removing '" + originalJavadocTag + "'; It is not a Throwable");
1953             }
1954         } else if (removeUnknownThrows) {
1955             getLog().warn("Ignoring unknown throws '" + exceptionClassName + "' defined on "
1956                     + getJavaMethodAsString(javaExecutable));
1957         } else {
1958             getLog().warn("Found unknown throws '" + exceptionClassName + "' defined on "
1959                     + getJavaMethodAsString(javaExecutable));
1960 
1961             sb.append(originalJavadocTag);
1962 
1963             if (params.size() == 1) {
1964                 sb.append(" if any.");
1965             }
1966 
1967             javaEntityTags.putJavadocThrowsTag(exceptionClassName, originalJavadocTag);
1968         }
1969     }
1970 
1971     /**
1972      * Add missing tags not already written.
1973      *
1974      * @param sb             not null
1975      * @param entity         not null
1976      * @param indent         not null
1977      * @param isJavaExecutable
1978      * @param javaEntityTags not null
1979      * @throws MojoExecutionException if any
1980      */
1981     private void addMissingJavadocTags(
1982             final StringBuilder sb,
1983             final JavaAnnotatedElement entity,
1984             final String indent,
1985             final boolean isJavaExecutable,
1986             final JavaEntityTags javaEntityTags)
1987             throws MojoExecutionException {
1988         if (isJavaExecutable) {
1989             JavaExecutable javaExecutable = (JavaExecutable) entity;
1990 
1991             if (fixTag(PARAM_TAG)) {
1992                 if (javaExecutable.getParameters() != null) {
1993                     for (JavaParameter javaParameter : javaExecutable.getParameters()) {
1994                         if (!javaEntityTags.hasJavadocParamTag(javaParameter.getName())) {
1995                             appendDefaultParamTag(sb, indent, javaParameter);
1996                         }
1997                     }
1998                 }
1999                 // is generic?
2000                 if (javaExecutable.getTypeParameters() != null) {
2001                     for (JavaTypeVariable<JavaGenericDeclaration> typeParam : javaExecutable.getTypeParameters()) {
2002                         if (!javaEntityTags.hasJavadocParamTag("<" + typeParam.getName() + ">")) {
2003                             appendDefaultParamTag(sb, indent, typeParam);
2004                         }
2005                     }
2006                 }
2007             }
2008 
2009             if (javaExecutable instanceof JavaMethod) {
2010                 JavaMethod javaMethod = (JavaMethod) javaExecutable;
2011                 if (fixTag(RETURN_TAG)
2012                         && StringUtils.isEmpty(javaEntityTags.getJavadocReturnTag())
2013                         && javaMethod.getReturns() != null
2014                         && !javaMethod.getReturns().isVoid()) {
2015                     appendDefaultReturnTag(sb, indent, javaMethod);
2016                 }
2017             }
2018 
2019             if (fixTag(THROWS_TAG) && javaExecutable.getExceptions() != null) {
2020                 for (JavaType exception : javaExecutable.getExceptions()) {
2021                     if (javaEntityTags.getJavadocThrowsTag(exception.getValue(), true) == null) {
2022                         appendDefaultThrowsTag(sb, indent, exception);
2023                     }
2024                 }
2025             }
2026         } else {
2027             if (!javaEntityTags.getNamesTags().contains(AUTHOR_TAG)) {
2028                 appendDefaultAuthorTag(sb, indent);
2029             }
2030             if (!javaEntityTags.getNamesTags().contains(VERSION_TAG)) {
2031                 appendDefaultVersionTag(sb, indent);
2032             }
2033         }
2034 
2035         if (fixTag(SINCE_TAG) && !javaEntityTags.getNamesTags().contains(SINCE_TAG)) {
2036             if (!isJavaExecutable) {
2037                 if (!ignoreClirr) {
2038                     if (isNewClassFromLastVersion((JavaClass) entity)) {
2039                         appendDefaultSinceTag(sb, indent);
2040                     }
2041                 } else {
2042                     appendDefaultSinceTag(sb, indent);
2043                     addSinceClasses((JavaClass) entity);
2044                 }
2045             } else {
2046                 if (!ignoreClirr) {
2047                     if (isNewMethodFromLastRevision((JavaExecutable) entity)) {
2048                         appendDefaultSinceTag(sb, indent);
2049                     }
2050                 } else if (sinceClasses != null) {
2051                     if (entity instanceof JavaMember
2052                             && !sinceClassesContains(((JavaMember) entity).getDeclaringClass())) {
2053                         appendDefaultSinceTag(sb, indent);
2054                     } else if (entity instanceof JavaClass
2055                             && !sinceClassesContains(((JavaClass) entity).getDeclaringClass())) {
2056                         appendDefaultSinceTag(sb, indent);
2057                     }
2058                 }
2059             }
2060         }
2061     }
2062 
2063     /**
2064      * @param sb             not null
2065      * @param indent         not null
2066      * @param separatorAdded
2067      * @return true if separator has been added.
2068      */
2069     private boolean appendDefaultAuthorTag(final StringBuilder sb, final String indent, boolean separatorAdded) {
2070         if (!fixTag(AUTHOR_TAG)) {
2071             return separatorAdded;
2072         }
2073 
2074         if (!separatorAdded) {
2075             appendSeparator(sb, indent);
2076             separatorAdded = true;
2077         }
2078 
2079         appendDefaultAuthorTag(sb, indent);
2080         return separatorAdded;
2081     }
2082 
2083     /**
2084      * @param sb     not null
2085      * @param indent not null
2086      */
2087     private void appendDefaultAuthorTag(final StringBuilder sb, final String indent) {
2088         if (!fixTag(AUTHOR_TAG)) {
2089             return;
2090         }
2091 
2092         sb.append(indent).append(" * @").append(AUTHOR_TAG).append(" ");
2093         sb.append(defaultAuthor);
2094         sb.append(EOL);
2095     }
2096 
2097     /**
2098      * @param sb             not null
2099      * @param indent         not null
2100      * @param separatorAdded
2101      * @return true if separator has been added.
2102      */
2103     private boolean appendDefaultSinceTag(final StringBuilder sb, final String indent, boolean separatorAdded) {
2104         if (!fixTag(SINCE_TAG)) {
2105             return separatorAdded;
2106         }
2107 
2108         if (!separatorAdded) {
2109             appendSeparator(sb, indent);
2110             separatorAdded = true;
2111         }
2112 
2113         appendDefaultSinceTag(sb, indent);
2114         return separatorAdded;
2115     }
2116 
2117     /**
2118      * @param sb     not null
2119      * @param indent not null
2120      */
2121     private void appendDefaultSinceTag(final StringBuilder sb, final String indent) {
2122         if (!fixTag(SINCE_TAG)) {
2123             return;
2124         }
2125 
2126         sb.append(indent).append(" * @").append(SINCE_TAG).append(" ");
2127         sb.append(defaultSince);
2128         sb.append(EOL);
2129     }
2130 
2131     /**
2132      * @param sb             not null
2133      * @param indent         not null
2134      * @param separatorAdded
2135      * @return true if separator has been added.
2136      */
2137     private boolean appendDefaultVersionTag(final StringBuilder sb, final String indent, boolean separatorAdded) {
2138         if (!fixTag(VERSION_TAG)) {
2139             return separatorAdded;
2140         }
2141 
2142         if (!separatorAdded) {
2143             appendSeparator(sb, indent);
2144             separatorAdded = true;
2145         }
2146 
2147         appendDefaultVersionTag(sb, indent);
2148         return separatorAdded;
2149     }
2150 
2151     /**
2152      * @param sb     not null
2153      * @param indent not null
2154      */
2155     private void appendDefaultVersionTag(final StringBuilder sb, final String indent) {
2156         if (!fixTag(VERSION_TAG)) {
2157             return;
2158         }
2159 
2160         sb.append(indent).append(" * @").append(VERSION_TAG).append(" ");
2161         sb.append(defaultVersion);
2162         sb.append(EOL);
2163     }
2164 
2165     /**
2166      * @param sb             not null
2167      * @param indent         not null
2168      * @param separatorAdded
2169      * @param typeParam  not null
2170      * @return true if separator has been added.
2171      */
2172     private boolean appendDefaultParamTag(
2173             final StringBuilder sb, final String indent, boolean separatorAdded, final JavaParameter typeParam) {
2174         if (!fixTag(PARAM_TAG)) {
2175             return separatorAdded;
2176         }
2177 
2178         if (!separatorAdded) {
2179             appendSeparator(sb, indent);
2180             separatorAdded = true;
2181         }
2182 
2183         appendDefaultParamTag(sb, indent, typeParam);
2184         return separatorAdded;
2185     }
2186 
2187     /**
2188      * @param sb             not null
2189      * @param indent         not null
2190      * @param separatorAdded
2191      * @param typeParameter  not null
2192      * @return true if separator has been added.
2193      */
2194     private boolean appendDefaultParamTag(
2195             final StringBuilder sb,
2196             final String indent,
2197             boolean separatorAdded,
2198             final JavaTypeVariable<JavaGenericDeclaration> typeParameter) {
2199         if (!fixTag(PARAM_TAG)) {
2200             return separatorAdded;
2201         }
2202 
2203         if (!separatorAdded) {
2204             appendSeparator(sb, indent);
2205             separatorAdded = true;
2206         }
2207 
2208         appendDefaultParamTag(sb, indent, typeParameter);
2209         return separatorAdded;
2210     }
2211 
2212     /**
2213      * @param sb            not null
2214      * @param indent        not null
2215      * @param typeParam not null
2216      */
2217     private void appendDefaultParamTag(final StringBuilder sb, final String indent, final JavaParameter typeParam) {
2218         if (!fixTag(PARAM_TAG)) {
2219             return;
2220         }
2221 
2222         sb.append(indent).append(" * @").append(PARAM_TAG).append(" ");
2223         sb.append(typeParam.getName());
2224         sb.append(" ");
2225         sb.append(getDefaultJavadocForType(typeParam.getJavaClass()));
2226         sb.append(EOL);
2227     }
2228 
2229     /**
2230      * @param sb            not null
2231      * @param indent        not null
2232      * @param typeParameter not null
2233      */
2234     private void appendDefaultParamTag(
2235             final StringBuilder sb, final String indent, final JavaTypeVariable<JavaGenericDeclaration> typeParameter) {
2236         if (!fixTag(PARAM_TAG)) {
2237             return;
2238         }
2239 
2240         sb.append(indent).append(" * @").append(PARAM_TAG).append(" ");
2241         sb.append("<").append(typeParameter.getName()).append(">");
2242         sb.append(" ");
2243         sb.append(getDefaultJavadocForType(typeParameter));
2244         sb.append(EOL);
2245     }
2246 
2247     /**
2248      * @param sb             not null
2249      * @param indent         not null
2250      * @param separatorAdded
2251      * @param javaMethod     not null
2252      * @return true if separator has been added.
2253      */
2254     private boolean appendDefaultReturnTag(
2255             final StringBuilder sb, final String indent, boolean separatorAdded, final JavaMethod javaMethod) {
2256         if (!fixTag(RETURN_TAG)) {
2257             return separatorAdded;
2258         }
2259 
2260         if (!separatorAdded) {
2261             appendSeparator(sb, indent);
2262             separatorAdded = true;
2263         }
2264 
2265         appendDefaultReturnTag(sb, indent, javaMethod);
2266         return separatorAdded;
2267     }
2268 
2269     /**
2270      * @param sb         not null
2271      * @param indent     not null
2272      * @param javaMethod not null
2273      */
2274     private void appendDefaultReturnTag(final StringBuilder sb, final String indent, final JavaMethod javaMethod) {
2275         if (!fixTag(RETURN_TAG)) {
2276             return;
2277         }
2278 
2279         sb.append(indent).append(" * @").append(RETURN_TAG).append(" ");
2280         sb.append(getDefaultJavadocForType(javaMethod.getReturns()));
2281         sb.append(EOL);
2282     }
2283 
2284     /**
2285      * @param sb             not null
2286      * @param indent         not null
2287      * @param separatorAdded
2288      * @param exception      not null
2289      * @return true if separator has been added.
2290      */
2291     private boolean appendDefaultThrowsTag(
2292             final StringBuilder sb, final String indent, boolean separatorAdded, final JavaType exception) {
2293         if (!fixTag(THROWS_TAG)) {
2294             return separatorAdded;
2295         }
2296 
2297         if (!separatorAdded) {
2298             appendSeparator(sb, indent);
2299             separatorAdded = true;
2300         }
2301 
2302         appendDefaultThrowsTag(sb, indent, exception);
2303         return separatorAdded;
2304     }
2305 
2306     /**
2307      * @param sb        not null
2308      * @param indent    not null
2309      * @param exception not null
2310      */
2311     private void appendDefaultThrowsTag(final StringBuilder sb, final String indent, final JavaType exception) {
2312         if (!fixTag(THROWS_TAG)) {
2313             return;
2314         }
2315 
2316         sb.append(indent).append(" * @").append(THROWS_TAG).append(" ");
2317         sb.append(exception.getFullyQualifiedName());
2318         sb.append(" if any.");
2319         sb.append(EOL);
2320     }
2321 
2322     /**
2323      * @param sb     not null
2324      * @param indent not null
2325      */
2326     private void appendSeparator(final StringBuilder sb, final String indent) {
2327         sb.append(indent).append(" *");
2328         sb.append(EOL);
2329     }
2330 
2331     /**
2332      * Verify if a method has <code>&#64;java.lang.Override()</code> annotation or if it is an inherited method
2333      * from an interface or a super class. The goal is to handle <code>&#123;&#64;inheritDoc&#125;</code> tag.
2334      *
2335      * @param javaMethod not null
2336      * @return <code>true</code> if the method is inherited, <code>false</code> otherwise.
2337      * @throws MojoExecutionException if any
2338      */
2339     private boolean isInherited(JavaExecutable javaMethod) throws MojoExecutionException {
2340         if (javaMethod.getAnnotations() != null) {
2341             for (JavaAnnotation annotation : javaMethod.getAnnotations()) {
2342                 if (annotation.toString().equals("@java.lang.Override()")) {
2343                     return true;
2344                 }
2345             }
2346         }
2347 
2348         Class<?> clazz = getClass(javaMethod.getDeclaringClass().getFullyQualifiedName());
2349 
2350         List<Class<?>> interfaces = ClassUtils.getAllInterfaces(clazz);
2351         for (Class<?> intface : interfaces) {
2352             if (isInherited(intface, javaMethod)) {
2353                 return true;
2354             }
2355         }
2356 
2357         List<Class<?>> classes = ClassUtils.getAllSuperclasses(clazz);
2358         for (Class<?> superClass : classes) {
2359             if (isInherited(superClass, javaMethod)) {
2360                 return true;
2361             }
2362         }
2363 
2364         return false;
2365     }
2366 
2367     /**
2368      * @param clazz      the Java class object, not null
2369      * @param javaMethod the QDox JavaMethod object not null
2370      * @return <code>true</code> if <code>javaMethod</code> exists in the given <code>clazz</code>,
2371      *         <code>false</code> otherwise.
2372      * @see #isInherited(JavaExecutable)
2373      */
2374     private boolean isInherited(Class<?> clazz, JavaExecutable javaMethod) {
2375         for (Method method : clazz.getDeclaredMethods()) {
2376             if (!method.getName().equals(javaMethod.getName())) {
2377                 continue;
2378             }
2379 
2380             if (method.getParameterTypes().length != javaMethod.getParameters().size()) {
2381                 continue;
2382             }
2383 
2384             boolean found = false;
2385             int j = 0;
2386             for (Class<?> paramType : method.getParameterTypes()) {
2387                 String name1 = paramType.getName();
2388                 String name2 = javaMethod.getParameters().get(j++).getType().getFullyQualifiedName();
2389                 found = name1.equals(name2); // TODO check algo, seems broken (only takes in account the last param)
2390             }
2391 
2392             return found;
2393         }
2394 
2395         return false;
2396     }
2397 
2398     /**
2399      * @param clazz
2400      * @return
2401      */
2402     private String getDefaultJavadocForType(JavaClass clazz) {
2403         StringBuilder sb = new StringBuilder();
2404 
2405         if (!JavaTypeVariable.class.isAssignableFrom(clazz.getClass()) && clazz.isPrimitive()) {
2406             if (clazz.isArray()) {
2407                 sb.append("an array of ").append(clazz.getComponentType().getCanonicalName());
2408             } else {
2409                 sb.append("a ").append(clazz.getCanonicalName());
2410             }
2411             return sb.toString();
2412         }
2413 
2414         StringBuilder javadocLink = new StringBuilder();
2415         try {
2416             getClass(clazz.getCanonicalName());
2417 
2418             javadocLink.append("{@link ");
2419 
2420             if (clazz.isArray()) {
2421                 javadocLink.append(clazz.getComponentType().getCanonicalName());
2422             } else {
2423                 javadocLink.append(clazz.getCanonicalName());
2424             }
2425             javadocLink.append("}");
2426         } catch (Exception e) {
2427             javadocLink.append(clazz.getValue());
2428         }
2429 
2430         if (clazz.isArray()) {
2431             sb.append("an array of ").append(javadocLink).append(" objects");
2432         } else {
2433             sb.append("a ").append(javadocLink).append(" object");
2434         }
2435 
2436         return sb.toString();
2437     }
2438 
2439     private String getDefaultJavadocForType(JavaTypeVariable<JavaGenericDeclaration> typeParameter) {
2440         return "a " + typeParameter.getName() + " class";
2441     }
2442 
2443     /**
2444      * Check under Clirr if this given class is newer from the last version.
2445      *
2446      * @param javaClass a given class not null
2447      * @return <code>true</code> if Clirr said that this class is added from the last version,
2448      *         <code>false</code> otherwise or if {@link #clirrNewClasses} is null.
2449      */
2450     private boolean isNewClassFromLastVersion(JavaClass javaClass) {
2451         return (clirrNewClasses != null) && clirrNewClasses.contains(javaClass.getFullyQualifiedName());
2452     }
2453 
2454     /**
2455      * Check under Clirr if this given method is newer from the last version.
2456      *
2457      * @param javaExecutable a given method not null
2458      * @return <code>true</code> if Clirr said that this method is added from the last version,
2459      *         <code>false</code> otherwise or if {@link #clirrNewMethods} is null.
2460      * @throws MojoExecutionException if any
2461      */
2462     private boolean isNewMethodFromLastRevision(JavaExecutable javaExecutable) throws MojoExecutionException {
2463         if (clirrNewMethods == null) {
2464             return false;
2465         }
2466 
2467         List<String> clirrMethods =
2468                 clirrNewMethods.get(javaExecutable.getDeclaringClass().getFullyQualifiedName());
2469         if (clirrMethods == null) {
2470             return false;
2471         }
2472 
2473         for (String clirrMethod : clirrMethods) {
2474             // see net.sf.clirr.core.internal.checks.MethodSetCheck#getMethodId(JavaType clazz, Method method)
2475             String retrn = "";
2476             if (javaExecutable instanceof JavaMethod && ((JavaMethod) javaExecutable).getReturns() != null) {
2477                 retrn = ((JavaMethod) javaExecutable).getReturns().getFullyQualifiedName();
2478             }
2479             StringBuilder params = new StringBuilder();
2480             for (JavaParameter parameter : javaExecutable.getParameters()) {
2481                 if (params.length() > 0) {
2482                     params.append(", ");
2483                 }
2484                 params.append(parameter.getResolvedFullyQualifiedName());
2485             }
2486             if (clirrMethod.contains(retrn + " ")
2487                     && clirrMethod.contains(javaExecutable.getName() + "(")
2488                     && clirrMethod.contains("(" + params.toString() + ")")) {
2489                 return true;
2490             }
2491         }
2492 
2493         return false;
2494     }
2495 
2496     /**
2497      * @param className not null
2498      * @return the Class corresponding to the given class name using the project classloader.
2499      * @throws MojoExecutionException if class not found
2500      * @see ClassUtils#getClass(ClassLoader, String, boolean)
2501      * @see #getProjectClassLoader()
2502      */
2503     private Class<?> getClass(String className) throws MojoExecutionException {
2504         try {
2505             return ClassUtils.getClass(getProjectClassLoader(), className, false);
2506         } catch (ClassNotFoundException e) {
2507             throw new MojoExecutionException("ClassNotFoundException: " + e.getMessage(), e);
2508         }
2509     }
2510 
2511     /**
2512      * Returns the Class object assignable for {@link RuntimeException} class and associated with the given
2513      * exception class name.
2514      *
2515      * @param currentClass       not null
2516      * @param exceptionClassName not null, an exception class name defined as:
2517      *                           <ul>
2518      *                           <li>exception class fully qualified</li>
2519      *                           <li>exception class in the same package</li>
2520      *                           <li>exception inner class</li>
2521      *                           <li>exception class in java.lang package</li>
2522      *                           </ul>
2523      * @return the class if found, otherwise {@code null}.
2524      * @see #getClass(String)
2525      */
2526     private Class<?> getClass(JavaClass currentClass, String exceptionClassName) {
2527         String[] potentialClassNames = new String[] {
2528             exceptionClassName,
2529             currentClass.getPackage().getName() + "." + exceptionClassName,
2530             currentClass.getPackage().getName() + "." + currentClass.getName() + "$" + exceptionClassName,
2531             "java.lang." + exceptionClassName
2532         };
2533 
2534         Class<?> clazz = null;
2535         for (String potentialClassName : potentialClassNames) {
2536             try {
2537                 clazz = getClass(potentialClassName);
2538             } catch (MojoExecutionException e) {
2539                 // nop
2540             }
2541             if (clazz != null) {
2542                 return clazz;
2543             }
2544         }
2545 
2546         return null;
2547     }
2548 
2549     /**
2550      * @param javaClass not null
2551      */
2552     private void addSinceClasses(JavaClass javaClass) {
2553         if (sinceClasses == null) {
2554             sinceClasses = new ArrayList<>();
2555         }
2556         sinceClasses.add(javaClass.getFullyQualifiedName());
2557     }
2558 
2559     private boolean sinceClassesContains(JavaClass javaClass) {
2560         return sinceClasses.contains(javaClass.getFullyQualifiedName());
2561     }
2562 
2563     // ----------------------------------------------------------------------
2564     // Static methods
2565     // ----------------------------------------------------------------------
2566 
2567     /**
2568      * Write content into the given javaFile and using the given encoding.
2569      * All line separators will be unified.
2570      *
2571      * @param javaFile not null
2572      * @param encoding not null
2573      * @param content  not null
2574      * @throws IOException if any
2575      */
2576     private static void writeFile(final File javaFile, final String encoding, final String content) throws IOException {
2577         String unified = StringUtils.unifyLineSeparators(content);
2578         FileUtils.fileWrite(javaFile, encoding, unified);
2579     }
2580 
2581     /**
2582      * @return the full clirr goal, i.e. <code>groupId:artifactId:version:goal</code>. The clirr-plugin version
2583      *         could be load from the pom.properties in the clirr-maven-plugin dependency.
2584      */
2585     private static String getFullClirrGoal() {
2586         StringBuilder sb = new StringBuilder();
2587 
2588         sb.append(CLIRR_MAVEN_PLUGIN_GROUPID)
2589                 .append(":")
2590                 .append(CLIRR_MAVEN_PLUGIN_ARTIFACTID)
2591                 .append(":");
2592 
2593         String clirrVersion = CLIRR_MAVEN_PLUGIN_VERSION;
2594 
2595         String resource = "META-INF/maven/" + CLIRR_MAVEN_PLUGIN_GROUPID + "/" + CLIRR_MAVEN_PLUGIN_ARTIFACTID
2596                 + "/pom.properties";
2597 
2598         try (InputStream resourceAsStream =
2599                 AbstractFixJavadocMojo.class.getClassLoader().getResourceAsStream(resource)) {
2600 
2601             if (resourceAsStream != null) {
2602                 Properties properties = new Properties();
2603                 properties.load(resourceAsStream);
2604                 if (StringUtils.isNotEmpty(properties.getProperty("version"))) {
2605                     clirrVersion = properties.getProperty("version");
2606                 }
2607             }
2608         } catch (IOException e) {
2609             // nop
2610         }
2611 
2612         sb.append(clirrVersion).append(":").append(CLIRR_MAVEN_PLUGIN_GOAL);
2613 
2614         return sb.toString();
2615     }
2616 
2617     /**
2618      * Default comment for class.
2619      *
2620      * @param javaClass not null
2621      * @return a default comment for class.
2622      */
2623     private static String getDefaultClassJavadocComment(final JavaClass javaClass) {
2624         StringBuilder sb = new StringBuilder();
2625 
2626         sb.append("<p>");
2627         if (javaClass.isAbstract()) {
2628             sb.append("Abstract ");
2629         }
2630 
2631         sb.append(javaClass.getName());
2632 
2633         if (!javaClass.isInterface()) {
2634             sb.append(" class.");
2635         } else {
2636             sb.append(" interface.");
2637         }
2638 
2639         sb.append("</p>");
2640 
2641         return sb.toString();
2642     }
2643 
2644     /**
2645      * Default comment for method with taking care of getter/setter in the javaMethod name.
2646      *
2647      * @param javaExecutable not null
2648      * @return a default comment for method
2649      */
2650     private static String getDefaultMethodJavadocComment(final JavaExecutable javaExecutable) {
2651         if (javaExecutable instanceof JavaConstructor) {
2652             return "<p>Constructor for " + javaExecutable.getName() + ".</p>";
2653         }
2654 
2655         if (javaExecutable.getName().length() > 3
2656                 && (javaExecutable.getName().startsWith("get")
2657                         || javaExecutable.getName().startsWith("set"))) {
2658             String field =
2659                     StringUtils.lowercaseFirstLetter(javaExecutable.getName().substring(3));
2660 
2661             JavaClass clazz = javaExecutable.getDeclaringClass();
2662 
2663             if (clazz.getFieldByName(field) == null) {
2664                 return "<p>" + javaExecutable.getName() + ".</p>";
2665             }
2666 
2667             StringBuilder sb = new StringBuilder();
2668 
2669             sb.append("<p>");
2670             if (javaExecutable.getName().startsWith("get")) {
2671                 sb.append("Getter ");
2672             } else if (javaExecutable.getName().startsWith("set")) {
2673                 sb.append("Setter ");
2674             }
2675             sb.append("for the field <code>").append(field).append("</code>.</p>");
2676 
2677             return sb.toString();
2678         }
2679 
2680         return "<p>" + javaExecutable.getName() + ".</p>";
2681     }
2682 
2683     /**
2684      * Try to find if a Javadoc comment has an {@link #INHERITED_TAG} for instance:
2685      * <pre>
2686      * &#47;&#42;&#42; {&#64;inheritDoc} &#42;&#47;
2687      * </pre>
2688      * or
2689      * <pre>
2690      * &#47;&#42;&#42;
2691      * &#32;&#42; {&#64;inheritDoc}
2692      * &#32;&#42;&#47;
2693      * </pre>
2694      *
2695      * @param content not null
2696      * @return <code>true</code> if the content has an inherited tag, <code>false</code> otherwise.
2697      */
2698     private static boolean hasInheritedTag(final String content) {
2699         final String inheritedTagPattern =
2700                 "^\\s*(\\/\\*\\*)?(\\s*(\\*)?)*(\\{)@inheritDoc\\s*(\\})(\\s*(\\*)?)*(\\*\\/)?$";
2701         return Pattern.matches(inheritedTagPattern, StringUtils.removeDuplicateWhitespace(content));
2702     }
2703 
2704     /**
2705      * Workaround for QDOX-146 about whitespace.
2706      * Ideally we want to use <code>entity.getComment()</code>
2707      * <br/>
2708      * For instance, with the following snippet:
2709      * <br/>
2710      * <p/>
2711      * <code>
2712      * <font color="#808080">1</font>&nbsp;<font color="#ffffff"></font><br />
2713      * <font color="#808080">2</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;</font>
2714      * <font color="#3f5fbf">&#47;&#42;&#42;</font><br />
2715      * <font color="#808080">3</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</font>
2716      * <font color="#3f5fbf">&#42;&nbsp;Dummy&nbsp;Javadoc&nbsp;comment.</font><br />
2717      * <font color="#808080">4</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</font>
2718      * <font color="#3f5fbf">&#42;&nbsp;</font><font color="#7f9fbf">@param&nbsp;</font>
2719      * <font color="#3f5fbf">s&nbsp;a&nbsp;String</font><br />
2720      * <font color="#808080">5</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</font>
2721      * <font color="#3f5fbf">&#42;&#47;</font><br />
2722      * <font color="#808080">6</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;</font>
2723      * <font color="#7f0055"><b>public&nbsp;</b></font><font color="#7f0055"><b>void&nbsp;</b></font>
2724      * <font color="#000000">dummyMethod</font><font color="#000000">(&nbsp;</font>
2725      * <font color="#000000">String&nbsp;s&nbsp;</font><font color="#000000">){}</font><br />
2726      * </code>
2727      * <p/>
2728      * <br/>
2729      * The return will be:
2730      * <br/>
2731      * <p/>
2732      * <code>
2733      * <font color="#808080">1</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</font>
2734      * <font color="#3f5fbf">&#42;&nbsp;Dummy&nbsp;Javadoc&nbsp;comment.</font><br />
2735      * </code>
2736      *
2737      * @param javaClassContent original class content not null
2738      * @param entity           not null
2739      * @return the javadoc comment for the entity without any tags.
2740      * @throws IOException if any
2741      */
2742     static String getJavadocComment(final String javaClassContent, final JavaAnnotatedElement entity)
2743             throws IOException {
2744         if (entity.getComment() == null) {
2745             return "";
2746         }
2747 
2748         String originalJavadoc = extractOriginalJavadocContent(javaClassContent, entity);
2749 
2750         StringBuilder sb = new StringBuilder();
2751         BufferedReader lr = new BufferedReader(new StringReader(originalJavadoc));
2752         String line;
2753         while ((line = lr.readLine()) != null) {
2754             String l = StringUtils.removeDuplicateWhitespace(line.trim());
2755             if (l.startsWith("* @") || l.startsWith("*@")) {
2756                 break;
2757             }
2758             sb.append(line).append(EOL);
2759         }
2760 
2761         return trimRight(sb.toString());
2762     }
2763 
2764     /**
2765      * Work around for QDOX-146 about whitespace.
2766      * Ideally we want to use <code>docletTag.getValue()</code>
2767      * <br/>
2768      * For instance, with the following snippet:
2769      * <br/>
2770      * <p/>
2771      * <code>
2772      * <font color="#808080">1</font>&nbsp;<font color="#ffffff"></font><br />
2773      * <font color="#808080">2</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;</font>
2774      * <font color="#3f5fbf">&#47;&#42;&#42;</font><br />
2775      * <font color="#808080">3</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</font>
2776      * <font color="#3f5fbf">&#42;&nbsp;Dummy&nbsp;Javadoc&nbsp;comment.</font><br />
2777      * <font color="#808080">4</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</font>
2778      * <font color="#3f5fbf">&#42;&nbsp;</font><font color="#7f9fbf">@param&nbsp;</font>
2779      * <font color="#3f5fbf">s&nbsp;a&nbsp;String</font><br />
2780      * <font color="#808080">5</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</font>
2781      * <font color="#3f5fbf">&#42;&#47;</font><br />
2782      * <font color="#808080">6</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;</font>
2783      * <font color="#7f0055"><b>public&nbsp;</b></font><font color="#7f0055"><b>void&nbsp;</b></font>
2784      * <font color="#000000">dummyMethod</font><font color="#000000">(&nbsp;</font>
2785      * <font color="#000000">String&nbsp;s&nbsp;</font><font color="#000000">){}</font><br />
2786      * </code>
2787      * <p/>
2788      * <br/>
2789      * The return will be:
2790      * <br/>
2791      * <p/>
2792      * <code>
2793      * <font color="#808080">1</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</font>
2794      * <font color="#3f5fbf">&#42;&nbsp;</font><font color="#7f9fbf">@param&nbsp;</font>
2795      * <font color="#3f5fbf">s&nbsp;a&nbsp;String</font><br />
2796      * </code>
2797      *
2798      * @param javaClassContent original class content not null
2799      * @param entity           not null
2800      * @param docletTag        not null
2801      * @return the javadoc comment for the entity without Javadoc tags.
2802      * @throws IOException if any
2803      */
2804     String getJavadocComment(
2805             final String javaClassContent, final JavaAnnotatedElement entity, final DocletTag docletTag)
2806             throws IOException {
2807         if (docletTag.getValue() == null || docletTag.getParameters().isEmpty()) {
2808             return "";
2809         }
2810 
2811         String originalJavadoc = extractOriginalJavadocContent(javaClassContent, entity);
2812 
2813         StringBuilder sb = new StringBuilder();
2814         BufferedReader lr = new BufferedReader(new StringReader(originalJavadoc));
2815         String line;
2816         boolean found = false;
2817 
2818         // matching first line of doclettag
2819         Pattern p = Pattern.compile("(\\s*\\*\\s?@" + docletTag.getName() + ")\\s+" + "(\\Q"
2820                 + docletTag.getValue().split("\r\n|\r|\n")[0] + "\\E)");
2821 
2822         while ((line = lr.readLine()) != null) {
2823             Matcher m = p.matcher(line);
2824             if (m.matches()) {
2825                 if (fixTag(LINK_TAG)) {
2826                     line = replaceLinkTags(line, entity);
2827                 }
2828                 sb.append(line).append(EOL);
2829                 found = true;
2830             } else {
2831                 if (line.trim().startsWith("* @") || line.trim().startsWith("*@")) {
2832                     found = false;
2833                 }
2834                 if (found) {
2835                     if (fixTag(LINK_TAG)) {
2836                         line = replaceLinkTags(line, entity);
2837                     }
2838                     sb.append(line).append(EOL);
2839                 }
2840             }
2841         }
2842 
2843         return trimRight(sb.toString());
2844     }
2845 
2846     /**
2847      * Extract the original Javadoc and others comments up to {@link #START_JAVADOC} form the entity. This method
2848      * takes care of the Javadoc indentation. All javadoc lines will be trimmed on right.
2849      * <br/>
2850      * For instance, with the following snippet:
2851      * <br/>
2852      * <p/>
2853      * <code>
2854      * <font color="#808080">1</font>&nbsp;<font color="#ffffff"></font><br />
2855      * <font color="#808080">2</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;</font>
2856      * <font color="#3f5fbf">&#47;&#42;&#42;</font><br />
2857      * <font color="#808080">3</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</font>
2858      * <font color="#3f5fbf">&#42;&nbsp;Dummy&nbsp;Javadoc&nbsp;comment.</font><br />
2859      * <font color="#808080">4</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</font>
2860      * <font color="#3f5fbf">&#42;&nbsp;</font><font color="#7f9fbf">@param&nbsp;</font>
2861      * <font color="#3f5fbf">s&nbsp;a&nbsp;String</font><br />
2862      * <font color="#808080">5</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</font>
2863      * <font color="#3f5fbf">&#42;&#47;</font><br />
2864      * <font color="#808080">6</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;</font>
2865      * <font color="#7f0055"><b>public&nbsp;</b></font><font color="#7f0055"><b>void&nbsp;</b></font>
2866      * <font color="#000000">dummyMethod</font><font color="#000000">(&nbsp;</font>
2867      * <font color="#000000">String&nbsp;s&nbsp;</font><font color="#000000">){}</font><br />
2868      * </code>
2869      * <p/>
2870      * <br/>
2871      * The return will be:
2872      * <br/>
2873      * <p/>
2874      * <code>
2875      * <font color="#808080">1</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;</font>
2876      * <font color="#3f5fbf">&#47;&#42;&#42;</font><br />
2877      * <font color="#808080">2</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</font>
2878      * <font color="#3f5fbf">&#42;&nbsp;Dummy&nbsp;Javadoc&nbsp;comment.</font><br />
2879      * <font color="#808080">3</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</font>
2880      * <font color="#3f5fbf">&#42;&nbsp;</font><font color="#7f9fbf">@param&nbsp;</font>
2881      * <font color="#3f5fbf">s&nbsp;a&nbsp;String</font><br />
2882      * <font color="#808080">4</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</font>
2883      * <font color="#3f5fbf">&#42;&#47;</font><br />
2884      * </code>
2885      *
2886      * @param javaClassContent not null
2887      * @param entity           not null
2888      * @return return the original javadoc as String for the current entity
2889      * @throws IOException if any
2890      */
2891     static String extractOriginalJavadoc(final String javaClassContent, final JavaAnnotatedElement entity)
2892             throws IOException {
2893         if (entity.getComment() == null) {
2894             return "";
2895         }
2896 
2897         String[] javaClassContentLines = getLines(javaClassContent);
2898         List<String> list = new LinkedList<>();
2899         for (int i = entity.getLineNumber() - 2; i >= 0; i--) {
2900             String line = javaClassContentLines[i];
2901 
2902             list.add(trimRight(line));
2903             if (line.trim().startsWith(START_JAVADOC)) {
2904                 break;
2905             }
2906         }
2907 
2908         Collections.reverse(list);
2909 
2910         return StringUtils.join(list.iterator(), EOL);
2911     }
2912 
2913     /**
2914      * Extract the Javadoc comment between {@link #START_JAVADOC} and {@link #END_JAVADOC} form the entity. This method
2915      * takes care of the Javadoc indentation. All javadoc lines will be trimmed on right.
2916      * <br/>
2917      * For instance, with the following snippet:
2918      * <br/>
2919      * <p/>
2920      * <code>
2921      * <font color="#808080">1</font>&nbsp;<font color="#ffffff"></font><br />
2922      * <font color="#808080">2</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;</font>
2923      * <font color="#3f5fbf">&#47;&#42;&#42;</font><br />
2924      * <font color="#808080">3</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</font>
2925      * <font color="#3f5fbf">&#42;&nbsp;Dummy&nbsp;Javadoc&nbsp;comment.</font><br />
2926      * <font color="#808080">4</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</font>
2927      * <font color="#3f5fbf">&#42;&nbsp;</font><font color="#7f9fbf">@param&nbsp;</font>
2928      * <font color="#3f5fbf">s&nbsp;a&nbsp;String</font><br />
2929      * <font color="#808080">5</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</font>
2930      * <font color="#3f5fbf">&#42;&#47;</font><br />
2931      * <font color="#808080">6</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;</font>
2932      * <font color="#7f0055"><b>public&nbsp;</b></font><font color="#7f0055"><b>void&nbsp;</b></font>
2933      * <font color="#000000">dummyMethod</font><font color="#000000">(&nbsp;</font>
2934      * <font color="#000000">String&nbsp;s&nbsp;</font><font color="#000000">){}</font><br />
2935      * </code>
2936      * <p/>
2937      * <br/>
2938      * The return will be:
2939      * <br/>
2940      * <p/>
2941      * <code>
2942      * <font color="#808080">1</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</font>
2943      * <font color="#3f5fbf">&#42;&nbsp;Dummy&nbsp;Javadoc&nbsp;comment.</font><br />
2944      * <font color="#808080">2</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</font>
2945      * <font color="#3f5fbf">&#42;&nbsp;</font><font color="#7f9fbf">@param&nbsp;</font>
2946      * <font color="#3f5fbf">s&nbsp;a&nbsp;String</font><br />
2947      * </code>
2948      *
2949      * @param javaClassContent not null
2950      * @param entity           not null
2951      * @return return the original javadoc as String for the current entity
2952      * @throws IOException if any
2953      */
2954     static String extractOriginalJavadocContent(final String javaClassContent, final JavaAnnotatedElement entity)
2955             throws IOException {
2956         if (entity.getComment() == null) {
2957             return "";
2958         }
2959 
2960         String originalJavadoc = extractOriginalJavadoc(javaClassContent, entity);
2961         int index = originalJavadoc.indexOf(START_JAVADOC);
2962         if (index != -1) {
2963             originalJavadoc = originalJavadoc.substring(index + START_JAVADOC.length());
2964         }
2965         index = originalJavadoc.indexOf(END_JAVADOC);
2966         if (index != -1) {
2967             originalJavadoc = originalJavadoc.substring(0, index);
2968         }
2969         if (originalJavadoc.startsWith("\r\n")) {
2970             originalJavadoc = originalJavadoc.substring(2);
2971         } else if (originalJavadoc.startsWith("\n") || originalJavadoc.startsWith("\r")) {
2972             originalJavadoc = originalJavadoc.substring(1);
2973         }
2974 
2975         return trimRight(originalJavadoc);
2976     }
2977 
2978     /**
2979      * @param content not null
2980      * @return the content without last lines containing javadoc separator (ie <code> * </code>)
2981      * @throws IOException if any
2982      * @see #getJavadocComment(String, JavaAnnotatedElement, DocletTag)
2983      */
2984     private static String removeLastEmptyJavadocLines(final String content) throws IOException {
2985         if (!content.contains(EOL)) {
2986             return content;
2987         }
2988 
2989         String[] lines = getLines(content);
2990         if (lines.length == 1) {
2991             return content;
2992         }
2993 
2994         List<String> linesList = new LinkedList<>(Arrays.asList(lines));
2995 
2996         Collections.reverse(linesList);
2997 
2998         for (Iterator<String> it = linesList.iterator(); it.hasNext(); ) {
2999             String line = it.next();
3000 
3001             if (line.trim().equals("*")) {
3002                 it.remove();
3003             } else {
3004                 break;
3005             }
3006         }
3007 
3008         Collections.reverse(linesList);
3009 
3010         return StringUtils.join(linesList.iterator(), EOL);
3011     }
3012 
3013     /**
3014      * @param content not null
3015      * @return the javadoc comment with the given indentation
3016      * @throws IOException if any
3017      * @see #getJavadocComment(String, JavaAnnotatedElement, DocletTag)
3018      */
3019     private static String alignIndentationJavadocLines(final String content, final String indent) throws IOException {
3020         StringBuilder sb = new StringBuilder();
3021         for (String line : getLines(content)) {
3022             if (sb.length() > 0) {
3023                 sb.append(EOL);
3024             }
3025             if (!line.trim().startsWith("*")) {
3026                 line = "*" + line;
3027             }
3028             sb.append(indent).append(" ").append(trimLeft(line));
3029         }
3030 
3031         return sb.toString();
3032     }
3033 
3034     /**
3035      * Autodetect the indentation of a given line:
3036      * <pre>
3037      * autodetectIndentation( null ) = "";
3038      * autodetectIndentation( "a" ) = "";
3039      * autodetectIndentation( "    a" ) = "    ";
3040      * autodetectIndentation( "\ta" ) = "\t";
3041      * </pre>
3042      *
3043      * @param line not null
3044      * @return the indentation for the given line.
3045      */
3046     private static String autodetectIndentation(final String line) {
3047         if (StringUtils.isEmpty(line)) {
3048             return "";
3049         }
3050 
3051         return line.substring(0, line.indexOf(trimLeft(line)));
3052     }
3053 
3054     /**
3055      * @param content not null
3056      * @return an array of all content lines
3057      * @throws IOException if any
3058      */
3059     private static String[] getLines(final String content) throws IOException {
3060         List<String> lines = new LinkedList<>();
3061 
3062         BufferedReader reader = new BufferedReader(new StringReader(content));
3063         String line = reader.readLine();
3064         while (line != null) {
3065             lines.add(line);
3066             line = reader.readLine();
3067         }
3068 
3069         return lines.toArray(new String[lines.size()]);
3070     }
3071 
3072     /**
3073      * Trim a given line on the left:
3074      * <pre>
3075      * trimLeft( null ) = "";
3076      * trimLeft( "  " ) = "";
3077      * trimLeft( "a" ) = "a";
3078      * trimLeft( "    a" ) = "a";
3079      * trimLeft( "\ta" ) = "a";
3080      * trimLeft( "    a    " ) = "a    ";
3081      * </pre>
3082      *
3083      * @param text
3084      * @return the text trimmed on left side or empty if text is null.
3085      */
3086     private static String trimLeft(final String text) {
3087         if (StringUtils.isEmpty(text) || StringUtils.isEmpty(text.trim())) {
3088             return "";
3089         }
3090 
3091         String textTrimmed = text.trim();
3092         return text.substring(text.indexOf(textTrimmed));
3093     }
3094 
3095     /**
3096      * Trim a given line on the right:
3097      * <pre>
3098      * trimRight( null ) = "";
3099      * trimRight( "  " ) = "";
3100      * trimRight( "a" ) = "a";
3101      * trimRight( "a\t" ) = "a";
3102      * trimRight( "    a    " ) = "    a";
3103      * </pre>
3104      *
3105      * @param text
3106      * @return the text trimmed on tight side or empty if text is null.
3107      */
3108     private static String trimRight(final String text) {
3109         if (StringUtils.isEmpty(text) || StringUtils.isEmpty(text.trim())) {
3110             return "";
3111         }
3112 
3113         String textTrimmed = text.trim();
3114         return text.substring(0, text.indexOf(textTrimmed) + textTrimmed.length());
3115     }
3116 
3117     /**
3118      * Wrapper class for the entity's tags.
3119      */
3120     class JavaEntityTags {
3121         private final JavaAnnotatedElement entity;
3122 
3123         private final boolean isJavaMethod;
3124 
3125         /**
3126          * List of tag names.
3127          */
3128         private List<String> namesTags;
3129 
3130         /**
3131          * Map with java parameter as key and original Javadoc lines as values.
3132          */
3133         private Map<String, String> tagParams;
3134 
3135         private Set<String> documentedParams = new HashSet<>();
3136 
3137         /**
3138          * Original javadoc lines.
3139          */
3140         private String tagReturn;
3141 
3142         /**
3143          * Map with java throw as key and original Javadoc lines as values.
3144          */
3145         private Map<String, String> tagThrows;
3146 
3147         /**
3148          * Original javadoc lines for unknown tags.
3149          */
3150         private List<String> unknownsTags;
3151 
3152         JavaEntityTags(JavaAnnotatedElement entity, boolean isJavaMethod) {
3153             this.entity = entity;
3154             this.isJavaMethod = isJavaMethod;
3155             this.namesTags = new LinkedList<>();
3156             this.tagParams = new LinkedHashMap<>();
3157             this.tagThrows = new LinkedHashMap<>();
3158             this.unknownsTags = new LinkedList<>();
3159         }
3160 
3161         public List<String> getNamesTags() {
3162             return namesTags;
3163         }
3164 
3165         public String getJavadocReturnTag() {
3166             return tagReturn;
3167         }
3168 
3169         public void setJavadocReturnTag(String s) {
3170             tagReturn = s;
3171         }
3172 
3173         public List<String> getUnknownTags() {
3174             return unknownsTags;
3175         }
3176 
3177         public void putJavadocParamTag(String paramName, String paramValue, String originalJavadocTag) {
3178             documentedParams.add(paramName);
3179             tagParams.put(paramValue, originalJavadocTag);
3180         }
3181 
3182         public String getJavadocParamTag(String paramValue) {
3183             String originalJavadocTag = tagParams.get(paramValue);
3184             if (originalJavadocTag == null && getLog().isWarnEnabled()) {
3185                 getLog().warn(getMessage(paramValue, "javaEntityTags.tagParams"));
3186             }
3187             return originalJavadocTag;
3188         }
3189 
3190         public boolean hasJavadocParamTag(String paramName) {
3191             return documentedParams.contains(paramName);
3192         }
3193 
3194         public void putJavadocThrowsTag(String paramName, String originalJavadocTag) {
3195             tagThrows.put(paramName, originalJavadocTag);
3196         }
3197 
3198         public String getJavadocThrowsTag(String paramName) {
3199             return getJavadocThrowsTag(paramName, false);
3200         }
3201 
3202         public String getJavadocThrowsTag(String paramName, boolean nullable) {
3203             String originalJavadocTag = tagThrows.get(paramName);
3204             if (!nullable && originalJavadocTag == null && getLog().isWarnEnabled()) {
3205                 getLog().warn(getMessage(paramName, "javaEntityTags.tagThrows"));
3206             }
3207 
3208             return originalJavadocTag;
3209         }
3210 
3211         private String getMessage(String paramName, String mapName) {
3212             StringBuilder msg = new StringBuilder();
3213             msg.append("No param '")
3214                     .append(paramName)
3215                     .append("' key found in ")
3216                     .append(mapName)
3217                     .append(" for the entity: ");
3218             if (isJavaMethod) {
3219                 JavaMethod javaMethod = (JavaMethod) entity;
3220                 msg.append(getJavaMethodAsString(javaMethod));
3221             } else {
3222                 JavaClass javaClass = (JavaClass) entity;
3223                 msg.append(javaClass.getFullyQualifiedName());
3224             }
3225 
3226             return msg.toString();
3227         }
3228 
3229         /**
3230          * {@inheritDoc}
3231          */
3232         @Override
3233         public String toString() {
3234             StringBuilder sb = new StringBuilder();
3235 
3236             sb.append("namesTags=").append(namesTags).append("\n");
3237             sb.append("tagParams=").append(tagParams).append("\n");
3238             sb.append("tagReturn=").append(tagReturn).append("\n");
3239             sb.append("tagThrows=").append(tagThrows).append("\n");
3240             sb.append("unknownsTags=").append(unknownsTags).append("\n");
3241 
3242             return sb.toString();
3243         }
3244     }
3245 }