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