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