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