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