package org.apache.maven.plugins.javadoc;

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

import com.thoughtworks.qdox.JavaProjectBuilder;
import com.thoughtworks.qdox.library.ClassLibraryBuilder;
import com.thoughtworks.qdox.library.OrderedClassLibraryBuilder;
import com.thoughtworks.qdox.model.DocletTag;
import com.thoughtworks.qdox.model.JavaAnnotatedElement;
import com.thoughtworks.qdox.model.JavaAnnotation;
import com.thoughtworks.qdox.model.JavaClass;
import com.thoughtworks.qdox.model.JavaConstructor;
import com.thoughtworks.qdox.model.JavaExecutable;
import com.thoughtworks.qdox.model.JavaField;
import com.thoughtworks.qdox.model.JavaGenericDeclaration;
import com.thoughtworks.qdox.model.JavaMember;
import com.thoughtworks.qdox.model.JavaMethod;
import com.thoughtworks.qdox.model.JavaParameter;
import com.thoughtworks.qdox.model.JavaType;
import com.thoughtworks.qdox.model.JavaTypeVariable;
import com.thoughtworks.qdox.parser.ParseException;
import com.thoughtworks.qdox.type.TypeResolver;

import org.apache.commons.lang3.ClassUtils;
import org.apache.commons.text.StringEscapeUtils;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.DependencyResolutionRequiredException;
import org.apache.maven.artifact.repository.ArtifactRepository;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.Component;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.project.MavenProject;
import org.apache.maven.settings.Settings;
import org.apache.maven.shared.invoker.MavenInvocationException;
import org.codehaus.plexus.components.interactivity.InputHandler;
import org.codehaus.plexus.languages.java.version.JavaVersion;
import org.codehaus.plexus.util.FileUtils;
import org.codehaus.plexus.util.ReaderFactory;
import org.codehaus.plexus.util.StringUtils;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.io.StringWriter;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Abstract class to fix Javadoc documentation and tags in source files.
 * <br>
 * See <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#wheretags">Where Tags
 * Can Be Used</a>.
 *
 * @author <a href="mailto:vincent.siveton@gmail.com">Vincent Siveton</a>
 * @since 2.6
 */
public abstract class AbstractFixJavadocMojo
    extends AbstractMojo
{
    /**
     * The vm line separator
     */
    private static final String EOL = System.getProperty( "line.separator" );

    /**
     * Tag name for &#64;author *
     */
    private static final String AUTHOR_TAG = "author";

    /**
     * Tag name for &#64;version *
     */
    private static final String VERSION_TAG = "version";

    /**
     * Tag name for &#64;since *
     */
    private static final String SINCE_TAG = "since";

    /**
     * Tag name for &#64;param *
     */
    private static final String PARAM_TAG = "param";

    /**
     * Tag name for &#64;return *
     */
    private static final String RETURN_TAG = "return";

    /**
     * Tag name for &#64;throws *
     */
    private static final String THROWS_TAG = "throws";

    /**
     * Tag name for &#64;link *
     */
    private static final String LINK_TAG = "link";

    /**
     * Tag name for {&#64;inheritDoc} *
     */
    private static final String INHERITED_TAG = "{@inheritDoc}";

    /**
     * Start Javadoc String i.e. <code>&#47;&#42;&#42;</code> *
     */
    private static final String START_JAVADOC = "/**";

    /**
     * End Javadoc String i.e. <code>&#42;&#47;</code> *
     */
    private static final String END_JAVADOC = "*/";

    /**
     * Javadoc Separator i.e. <code> &#42; </code> *
     */
    private static final String SEPARATOR_JAVADOC = " * ";

    /**
     * Inherited Javadoc i.e. <code>&#47;&#42;&#42; {&#64;inheritDoc} &#42;&#47;</code> *
     */
    private static final String INHERITED_JAVADOC = START_JAVADOC + " " + INHERITED_TAG + " " + END_JAVADOC;

    /**
     * <code>all</code> parameter used by {@link #fixTags} *
     */
    private static final String FIX_TAGS_ALL = "all";

    /**
     * <code>public</code> parameter used by {@link #level} *
     */
    private static final String LEVEL_PUBLIC = "public";

    /**
     * <code>protected</code> parameter used by {@link #level} *
     */
    private static final String LEVEL_PROTECTED = "protected";

    /**
     * <code>package</code> parameter used by {@link #level} *
     */
    private static final String LEVEL_PACKAGE = "package";

    /**
     * <code>private</code> parameter used by {@link #level} *
     */
    private static final String LEVEL_PRIVATE = "private";

    /**
     * The Clirr Maven plugin groupId <code>org.codehaus.mojo</code> *
     */
    private static final String CLIRR_MAVEN_PLUGIN_GROUPID = "org.codehaus.mojo";

    /**
     * The Clirr Maven plugin artifactId <code>clirr-maven-plugin</code> *
     */
    private static final String CLIRR_MAVEN_PLUGIN_ARTIFACTID = "clirr-maven-plugin";

    /**
     * The latest Clirr Maven plugin version <code>2.2.2</code> *
     */
    private static final String CLIRR_MAVEN_PLUGIN_VERSION = "2.2.2";

    /**
     * The Clirr Maven plugin goal <code>check</code> *
     */
    private static final String CLIRR_MAVEN_PLUGIN_GOAL = "check";

    /**
     * Java Files Pattern.
     */
    public static final String JAVA_FILES = "**\\/*.java";

    /**
     * Default version value.
     */
    public static final String DEFAULT_VERSION_VALUE = "\u0024Id: \u0024Id";

    // ----------------------------------------------------------------------
    // Mojo components
    // ----------------------------------------------------------------------

    /**
     * Input handler, needed for command line handling.
     */
    @Component
    private InputHandler inputHandler;

    // ----------------------------------------------------------------------
    // Mojo parameters
    // ----------------------------------------------------------------------

    /**
     * Version to compare the current code against using the
     * <a href="http://mojo.codehaus.org/clirr-maven-plugin/">Clirr Maven Plugin</a>.
     * <br/>
     * See <a href="#defaultSince">defaultSince</a>.
     */
    @Parameter ( property = "comparisonVersion", defaultValue = "(,${project.version})" )
    private String comparisonVersion;

    /**
     * Default value for the Javadoc tag <code>&#64;author</code>.
     * <br/>
     * If not specified, the <code>user.name</code> defined in the System properties will be used.
     */
    @Parameter ( property = "defaultAuthor" )
    private String defaultAuthor;

    /**
     * Default value for the Javadoc tag <code>&#64;since</code>.
     */
    @Parameter ( property = "defaultSince", defaultValue = "${project.version}" )
    private String defaultSince;

    /**
     * Default value for the Javadoc tag <code>&#64;version</code>.
     * <br/>
     * By default, it is <code>&#36;Id:&#36;</code>, corresponding to a
     * <a href="http://svnbook.red-bean.com/en/1.1/ch07s02.html#svn-ch-7-sect-2.3.4">SVN keyword</a>.
     * Refer to your SCM to use an other SCM keyword.
     */
    @Parameter ( property = "defaultVersion", defaultValue = DEFAULT_VERSION_VALUE )
    private String defaultVersion = "\u0024Id: \u0024"; // can't use default-value="\u0024Id: \u0024"

    /**
     * The file encoding to use when reading the source files. If the property
     * <code>project.build.sourceEncoding</code> is not set, the platform default encoding is used.
     */
    @Parameter ( property = "encoding", defaultValue = "${project.build.sourceEncoding}" )
    private String encoding;

    /**
     * Comma separated excludes Java files, i.e. <code>&#42;&#42;/&#42;Test.java</code>.
     */
    @Parameter ( property = "excludes" )
    private String excludes;

    /**
     * Comma separated tags to fix in classes, interfaces or methods Javadoc comments.
     * Possible values are:
     * <ul>
     * <li>all (fix all Javadoc tags)</li>
     * <li>author (fix only &#64;author tag)</li>
     * <li>version (fix only &#64;version tag)</li>
     * <li>since (fix only &#64;since tag)</li>
     * <li>param (fix only &#64;param tag)</li>
     * <li>return (fix only &#64;return tag)</li>
     * <li>throws (fix only &#64;throws tag)</li>
     * <li>link (fix only &#64;link tag)</li>
     * </ul>
     */
    @Parameter ( property = "fixTags", defaultValue = "all" )
    private String fixTags;

    /**
     * Flag to fix the classes or interfaces Javadoc comments according the <code>level</code>.
     */
    @Parameter ( property = "fixClassComment", defaultValue = "true" )
    private boolean fixClassComment;

    /**
     * Flag to fix the fields Javadoc comments according the <code>level</code>.
     */
    @Parameter ( property = "fixFieldComment", defaultValue = "true" )
    private boolean fixFieldComment;

    /**
     * Flag to fix the methods Javadoc comments according the <code>level</code>.
     */
    @Parameter ( property = "fixMethodComment", defaultValue = "true" )
    private boolean fixMethodComment;

    /**
     * <p>Flag to remove throws tags from unknown classes.</p>
     * <p><strong>NOTE:</strong>Since 3.1.0 the default value has been changed to {@code true}, 
     * due to JavaDoc 8 strictness. 
     */
    @Parameter ( property = "removeUnknownThrows", defaultValue = "true" )
    private boolean removeUnknownThrows;

    /**
     * Forcing the goal execution i.e. skip warranty messages (not recommended).
     */
    @Parameter ( property = "force" )
    private boolean force;

    /**
     * Flag to ignore or not Clirr.
     */
    @Parameter ( property = "ignoreClirr", defaultValue = "false" )
    protected boolean ignoreClirr;

    /**
     * Comma separated includes Java files, i.e. <code>&#42;&#42;/&#42;Test.java</code>.
     * <p/>
     * <strong>Note:</strong> default value is {@code **\/*.java}.
     */
    @Parameter ( property = "includes", defaultValue = JAVA_FILES )
    private String includes;

    /**
     * Specifies the access level for classes and members to show in the Javadocs.
     * Possible values are:
     * <ul>
     * <li><a href="http://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#public">public</a>
     * (shows only public classes and members)</li>
     * <li><a href="http://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#protected">protected</a>
     * (shows only public and protected classes and members)</li>
     * <li><a href="http://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#package">package</a>
     * (shows all classes and members not marked private)</li>
     * <li><a href="http://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#private">private</a>
     * (shows all classes and members)</li>
     * </ul>
     */
    @Parameter ( property = "level", defaultValue = "protected" )
    private String level;

    /**
     * The local repository where the artifacts are located, used by the tests.
     */
    @Parameter ( property = "localRepository" )
    private ArtifactRepository localRepository;

    /**
     * Output directory where Java classes will be rewritten.
     */
    @Parameter ( property = "outputDirectory", defaultValue = "${project.build.sourceDirectory}" )
    private File outputDirectory;

    /**
     * The Maven Project Object.
     */
    @Parameter( defaultValue = "${project}", readonly = true, required = true )
    private MavenProject project;

    @Parameter ( defaultValue = "${session}", readonly = true, required = true )
    private MavenSession session;

    /**
     * The current user system settings for use in Maven.
     */
    @Parameter( defaultValue = "${settings}", readonly = true, required = true )
    private Settings settings;

    // ----------------------------------------------------------------------
    // Internal fields
    // ----------------------------------------------------------------------

    /**
     * The current project class loader.
     */
    private ClassLoader projectClassLoader;

    /**
     * Split {@link #fixTags} by comma.
     *
     * @see #init()
     */
    private String[] fixTagsSplitted;

    /**
     * New classes found by Clirr.
     */
    private List<String> clirrNewClasses;

    /**
     * New Methods in a Class (the key) found by Clirr.
     */
    private Map<String, List<String>> clirrNewMethods;

    /**
     * List of classes where <code>&#42;since</code> is added. Will be used to add or not this tag in the methods.
     */
    private List<String> sinceClasses;

    /**
     * {@inheritDoc}
     */
    @Override
    public void execute()
        throws MojoExecutionException, MojoFailureException
    {
        if ( !fixClassComment && !fixFieldComment && !fixMethodComment )
        {
            getLog().info( "Specified to NOT fix classes, fields and methods. Nothing to do." );
            return;
        }

        // verify goal params
        init();

        if ( fixTagsSplitted.length == 0 )
        {
            getLog().info( "No fix tag specified. Nothing to do." );
            return;
        }

        // add warranty msg
        if ( !preCheck() )
        {
            return;
        }

        // run clirr
        try
        {
            executeClirr();
        }
        catch ( MavenInvocationException e )
        {
            if ( getLog().isDebugEnabled() )
            {
                getLog().error( "MavenInvocationException: " + e.getMessage(), e );
            }
            else
            {
                getLog().error( "MavenInvocationException: " + e.getMessage() );
            }
            getLog().info( "Clirr is ignored." );
        }

        // run qdox and process
        try
        {
            Collection<JavaClass> javaClasses = getQdoxClasses();

            if ( javaClasses != null )
            {
                for ( JavaClass javaClass : javaClasses )
                {
                    processFix( javaClass );
                }
         }
        }
        catch ( IOException e )
        {
            throw new MojoExecutionException( "IOException: " + e.getMessage(), e );
        }
    }

    // ----------------------------------------------------------------------
    // protected methods
    // ----------------------------------------------------------------------

    protected final MavenProject getProject()
    {
        return project;
    }    

    /**
     * @param p not null maven project.
     * @return the artifact type.
     */
    protected String getArtifactType( MavenProject p )
    {
        return p.getArtifact().getType();
    }

    /**
     * @param p not null maven project.
     * @return the list of source paths for the given project.
     */
    protected List<String> getProjectSourceRoots( MavenProject p )
    {
        return ( p.getCompileSourceRoots() == null
            ? Collections.<String>emptyList()
            : new LinkedList<>( p.getCompileSourceRoots() ) );
    }

    /**
     * @param p not null
     * @return the compile classpath elements
     * @throws DependencyResolutionRequiredException
     *          if any
     */
    protected List<String> getCompileClasspathElements( MavenProject p )
        throws DependencyResolutionRequiredException
    {
        return ( p.getCompileClasspathElements() == null
            ? Collections.<String>emptyList()
            : new LinkedList<>( p.getCompileClasspathElements() ) );
    }

    /**
     * @param javaExecutable not null
     * @return the fully qualify name of javaMethod with signature
     */
    protected static String getJavaMethodAsString( JavaExecutable javaExecutable )
    {
        return javaExecutable.getDeclaringClass().getFullyQualifiedName() + "#" + javaExecutable.getCallSignature();
    }

    // ----------------------------------------------------------------------
    // private methods
    // ----------------------------------------------------------------------

    /**
     * Init goal parameters.
     */
    private void init()
    {
        // defaultAuthor
        if ( StringUtils.isEmpty( defaultAuthor ) )
        {
            defaultAuthor = System.getProperty( "user.name" );
        }

        // defaultSince
        int i = defaultSince.indexOf( "-" + Artifact.SNAPSHOT_VERSION );
        if ( i != -1 )
        {
            defaultSince = defaultSince.substring( 0, i );
        }

        // fixTags
        if ( !FIX_TAGS_ALL.equalsIgnoreCase( fixTags.trim() ) )
        {
            String[] split = StringUtils.split( fixTags, "," );
            List<String> filtered = new LinkedList<>();
            for ( String aSplit : split )
            {
                String s = aSplit.trim();
                if ( JavadocUtil.equalsIgnoreCase( s, FIX_TAGS_ALL, AUTHOR_TAG, VERSION_TAG, SINCE_TAG, PARAM_TAG,
                                                   THROWS_TAG, LINK_TAG, RETURN_TAG ) )
                {
                    filtered.add( s );
                }
                else
                {
                    if ( getLog().isWarnEnabled() )
                    {
                        getLog().warn( "Unrecognized '" + s + "' for fixTags parameter. Ignored it!" );
                    }
                }
            }
            fixTags = StringUtils.join( filtered.iterator(), "," );
        }
        fixTagsSplitted = StringUtils.split( fixTags, "," );

        // encoding
        if ( StringUtils.isEmpty( encoding ) )
        {
            if ( getLog().isWarnEnabled() )
            {
                getLog().warn( "File encoding has not been set, using platform encoding " + ReaderFactory.FILE_ENCODING
                                   + ", i.e. build is platform dependent!" );
            }
            encoding = ReaderFactory.FILE_ENCODING;
        }

        // level
        level = level.trim();
        if ( !JavadocUtil.equalsIgnoreCase( level, LEVEL_PUBLIC, LEVEL_PROTECTED, LEVEL_PACKAGE, LEVEL_PRIVATE ) )
        {
            if ( getLog().isWarnEnabled() )
            {
                getLog().warn( "Unrecognized '" + level + "' for level parameter, using 'protected' level." );
            }
            level = "protected";
        }
    }

    /**
     * @return <code>true</code> if the user wants to proceed, <code>false</code> otherwise.
     * @throws MojoExecutionException if any
     */
    private boolean preCheck()
        throws MojoExecutionException
    {
        if ( force )
        {
            return true;
        }

        if ( outputDirectory != null && !outputDirectory.getAbsolutePath().equals(
            getProjectSourceDirectory().getAbsolutePath() ) )
        {
            return true;
        }

        if ( !settings.isInteractiveMode() )
        {
            getLog().error( "Maven is not attempt to interact with the user for input. "
                                + "Verify the <interactiveMode/> configuration in your settings." );
            return false;
        }

        getLog().warn( "" );
        getLog().warn( "    WARRANTY DISCLAIMER" );
        getLog().warn( "" );
        getLog().warn( "All warranties with regard to this Maven goal are disclaimed!" );
        getLog().warn( "The changes will be done directly in the source code." );
        getLog().warn( "The Maven Team strongly recommends the use of a SCM software BEFORE executing this goal." );
        getLog().warn( "" );

        while ( true )
        {
            getLog().info( "Are you sure to proceed? [Y]es [N]o" );

            try
            {
                String userExpression = inputHandler.readLine();
                if ( userExpression == null || JavadocUtil.equalsIgnoreCase( userExpression, "Y", "Yes" ) )
                {
                    getLog().info( "OK, let's proceed..." );
                    break;
                }
                if ( JavadocUtil.equalsIgnoreCase( userExpression, "N", "No" ) )
                {
                    getLog().info( "No changes in your sources occur." );
                    return false;
                }
            }
            catch ( IOException e )
            {
                throw new MojoExecutionException( "Unable to read from standard input.", e );
            }
        }

        return true;
    }

    /**
     * @return the source dir as File for the given project
     */
    private File getProjectSourceDirectory()
    {
        return new File( project.getBuild().getSourceDirectory() );
    }

    /**
     * Invoke Maven to run clirr-maven-plugin to find API differences.
     *
     * @throws MavenInvocationException if any
     */
    private void executeClirr()
        throws MavenInvocationException
    {
        if ( ignoreClirr )
        {
            getLog().info( "Clirr is ignored." );
            return;
        }

        String clirrGoal = getFullClirrGoal();

        // http://mojo.codehaus.org/clirr-maven-plugin/check-mojo.html
        File clirrTextOutputFile =
            FileUtils.createTempFile( "clirr", ".txt", new File( project.getBuild().getDirectory() ) );
        Properties properties = new Properties();
        properties.put( "textOutputFile", clirrTextOutputFile.getAbsolutePath() );
        properties.put( "comparisonVersion", comparisonVersion );
        properties.put( "failOnError", "false" );
        if ( JavaVersion.JAVA_SPECIFICATION_VERSION.isBefore( "8" ) )
        {
            // ensure that Java7 picks up TLSv1.2 when connecting with Central
            properties.put( "https.protocols", "TLSv1.2" );
        }

        File invokerDir = new File( project.getBuild().getDirectory(), "invoker" );
        invokerDir.mkdirs();
        File invokerLogFile = FileUtils.createTempFile( "clirr-maven-plugin", ".txt", invokerDir );

        JavadocUtil.invokeMaven( getLog(), new File( localRepository.getBasedir() ), project.getFile(),
                                 Collections.singletonList( clirrGoal ), properties, invokerLogFile,
                                 session.getRequest().getGlobalSettingsFile() );

        try
        {
            if ( invokerLogFile.exists() )
            {
                String invokerLogContent =
                    StringUtils.unifyLineSeparators( FileUtils.fileRead( invokerLogFile, "UTF-8" ) );
                // see org.codehaus.mojo.clirr.AbstractClirrMojo#getComparisonArtifact()
                final String artifactNotFoundMsg = "Unable to find a previous version of the project in the repository";
                if ( invokerLogContent.contains( artifactNotFoundMsg ) )
                {
                    getLog().warn( "No previous artifact has been deployed, Clirr is ignored." );
                    return;
                }
            }
        }
        catch ( IOException e )
        {
            getLog().debug( "IOException: " + e.getMessage() );
        }

        try
        {
            parseClirrTextOutputFile( clirrTextOutputFile );
        }
        catch ( IOException e )
        {
            if ( getLog().isDebugEnabled() )
            {
                getLog().debug( "IOException: " + e.getMessage(), e );
            }
            getLog().info( "IOException when parsing Clirr output '" + clirrTextOutputFile.getAbsolutePath()
                               + "', Clirr is ignored." );
        }
    }

    /**
     * @param clirrTextOutputFile not null
     * @throws IOException if any
     */
    private void parseClirrTextOutputFile( File clirrTextOutputFile )
        throws IOException
    {
        if ( !clirrTextOutputFile.exists() )
        {
            if ( getLog().isInfoEnabled() )
            {
                getLog().info(
                    "No Clirr output file '" + clirrTextOutputFile.getAbsolutePath() + "' exists, Clirr is ignored." );
            }
            return;
        }

        if ( getLog().isInfoEnabled() )
        {
            getLog().info( "Clirr output file was created: " + clirrTextOutputFile.getAbsolutePath() );
        }

        clirrNewClasses = new LinkedList<>();
        clirrNewMethods = new LinkedHashMap<>();

        try ( BufferedReader reader = new BufferedReader( ReaderFactory.newReader( clirrTextOutputFile, "UTF-8" ) ) )
        {

            for ( String line = reader.readLine(); line != null; line = reader.readLine() )
            {
                String[] split = StringUtils.split( line, ":" );
                if ( split.length != 4 )
                {
                    if ( getLog().isDebugEnabled() )
                    {
                        getLog().debug( "Unable to parse the clirr line: " + line );
                    }
                    continue;
                }

                int code;
                try
                {
                    code = Integer.parseInt( split[1].trim() );
                }
                catch ( NumberFormatException e )
                {
                    if ( getLog().isDebugEnabled() )
                    {
                        getLog().debug( "Unable to parse the clirr line: " + line );
                    }
                    continue;
                }

                // http://clirr.sourceforge.net/clirr-core/exegesis.html
                // 7011 - Method Added
                // 7012 - Method Added to Interface
                // 8000 - Class Added

                // CHECKSTYLE_OFF: MagicNumber
                switch ( code )
                {
                    case 7011:
                        methodAdded( split );
                        break;
                    case 7012:
                        methodAdded( split );
                        break;
                    case 8000:
                        clirrNewClasses.add( split[2].trim() );
                        break;
                    default:
                        break;
                }
                // CHECKSTYLE_ON: MagicNumber
            }
        }
        if ( clirrNewClasses.isEmpty() && clirrNewMethods.isEmpty() )
        {
            getLog().info( "Clirr NOT found API differences." );
        }
        else
        {
            getLog().info( "Clirr found API differences, i.e. new classes/interfaces or methods." );
        }
    }

    private void methodAdded( String[] split )
    {
        List<String> list = clirrNewMethods.get( split[2].trim() );
        if ( list == null )
        {
            list = new ArrayList<>();
        }
        String[] splits2 = StringUtils.split( split[3].trim(), "'" );
        if ( splits2.length != 3 )
        {
            return;
        }
        list.add( splits2[1].trim() );
        clirrNewMethods.put( split[2].trim(), list );
    }

    /**
     * @param tag not null
     * @return <code>true</code> if <code>tag</code> is defined in {@link #fixTags}.
     */
    private boolean fixTag( String tag )
    {
        if ( fixTagsSplitted.length == 1 && fixTagsSplitted[0].equals( FIX_TAGS_ALL ) )
        {
            return true;
        }

        for ( String aFixTagsSplitted : fixTagsSplitted )
        {
            if ( aFixTagsSplitted.trim().equals( tag ) )
            {
                return true;
            }
        }

        return false;
    }

    /**
     * Calling Qdox to find {@link JavaClass} objects from the Maven project sources.
     * Ignore java class if Qdox has parsing errors.
     *
     * @return an array of {@link JavaClass} found by QDox
     * @throws IOException            if any
     * @throws MojoExecutionException if any
     */
    private Collection<JavaClass> getQdoxClasses()
        throws IOException, MojoExecutionException
    {
        if ( "pom".equalsIgnoreCase( project.getPackaging() ) )
        {
            getLog().warn( "This project has 'pom' packaging, no Java sources is available." );
            return null;
        }

        List<File> javaFiles = new LinkedList<>();
        for ( String sourceRoot : getProjectSourceRoots( project ) )
        {
            File f = new File( sourceRoot );
            if ( f.isDirectory() )
            {
                javaFiles.addAll( FileUtils.getFiles( f, includes, excludes, true ) );
            }
            else
            {
                if ( getLog().isWarnEnabled() )
                {
                    getLog().warn( f + " doesn't exist. Ignored it." );
                }
            }
        }

        ClassLibraryBuilder classLibraryBuilder = new OrderedClassLibraryBuilder();
        classLibraryBuilder.appendClassLoader( getProjectClassLoader() );

        JavaProjectBuilder builder = new JavaProjectBuilder( classLibraryBuilder );
        builder.setEncoding( encoding );
        for ( File f : javaFiles )
        {
            if ( !f.getAbsolutePath().toLowerCase( Locale.ENGLISH ).endsWith( ".java" ) && getLog().isWarnEnabled() )
            {
                getLog().warn( "'" + f + "' is not a Java file. Ignored it." );
                continue;
            }

            try
            {
                builder.addSource( f );
            }
            catch ( ParseException e )
            {
                if ( getLog().isWarnEnabled() )
                {
                    getLog().warn( "QDOX ParseException: " + e.getMessage() + ". Can't fix it." );
                }
            }
        }

        return builder.getClasses();
    }

    /**
     * @return the classLoader for the given project using lazy instantiation.
     * @throws MojoExecutionException if any
     */
    private ClassLoader getProjectClassLoader()
        throws MojoExecutionException
    {
        if ( projectClassLoader == null )
        {
            List<String> classPath;
            try
            {
                classPath = getCompileClasspathElements( project );
            }
            catch ( DependencyResolutionRequiredException e )
            {
                throw new MojoExecutionException( "DependencyResolutionRequiredException: " + e.getMessage(), e );
            }

            List<URL> urls = new ArrayList<>( classPath.size() );
            for ( String filename : classPath )
            {
                try
                {
                    urls.add( new File( filename ).toURI().toURL() );
                }
                catch ( MalformedURLException e )
                {
                    throw new MojoExecutionException( "MalformedURLException: " + e.getMessage(), e );
                }
            }

            projectClassLoader = new URLClassLoader( urls.toArray( new URL[urls.size()] ), null );
        }

        return projectClassLoader;
    }

    /**
     * Process the given {@link JavaClass}, ie add missing javadoc tags depending user parameters.
     *
     * @param javaClass not null
     * @throws IOException            if any
     * @throws MojoExecutionException if any
     */
    private void processFix( JavaClass javaClass )
        throws IOException, MojoExecutionException
    {
        // Skipping inner classes
        if ( javaClass.isInner() )
        {
            return;
        }

        File javaFile;
        try
        {
            javaFile = Paths.get( javaClass.getSource().getURL().toURI() ).toFile();
        }
        catch ( URISyntaxException e )
        {
            throw new MojoExecutionException( e.getMessage() );
        }
        
        // the original java content in memory
        final String originalContent = StringUtils.unifyLineSeparators( FileUtils.fileRead( javaFile, encoding ) );

        if ( getLog().isDebugEnabled() )
        {
            getLog().debug( "Analyzing " + javaClass.getFullyQualifiedName() );
        }

        final StringWriter stringWriter = new StringWriter();
        boolean changeDetected = false;
        try ( BufferedReader reader = new BufferedReader( new StringReader( originalContent ) ) )
        {

            int lineNumber = 0;
            for ( String line = reader.readLine(); line != null; line = reader.readLine() )
            {
                lineNumber++;
                final String indent = autodetectIndentation( line );

                // fixing classes
                if ( javaClass.getComment() == null && javaClass.getAnnotations() != null
                    && !javaClass.getAnnotations().isEmpty() )
                {
                    if ( lineNumber == javaClass.getAnnotations().get( 0 ).getLineNumber() )
                    {
                        changeDetected |= fixClassComment( stringWriter, originalContent, javaClass, indent );

                        takeCareSingleComment( stringWriter, originalContent, javaClass );
                    }
                }
                else if ( lineNumber == javaClass.getLineNumber() )
                {
                    changeDetected |= fixClassComment( stringWriter, originalContent, javaClass, indent );

                    takeCareSingleComment( stringWriter, originalContent, javaClass );
                }

                // fixing fields
                if ( javaClass.getFields() != null )
                {
                    for ( JavaField field : javaClass.getFields() )
                    {
                        if ( lineNumber == field.getLineNumber() )
                        {
                            changeDetected |= fixFieldComment( stringWriter, javaClass, field, indent );
                        }
                    }
                }
                
                // fixing methods
                if ( javaClass.getConstructors() != null )
                {
                    for ( JavaConstructor method :  javaClass.getConstructors() )
                    {
                        if ( lineNumber == method.getLineNumber() )
                        {
                            final boolean commentUpdated =
                                    fixMethodComment( stringWriter, originalContent, method, indent );
                            if ( commentUpdated )
                            {
                                takeCareSingleComment( stringWriter, originalContent, method );
                            }
                            changeDetected |= commentUpdated;
                        }
                    }
                }
                

                // fixing methods
                for ( JavaMethod method :  javaClass.getMethods() )
                {
                    int methodLineNumber;
                    if ( method.getComment() == null && !method.getAnnotations().isEmpty() )
                    {
                        methodLineNumber = method.getAnnotations().get( 0 ).getLineNumber();
                    }
                    else
                    {
                        methodLineNumber = method.getLineNumber();
                    }
                    
                    if ( lineNumber == methodLineNumber )
                    {
                        final boolean commentUpdated =
                                fixMethodComment( stringWriter, originalContent, method, indent );
                        if ( commentUpdated )
                        {
                            takeCareSingleComment( stringWriter, originalContent, method );
                        }
                        changeDetected |= commentUpdated;
                    }
                }

                stringWriter.write( line );
                stringWriter.write( EOL );
            }
        }

        if ( changeDetected )
        {
            if ( getLog().isInfoEnabled() )
            {
                getLog().info( "Saving changes to " + javaClass.getFullyQualifiedName() );
            }

            if ( outputDirectory != null && !outputDirectory.getAbsolutePath().equals(
                    getProjectSourceDirectory().getAbsolutePath() ) )
            {
                String path = StringUtils.replace( javaFile.getAbsolutePath().replaceAll( "\\\\", "/" ),
                        project.getBuild().getSourceDirectory().replaceAll( "\\\\", "/" ), "" );
                javaFile = new File( outputDirectory, path );
                javaFile.getParentFile().mkdirs();
            }
            writeFile( javaFile, encoding, stringWriter.toString() );
        }
        else
        {
            if ( getLog().isDebugEnabled() ) 
            {
                getLog().debug( "No changes made to " + javaClass.getFullyQualifiedName() );
            }
        }
    }

    /**
     * Take care of block or single comments between Javadoc comment and entity declaration ie:
     * <br/>
     * <code>
     * <font color="#808080">1</font>&nbsp;<font color="#ffffff">&nbsp;</font>
     * <font color="#3f5fbf">&#47;&#42;&#42;</font><br />
     * <font color="#808080">2</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;</font>
     * <font color="#3f5fbf">&#42;&nbsp;{Javadoc&nbsp;Comment}</font><br />
     * <font color="#808080">3</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;</font>
     * <font color="#3f5fbf">&#42;&#47;</font><br />
     * <font color="#808080">4</font>&nbsp;<font color="#ffffff">&nbsp;</font>
     * <font color="#3f7f5f">&#47;&#42;</font><br />
     * <font color="#808080">5</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;</font>
     * <font color="#3f7f5f">&#42;&nbsp;{Block&nbsp;Comment}</font><br />
     * <font color="#808080">6</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;</font>
     * <font color="#3f7f5f">&#42;&#47;</font><br />
     * <font color="#808080">7</font>&nbsp;<font color="#ffffff">&nbsp;</font>
     * <font color="#3f7f5f">&#47;&#47;&nbsp;{Single&nbsp;comment}</font><br />
     * <font color="#808080">8</font>&nbsp;<font color="#ffffff">&nbsp;</font>
     * <font color="#7f0055"><b>public&nbsp;</b></font><font color="#7f0055"><b>void&nbsp;</b></font>
     * <font color="#000000">dummyMethod</font><font color="#000000">(&nbsp;</font>
     * <font color="#000000">String&nbsp;s&nbsp;</font><font color="#000000">){}</font>
     * </code>
     *
     * @param stringWriter    not null
     * @param originalContent not null
     * @param entity          not null
     * @throws IOException if any
     * @see #extractOriginalJavadoc
     */
    private void takeCareSingleComment( final StringWriter stringWriter, final String originalContent,
                                        final JavaAnnotatedElement entity )
        throws IOException
    {
        if ( entity.getComment() == null )
        {
            return;
        }

        String javadocComment = trimRight( extractOriginalJavadoc( originalContent, entity ) );
        String extraComment = javadocComment.substring( javadocComment.indexOf( END_JAVADOC ) + END_JAVADOC.length() );
        if ( StringUtils.isNotEmpty( extraComment ) )
        {
            if ( extraComment.contains( EOL ) )
            {
                stringWriter.write( extraComment.substring( extraComment.indexOf( EOL ) + EOL.length() ) );
            }
            else
            {
                stringWriter.write( extraComment );
            }
            stringWriter.write( EOL );
        }
    }

    /**
     * Add/update Javadoc class comment.
     *
     * @param stringWriter
     * @param originalContent
     * @param javaClass
     * @param indent
     * @return {@code true} if the comment is updated, otherwise {@code false}
     * @throws MojoExecutionException
     * @throws IOException
     */
    private boolean fixClassComment( final StringWriter stringWriter, final String originalContent,
                                  final JavaClass javaClass, final String indent )
        throws MojoExecutionException, IOException
    {
        if ( !fixClassComment )
        {
            return false;
        }

        if ( !isInLevel( javaClass.getModifiers() ) )
        {
            return false;
        }

        // add
        if ( javaClass.getComment() == null )
        {
            addDefaultClassComment( stringWriter, javaClass, indent );
            return true;
        }

        // update
        return updateEntityComment( stringWriter, originalContent, javaClass, indent );
    }

    /**
     * @param modifiers list of modifiers (public, private, protected, package)
     * @return <code>true</code> if modifier is align with <code>level</code>.
     */
    private boolean isInLevel( List<String> modifiers )
    {
        if ( LEVEL_PUBLIC.equalsIgnoreCase( level.trim() ) )
        {
            return modifiers.contains( LEVEL_PUBLIC );
        }

        if ( LEVEL_PROTECTED.equalsIgnoreCase( level.trim() ) )
        {
            return ( modifiers.contains( LEVEL_PUBLIC ) || modifiers.contains( LEVEL_PROTECTED ) );
        }

        if ( LEVEL_PACKAGE.equalsIgnoreCase( level.trim() ) )
        {
            return !modifiers.contains( LEVEL_PRIVATE );
        }

        // should be private (shows all classes and members)
        return true;
    }

    /**
     * Add a default Javadoc for the given class, i.e.:
     * <br/>
     * <code>
     * <font color="#808080">1</font>&nbsp;<font color="#ffffff">&nbsp;</font>
     * <font color="#3f5fbf">&#47;&#42;&#42;</font><br />
     * <font color="#808080">2</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;</font>
     * <font color="#3f5fbf">&#42;&nbsp;{Comment&nbsp;based&nbsp;on&nbsp;the&nbsp;class&nbsp;name}</font><br />
     * <font color="#808080">3</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;</font>
     * <font color="#3f5fbf">&#42;</font><br />
     * <font color="#808080">4</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;</font>
     * <font color="#3f5fbf">&#42;&nbsp;</font><font color="#7f9fbf">@author&nbsp;</font>
     * <font color="#3f5fbf">X&nbsp;{added&nbsp;if&nbsp;addMissingAuthor}</font><br />
     * <font color="#808080">5</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;</font>
     * <font color="#3f5fbf">&#42;&nbsp;</font><font color="#7f9fbf">@version&nbsp;</font>
     * <font color="#3f5fbf">X&nbsp;{added&nbsp;if&nbsp;addMissingVersion}</font><br />
     * <font color="#808080">6</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;</font>
     * <font color="#3f5fbf">&#42;&nbsp;</font><font color="#7f9fbf">@since&nbsp;</font>
     * <font color="#3f5fbf">X&nbsp;{added&nbsp;if&nbsp;addMissingSince&nbsp;and&nbsp;new&nbsp;classes
     * from&nbsp;previous&nbsp;version}</font><br />
     * <font color="#808080">7</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;</font>
     * <font color="#3f5fbf">&#42;&#47;</font><br />
     * <font color="#808080">8</font>&nbsp;<font color="#7f0055"><b>public&nbsp;class&nbsp;</b></font>
     * <font color="#000000">DummyClass&nbsp;</font><font color="#000000">{}</font></code>
     * </code>
     *
     * @param stringWriter not null
     * @param javaClass    not null
     * @param indent       not null
     * @see #getDefaultClassJavadocComment(JavaClass)
     * @see #appendDefaultAuthorTag(StringBuilder, String)
     * @see #appendDefaultSinceTag(StringBuilder, String)
     * @see #appendDefaultVersionTag(StringBuilder, String)
     */
    private void addDefaultClassComment( final StringWriter stringWriter, final JavaClass javaClass,
                                         final String indent )
    {
        StringBuilder sb = new StringBuilder();

        sb.append( indent ).append( START_JAVADOC );
        sb.append( EOL );
        sb.append( indent ).append( SEPARATOR_JAVADOC );
        sb.append( getDefaultClassJavadocComment( javaClass ) );
        sb.append( EOL );

        appendSeparator( sb, indent );

        appendDefaultAuthorTag( sb, indent );

        appendDefaultVersionTag( sb, indent );

        if ( fixTag( SINCE_TAG ) )
        {
            if ( !ignoreClirr )
            {
                if ( isNewClassFromLastVersion( javaClass ) )
                {
                    appendDefaultSinceTag( sb, indent );
                }
            }
            else
            {
                appendDefaultSinceTag( sb, indent );
                addSinceClasses( javaClass );
            }
        }

        sb.append( indent ).append( " " ).append( END_JAVADOC );
        sb.append( EOL );

        stringWriter.write( sb.toString() );
    }

    /**
     * Add Javadoc field comment, only for static fields or interface fields.
     *
     * @param stringWriter not null
     * @param javaClass    not null
     * @param field        not null
     * @param indent       not null
     * @return {@code true} if comment was updated, otherwise {@code false}
     * @throws IOException if any
     */
    private boolean fixFieldComment( final StringWriter stringWriter, final JavaClass javaClass, final JavaField field,
                                  final String indent )
        throws IOException
    {
        if ( !fixFieldComment )
        {
            return false;
        }

        if ( !javaClass.isInterface() && ( !isInLevel( field.getModifiers() ) || !field.isStatic() ) )
        {
            return false;
        }

        // add
        if ( field.getComment() == null )
        {
            addDefaultFieldComment( stringWriter, field, indent );
            return true;
        }

        // no update
        return false;
    }

    /**
     * Add a default Javadoc for the given field, i.e.:
     * <br/>
     * <code>
     * <font color="#808080">1</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;</font>
     * <font color="#3f5fbf">&#47;&#42;&#42;&nbsp;Constant&nbsp;</font><font color="#7f7f9f">&lt;code&gt;</font>
     * <font color="#3f5fbf">MY_STRING_CONSTANT=&#34;value&#34;</font>
     * <font color="#7f7f9f">&lt;/code&gt;&nbsp;</font><font color="#3f5fbf">&#42;&#47;</font><br />
     * <font color="#808080">2</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;</font>
     * <font color="#7f0055"><b>public&nbsp;static&nbsp;final&nbsp;</b></font>
     * <font color="#000000">String&nbsp;MY_STRING_CONSTANT&nbsp;=&nbsp;</font>
     * <font color="#2a00ff">&#34;value&#34;</font><font color="#000000">;</font>
     * </code>
     *
     * @param stringWriter not null
     * @param field        not null
     * @param indent       not null
     * @throws IOException if any
     */
    private void addDefaultFieldComment( final StringWriter stringWriter, final JavaField field, final String indent )
        throws IOException
    {
        StringBuilder sb = new StringBuilder();

        sb.append( indent ).append( START_JAVADOC ).append( " " );
        sb.append( "Constant <code>" ).append( field.getName() );

        if ( StringUtils.isNotEmpty( field.getInitializationExpression() ) )
        {
            String qualifiedName = field.getType().getFullyQualifiedName();

            if ( qualifiedName.equals( Byte.TYPE.toString() ) || qualifiedName.equals( Short.TYPE.toString() )
                || qualifiedName.equals( Integer.TYPE.toString() ) || qualifiedName.equals( Long.TYPE.toString() )
                || qualifiedName.equals( Float.TYPE.toString() ) || qualifiedName.equals( Double.TYPE.toString() )
                || qualifiedName.equals( Boolean.TYPE.toString() ) || qualifiedName.equals(
                Character.TYPE.toString() ) )
            {
                sb.append( "=" );
                sb.append( StringEscapeUtils.escapeHtml4( field.getInitializationExpression().trim() ) );
            }

            if ( qualifiedName.equals( String.class.getName() ) )
            {
                StringBuilder value = new StringBuilder();
                String[] lines = getLines( field.getInitializationExpression() );
                for ( String line : lines )
                {
                    StringTokenizer token = new StringTokenizer( line.trim(), "\"\n\r" );
                    while ( token.hasMoreTokens() )
                    {
                        String s = token.nextToken();

                        if ( s.trim().equals( "+" ) )
                        {
                            continue;
                        }
                        if ( s.trim().endsWith( "\\" ) )
                        {
                            s += "\"";
                        }
                        value.append( s );
                    }
                }

                sb.append( "=\"" );
                String escapedValue = StringEscapeUtils.escapeHtml4( value.toString() );
                // reduce the size
                // CHECKSTYLE_OFF: MagicNumber
                if ( escapedValue.length() < 40 )
                {
                    sb.append( escapedValue ).append( "\"" );
                }
                else
                {
                    sb.append( escapedValue, 0, 39 ).append( "\"{trunked}" );
                }
                // CHECKSTYLE_ON: MagicNumber
            }
        }

        sb.append( "</code> " ).append( END_JAVADOC );
        sb.append( EOL );

        stringWriter.write( sb.toString() );
    }

    /**
     * Add/update Javadoc method comment.
     *
     * @param stringWriter    not null
     * @param originalContent not null
     * @param javaExecutable      not null
     * @param indent          not null
     * @return {@code true} if comment was updated, otherwise {@code false}
     * @throws MojoExecutionException if any
     * @throws IOException            if any
     */
    private boolean fixMethodComment( final StringWriter stringWriter, final String originalContent,
                                   final JavaExecutable javaExecutable, final String indent )
        throws MojoExecutionException, IOException
    {
        if ( !fixMethodComment )
        {
            return false;
        }

        if ( !javaExecutable.getDeclaringClass().isInterface() && !isInLevel( javaExecutable.getModifiers() ) )
        {
            return false;
        }

        // add
        if ( javaExecutable.getComment() == null )
        {
            addDefaultMethodComment( stringWriter, javaExecutable, indent );
            return true;
        }

        // update
        return updateEntityComment( stringWriter, originalContent, javaExecutable, indent );
    }

    /**
     * Add in the buffer a default Javadoc for the given class:
     * <br/>
     * <code>
     * <font color="#808080">1</font>&nbsp;<font color="#ffffff">&nbsp;</font>
     * <font color="#3f5fbf">&#47;&#42;&#42;</font><br />
     * <font color="#808080">2</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;</font>
     * <font color="#3f5fbf">&#42;&nbsp;{Comment&nbsp;based&nbsp;on&nbsp;the&nbsp;method&nbsp;name}</font><br />
     * <font color="#808080">3</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;</font>
     * <font color="#3f5fbf">&#42;</font><br />
     * <font color="#808080">4</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;</font>
     * <font color="#3f5fbf">&#42;&nbsp;</font><font color="#7f9fbf">@param&nbsp;</font>
     * <font color="#3f5fbf">X&nbsp;{added&nbsp;if&nbsp;addMissingParam}</font><br />
     * <font color="#808080">5</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;</font>
     * <font color="#3f5fbf">&#42;&nbsp;</font><font color="#7f9fbf">@return&nbsp;</font>
     * <font color="#3f5fbf">X&nbsp;{added&nbsp;if&nbsp;addMissingReturn}</font><br />
     * <font color="#808080">6</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;</font>
     * <font color="#3f5fbf">&#42;&nbsp;</font><font color="#7f9fbf">@throws&nbsp;</font>
     * <font color="#3f5fbf">X&nbsp;{added&nbsp;if&nbsp;addMissingThrows}</font><br />
     * <font color="#808080">7</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;</font>
     * <font color="#3f5fbf">&#42;&nbsp;</font><font color="#7f9fbf">@since&nbsp;</font>
     * <font color="#3f5fbf">X&nbsp;{added&nbsp;if&nbsp;addMissingSince&nbsp;and&nbsp;new&nbsp;classes
     * from&nbsp;previous&nbsp;version}</font><br />
     * <font color="#808080">8</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;</font>
     * <font color="#3f5fbf">&#42;&#47;</font><br />
     * <font color="#808080">9</font>&nbsp;<font color="#7f0055"><b>public&nbsp;</b></font>
     * <font color="#7f0055"><b>void&nbsp;</b></font><font color="#000000">dummyMethod</font>
     * <font color="#000000">(&nbsp;</font><font color="#000000">String&nbsp;s&nbsp;</font>
     * <font color="#000000">){}</font>
     * </code>
     *
     * @param stringWriter   not null
     * @param javaExecutable not null
     * @param indent         not null
     * @throws MojoExecutionException if any
     * @see #getDefaultMethodJavadocComment
     * @see #appendDefaultSinceTag(StringBuilder, String)
     */
    private void addDefaultMethodComment( final StringWriter stringWriter, final JavaExecutable javaExecutable,
                                          final String indent )
        throws MojoExecutionException
    {
        StringBuilder sb = new StringBuilder();

        // special case
        if ( isInherited( javaExecutable ) )
        {
            sb.append( indent ).append( INHERITED_JAVADOC );
            sb.append( EOL );

            stringWriter.write( sb.toString() );
            return;
        }

        sb.append( indent ).append( START_JAVADOC );
        sb.append( EOL );
        sb.append( indent ).append( SEPARATOR_JAVADOC );
        sb.append( getDefaultMethodJavadocComment( javaExecutable ) );
        sb.append( EOL );

        boolean separatorAdded = false;
        if ( fixTag( PARAM_TAG ) )
        {
            if ( javaExecutable.getParameters() != null )
            {
                for ( JavaParameter javaParameter : javaExecutable.getParameters() )
                {
                    separatorAdded = appendDefaultParamTag( sb, indent, separatorAdded, javaParameter );
                }
            }
            // is generic?
            if ( javaExecutable.getTypeParameters() != null )
            {
                for ( JavaTypeVariable<JavaGenericDeclaration> typeParam : javaExecutable.getTypeParameters() )
                {
                    separatorAdded = appendDefaultParamTag( sb, indent, separatorAdded, typeParam );
                }
            }
        }
        if ( javaExecutable instanceof JavaMethod )
        {
            JavaMethod javaMethod = (JavaMethod) javaExecutable;
            if ( fixTag( RETURN_TAG ) && javaMethod.getReturns() != null && !javaMethod.getReturns().isVoid() )
            {
                separatorAdded = appendDefaultReturnTag( sb, indent, separatorAdded, javaMethod );
            }
            
        }
        if ( fixTag( THROWS_TAG ) && javaExecutable.getExceptions() != null )
        {
            for ( JavaType exception : javaExecutable.getExceptions() )
            {
                separatorAdded = appendDefaultThrowsTag( sb, indent, separatorAdded, exception );
            }
        }
        if ( fixTag( SINCE_TAG ) && isNewMethodFromLastRevision( javaExecutable ) )
        {
            separatorAdded = appendDefaultSinceTag( sb, indent, separatorAdded );
        }

        sb.append( indent ).append( " " ).append( END_JAVADOC );
        sb.append( EOL );

        stringWriter.write( sb.toString() );
    }

    /**
     * @param stringWriter    not null
     * @param originalContent not null
     * @param entity          not null
     * @param indent          not null
     * @return the updated changeDetected flag
     * @throws MojoExecutionException if any
     * @throws IOException            if any
     */
    private boolean updateEntityComment( final StringWriter stringWriter, final String originalContent,
                                         final JavaAnnotatedElement entity, final String indent )
        throws MojoExecutionException, IOException
    {
        boolean changeDetected = false;
        
        String old = null;
        String s = stringWriter.toString();
        int i = s.lastIndexOf( START_JAVADOC );
        if ( i != -1 )
        {
            String tmp = s.substring( 0, i );
            if ( tmp.lastIndexOf( EOL ) != -1 )
            {
                tmp = tmp.substring( 0, tmp.lastIndexOf( EOL ) );
            }
            
            old = stringWriter.getBuffer().substring( i );

            stringWriter.getBuffer().delete( 0, stringWriter.getBuffer().length() );
            stringWriter.write( tmp );
            stringWriter.write( EOL );
        }
        else
        {
            changeDetected = true;
        }

        updateJavadocComment( stringWriter, originalContent, entity, indent );
        
        if ( changeDetected )
        {
            return true; // return now if we already know there's a change
        }
        
        return !stringWriter.getBuffer().substring( i ).equals( old );
    }

    /**
     * @param stringWriter    not null
     * @param originalContent not null
     * @param entity          not null
     * @param indent          not null
     * @throws MojoExecutionException if any
     * @throws IOException            if any
     */
    private void updateJavadocComment( final StringWriter stringWriter, final String originalContent,
                                       final JavaAnnotatedElement entity, final String indent )
        throws MojoExecutionException, IOException
    {
        if ( entity.getComment() == null && ( entity.getTags() == null || entity.getTags().isEmpty() ) )
        {
            return;
        }

        boolean isJavaExecutable = entity instanceof JavaExecutable;

        StringBuilder sb = new StringBuilder();

        // special case for inherited method
        if ( isJavaExecutable )
        {
            JavaExecutable javaMethod = (JavaExecutable) entity;

            if ( isInherited( javaMethod ) )
            {
                // QDOX-154 could be empty
                if ( StringUtils.isEmpty( javaMethod.getComment() ) )
                {
                    sb.append( indent ).append( INHERITED_JAVADOC );
                    sb.append( EOL );
                    stringWriter.write( sb.toString() );
                    return;
                }

                String javadoc = getJavadocComment( originalContent, javaMethod );

                // case: /** {@inheritDoc} */ or no tags
                if ( hasInheritedTag( javadoc ) && ( javaMethod.getTags() == null
                    || javaMethod.getTags().isEmpty() ) )
                {
                    sb.append( indent ).append( INHERITED_JAVADOC );
                    sb.append( EOL );
                    stringWriter.write( sb.toString() );
                    return;
                }

                if ( javadoc.contains( START_JAVADOC ) )
                {
                    javadoc = javadoc.substring( javadoc.indexOf( START_JAVADOC ) + START_JAVADOC.length() );
                }
                if ( javadoc.contains( END_JAVADOC ) )
                {
                    javadoc = javadoc.substring( 0, javadoc.indexOf( END_JAVADOC ) );
                }

                sb.append( indent ).append( START_JAVADOC );
                sb.append( EOL );
                if ( !javadoc.contains( INHERITED_TAG ) )
                {
                    sb.append( indent ).append( SEPARATOR_JAVADOC ).append( INHERITED_TAG );
                    sb.append( EOL );
                    appendSeparator( sb, indent );
                }
                javadoc = removeLastEmptyJavadocLines( javadoc );
                javadoc = alignIndentationJavadocLines( javadoc, indent );
                sb.append( javadoc );
                sb.append( EOL );
                if ( javaMethod.getTags() != null )
                {
                    for ( DocletTag docletTag : javaMethod.getTags() )
                    {
                        // Voluntary ignore these tags
                        if ( JavadocUtil.equals( docletTag.getName(), PARAM_TAG, RETURN_TAG, THROWS_TAG ) )
                        {
                            continue;
                        }

                        String s = getJavadocComment( originalContent, entity, docletTag );
                        s = removeLastEmptyJavadocLines( s );
                        s = alignIndentationJavadocLines( s, indent );
                        sb.append( s );
                        sb.append( EOL );
                    }
                }
                sb.append( indent ).append( " " ).append( END_JAVADOC );
                sb.append( EOL );

                if ( hasInheritedTag( sb.toString().trim() ) )
                {
                    sb = new StringBuilder();
                    sb.append( indent ).append( INHERITED_JAVADOC );
                    sb.append( EOL );
                    stringWriter.write( sb.toString() );
                    return;
                }

                stringWriter.write( sb.toString() );
                return;
            }
        }

        sb.append( indent ).append( START_JAVADOC );
        sb.append( EOL );

        // comment
        if ( StringUtils.isNotEmpty( entity.getComment() ) )
        {
            updateJavadocComment( sb, originalContent, entity, indent );
        }
        else
        {
            addDefaultJavadocComment( sb, entity, indent, isJavaExecutable );
        }

        // tags
        updateJavadocTags( sb, originalContent, entity, indent, isJavaExecutable );

        sb = new StringBuilder( removeLastEmptyJavadocLines( sb.toString() ) ).append( EOL );

        sb.append( indent ).append( " " ).append( END_JAVADOC );
        sb.append( EOL );

        stringWriter.write( sb.toString() );
    }

    /**
     * @param sb              not null
     * @param originalContent not null
     * @param entity          not null
     * @param indent          not null
     * @throws IOException if any
     */
    private void updateJavadocComment( final StringBuilder sb, final String originalContent,
                                       final JavaAnnotatedElement entity, final String indent )
        throws IOException
    {
        String comment = getJavadocComment( originalContent, entity );
        comment = removeLastEmptyJavadocLines( comment );
        comment = alignIndentationJavadocLines( comment, indent );

        if ( comment.contains( START_JAVADOC ) )
        {
            comment = comment.substring( comment.indexOf( START_JAVADOC ) + START_JAVADOC.length() );
            comment = indent + SEPARATOR_JAVADOC + comment.trim();
        }
        if ( comment.contains( END_JAVADOC ) )
        {
            comment = comment.substring( 0, comment.indexOf( END_JAVADOC ) );
        }

        if ( fixTag( LINK_TAG ) )
        {
            comment = replaceLinkTags( comment, entity );
        }

        String[] lines = getLines( comment );
        for ( String line : lines )
        {
            sb.append( indent ).append( " " ).append( line.trim() );
            sb.append( EOL );
        }
    }

    private static final Pattern REPLACE_LINK_TAGS_PATTERN = Pattern.compile( "\\{@link\\s" );

    static String replaceLinkTags( String comment, JavaAnnotatedElement entity )
    {
        StringBuilder resolvedComment = new StringBuilder();
        // scan comment for {@link someClassName} and try to resolve this
        Matcher linktagMatcher = REPLACE_LINK_TAGS_PATTERN.matcher( comment );
        int startIndex = 0;
        while ( linktagMatcher.find() )
        {
            int startName = linktagMatcher.end();
            resolvedComment.append( comment, startIndex, startName );
            int endName = comment.indexOf( "}", startName );
            if ( endName >= 0 )
            {
                String name;
                String link = comment.substring( startName, endName );
                int hashIndex = link.indexOf( '#' );
                if ( hashIndex >= 0 )
                {
                    name = link.substring( 0, hashIndex );
                }
                else
                {
                    name = link;
                }
                if ( StringUtils.isNotBlank( name ) )
                {
                    String typeName;
                    if ( entity instanceof JavaClass )
                    {
                        JavaClass clazz = (JavaClass) entity;
                        typeName =
                            TypeResolver.byClassName( clazz.getBinaryName(), clazz.getJavaClassLibrary(),
                                                      clazz.getSource().getImports() ).resolveType( name.trim() );
                    }
                    else if ( entity instanceof JavaMember )
                    {
                        JavaClass clazz = ( (JavaMember) entity ).getDeclaringClass();
                        typeName =
                            TypeResolver.byClassName( clazz.getBinaryName(), clazz.getJavaClassLibrary(),
                                                      clazz.getSource().getImports() ).resolveType( name.trim() );
                    }
                    else
                    {
                        typeName = null;
                    }

                    if ( typeName == null )
                    {
                        typeName = name.trim();
                    }
                    else
                    {
                        typeName = typeName.replaceAll( "\\$", "." );
                    }
                    //adjust name for inner classes
                    resolvedComment.append( typeName );
                }
                if ( hashIndex >= 0 )
                {
                    resolvedComment.append( link.substring( hashIndex ).trim() );
                }
                startIndex = endName;
            }
            else
            {
                startIndex = startName;
            }

        }
        resolvedComment.append( comment.substring( startIndex ) );
        return resolvedComment.toString();

    }

    /**
     * @param sb           not null
     * @param entity       not null
     * @param indent       not null
     * @param isJavaExecutable
     */
    private void addDefaultJavadocComment( final StringBuilder sb, final JavaAnnotatedElement entity,
                                           final String indent, final boolean isJavaExecutable )
    {
        sb.append( indent ).append( SEPARATOR_JAVADOC );
        if ( isJavaExecutable )
        {
            sb.append( getDefaultMethodJavadocComment( (JavaExecutable) entity ) );
        }
        else
        {
            sb.append( getDefaultClassJavadocComment( (JavaClass) entity ) );
        }
        sb.append( EOL );
    }

    /**
     * @param sb              not null
     * @param originalContent not null
     * @param entity          not null
     * @param indent          not null
     * @param isJavaExecutable
     * @throws IOException            if any
     * @throws MojoExecutionException if any
     */
    private void updateJavadocTags( final StringBuilder sb, final String originalContent,
                                    final JavaAnnotatedElement entity, final String indent,
                                    final boolean isJavaExecutable )
        throws IOException, MojoExecutionException
    {
        appendSeparator( sb, indent );

        // parse tags
        JavaEntityTags javaEntityTags = parseJavadocTags( originalContent, entity, indent, isJavaExecutable );

        // update and write tags
        updateJavadocTags( sb, entity, isJavaExecutable, javaEntityTags );

        // add missing tags...
        addMissingJavadocTags( sb, entity, indent, isJavaExecutable, javaEntityTags );
    }

    /**
     * Parse entity tags
     *
     * @param originalContent not null
     * @param entity          not null
     * @param indent          not null
     * @param isJavaMethod
     * @return an instance of {@link JavaEntityTags}
     * @throws IOException if any
     */
    JavaEntityTags parseJavadocTags( final String originalContent, final JavaAnnotatedElement entity,
                                             final String indent, final boolean isJavaMethod )
        throws IOException
    {
        JavaEntityTags javaEntityTags = new JavaEntityTags( entity, isJavaMethod );
        for ( DocletTag docletTag : entity.getTags() )
        {
            String originalJavadocTag = getJavadocComment( originalContent, entity, docletTag );
            originalJavadocTag = removeLastEmptyJavadocLines( originalJavadocTag );
            originalJavadocTag = alignIndentationJavadocLines( originalJavadocTag, indent );

            javaEntityTags.getNamesTags().add( docletTag.getName() );

            if ( isJavaMethod )
            {
                List<String> params = docletTag.getParameters();
                if ( params.size() < 1 )
                {
                    continue;
                }

                String paramName = params.get( 0 );
                switch ( docletTag.getName() ) 
                {
                    case PARAM_TAG:
                        javaEntityTags.putJavadocParamTag( paramName, docletTag.getValue(), originalJavadocTag );
                        break;
                    case RETURN_TAG:
                        javaEntityTags.setJavadocReturnTag( originalJavadocTag );
                        break;
                    case THROWS_TAG:
                        javaEntityTags.putJavadocThrowsTag( paramName, originalJavadocTag );
                        break;
                    default:
                        javaEntityTags.getUnknownTags().add( originalJavadocTag );
                        break;
                }
            }
            else
            {
                javaEntityTags.getUnknownTags().add( originalJavadocTag );
            }
        }

        return javaEntityTags;
    }

    /**
     * Write tags according javaEntityTags.
     *
     * @param sb             not null
     * @param entity         not null
     * @param isJavaExecutable
     * @param javaEntityTags not null
     */
    private void updateJavadocTags( final StringBuilder sb, final JavaAnnotatedElement entity,
                                    final boolean isJavaExecutable, final JavaEntityTags javaEntityTags )
    {
        for ( DocletTag docletTag : entity.getTags() )
        {
            if ( isJavaExecutable )
            {
                JavaExecutable javaExecutable = (JavaExecutable) entity;

                List<String> params = docletTag.getParameters();
                if ( params.size() < 1 )
                {
                    continue;
                }

                if ( docletTag.getName().equals( PARAM_TAG ) )
                {
                    writeParamTag( sb, javaExecutable, javaEntityTags, params.get( 0 ), docletTag.getValue() );
                }
                else if ( docletTag.getName().equals( RETURN_TAG ) && javaExecutable instanceof JavaMethod )
                {
                    writeReturnTag( sb, (JavaMethod) javaExecutable, javaEntityTags );
                }
                else if ( docletTag.getName().equals( THROWS_TAG ) )
                {
                    writeThrowsTag( sb, javaExecutable, javaEntityTags, params );
                }
                else
                {
                    // write unknown tags
                    for ( Iterator<String> it = javaEntityTags.getUnknownTags().iterator(); it.hasNext(); )
                    {
                        String originalJavadocTag = it.next();
                        String simplified = StringUtils.removeDuplicateWhitespace( originalJavadocTag ).trim();

                        if ( simplified.contains( "@" + docletTag.getName() ) )
                        {
                            it.remove();
                            sb.append( originalJavadocTag );
                            sb.append( EOL );
                        }
                    }
                }
            }
            else
            {
                for ( Iterator<String> it = javaEntityTags.getUnknownTags().iterator(); it.hasNext(); )
                {
                    String originalJavadocTag = it.next();
                    String simplified = StringUtils.removeDuplicateWhitespace( originalJavadocTag ).trim();

                    if ( simplified.contains( "@" + docletTag.getName() ) )
                    {
                        it.remove();
                        sb.append( originalJavadocTag );
                        sb.append( EOL );
                    }
                }
            }

            if ( sb.toString().endsWith( EOL ) )
            {
                sb.delete( sb.toString().lastIndexOf( EOL ), sb.toString().length() );
            }

            sb.append( EOL );
        }
    }

    private void writeParamTag( final StringBuilder sb, final JavaExecutable javaExecutable,
                                final JavaEntityTags javaEntityTags, String paramName, String paramValue )
    {
        if ( !fixTag( PARAM_TAG ) )
        {
            // write original param tag if found
            String originalJavadocTag = javaEntityTags.getJavadocParamTag( paramValue );
            if ( originalJavadocTag != null )
            {
                sb.append( originalJavadocTag );
            }
            return;
        }

        boolean found = false;
        JavaParameter javaParam = javaExecutable.getParameterByName( paramName );
        if ( javaParam == null )
        {
            // is generic?
            List<JavaTypeVariable<JavaGenericDeclaration>> typeParams = javaExecutable.getTypeParameters();
            for ( JavaTypeVariable<JavaGenericDeclaration> typeParam : typeParams )
            {
                if ( ( "<" + typeParam.getName() + ">" ).equals( paramName ) )
                {
                    found = true;
                }
            }
        }
        else
        {
            found = true;
        }

        if ( !found )
        {
            if ( getLog().isWarnEnabled() )
            {
                getLog().warn(
                    "Fixed unknown param '" + paramName + "' defined in " + getJavaMethodAsString( javaExecutable ) );
            }

            if ( sb.toString().endsWith( EOL ) )
            {
                sb.delete( sb.toString().lastIndexOf( EOL ), sb.toString().length() );
            }
        }
        else
        {
            String originalJavadocTag = javaEntityTags.getJavadocParamTag( paramValue );
            if ( originalJavadocTag != null )
            {
                sb.append( originalJavadocTag );
                String s = "@" + PARAM_TAG + " " + paramName;
                if ( StringUtils.removeDuplicateWhitespace( originalJavadocTag ).trim().endsWith( s ) )
                {
                    sb.append( " " );
                    sb.append( getDefaultJavadocForType( javaParam.getJavaClass() ) );
                }
            }
        }
    }

    private void writeReturnTag( final StringBuilder sb, final JavaMethod javaMethod,
                                 final JavaEntityTags javaEntityTags )
    {
        String originalJavadocTag = javaEntityTags.getJavadocReturnTag();
        if ( originalJavadocTag == null )
        {
            return;
        }

        if ( !fixTag( RETURN_TAG ) )
        {
            // write original param tag if found
            sb.append( originalJavadocTag );
            return;
        }

        if ( StringUtils.isNotEmpty( originalJavadocTag ) && javaMethod.getReturns() != null
            && !javaMethod.getReturns().isVoid() )
        {
            sb.append( originalJavadocTag );
            if ( originalJavadocTag.trim().endsWith( "@" + RETURN_TAG ) )
            {
                sb.append( " " );
                sb.append( getDefaultJavadocForType( javaMethod.getReturns() ) );
            }
        }
    }

    void writeThrowsTag( final StringBuilder sb, final JavaExecutable javaExecutable,
                                 final JavaEntityTags javaEntityTags, final List<String> params )
    {
        String exceptionClassName = params.get( 0 );

        String originalJavadocTag = javaEntityTags.getJavadocThrowsTag( exceptionClassName );
        if ( originalJavadocTag == null )
        {
            return;
        }

        if ( !fixTag( THROWS_TAG ) )
        {
            // write original param tag if found
            sb.append( originalJavadocTag );
            return;
        }

        if ( javaExecutable.getExceptions() != null )
        {
            for ( JavaType exception : javaExecutable.getExceptions() )
            {
                if ( exception.getFullyQualifiedName().endsWith( exceptionClassName ) )
                {
                    originalJavadocTag = StringUtils.replace( originalJavadocTag, exceptionClassName,
                                                              exception.getFullyQualifiedName() );
                    if ( StringUtils.removeDuplicateWhitespace( originalJavadocTag ).trim().endsWith(
                        "@" + THROWS_TAG + " " + exception.getValue() ) )
                    {
                        originalJavadocTag += " if any.";
                    }

                    sb.append( originalJavadocTag );

                    // added qualified name
                    javaEntityTags.putJavadocThrowsTag( exception.getValue(), originalJavadocTag );

                    return;
                }
            }
        }

        Class<?> clazz = getClass( javaExecutable.getDeclaringClass(), exceptionClassName );
        
        if ( clazz != null )
        {
            if ( RuntimeException.class.isAssignableFrom( clazz ) )
            {
                sb.append( StringUtils.replace( originalJavadocTag, exceptionClassName, clazz.getName() ) );

                // added qualified name
                javaEntityTags.putJavadocThrowsTag( clazz.getName(), originalJavadocTag );
            }
            else if ( Throwable.class.isAssignableFrom( clazz ) )
            {
                getLog().debug( "Removing '" + originalJavadocTag + "'; Throwable not specified by "
                    + getJavaMethodAsString( javaExecutable ) + " and it is not a RuntimeException." );
            }
            else
            {
                getLog().debug( "Removing '" + originalJavadocTag + "'; It is not a Throwable" );
            }
        }
        else if ( removeUnknownThrows )
        {
            getLog().warn( "Ignoring unknown throws '" + exceptionClassName + "' defined on "
                    + getJavaMethodAsString( javaExecutable ) );
        }
        else
        {
            getLog().warn( "Found unknown throws '" + exceptionClassName + "' defined on "
                    + getJavaMethodAsString( javaExecutable ) );
            
            sb.append( originalJavadocTag );
            
            if ( params.size() == 1 )
            {
                sb.append( " if any." );
            }
            
            javaEntityTags.putJavadocThrowsTag( exceptionClassName, originalJavadocTag );
        }
    }

    /**
     * Add missing tags not already written.
     *
     * @param sb             not null
     * @param entity         not null
     * @param indent         not null
     * @param isJavaExecutable
     * @param javaEntityTags not null
     * @throws MojoExecutionException if any
     */
    private void addMissingJavadocTags( final StringBuilder sb, final JavaAnnotatedElement entity,
                                        final String indent, final boolean isJavaExecutable,
                                        final JavaEntityTags javaEntityTags )
        throws MojoExecutionException
    {
        if ( isJavaExecutable )
        {
            JavaExecutable javaExecutable = (JavaExecutable) entity;

            if ( fixTag( PARAM_TAG ) )
            {
                if ( javaExecutable.getParameters() != null )
                {
                    for ( JavaParameter javaParameter : javaExecutable.getParameters() )
                    {
                        if ( !javaEntityTags.hasJavadocParamTag( javaParameter.getName() ) )
                        {
                            appendDefaultParamTag( sb, indent, javaParameter );
                        }
                    }
                }
                // is generic?
                if ( javaExecutable.getTypeParameters() != null )
                {
                    for ( JavaTypeVariable<JavaGenericDeclaration> typeParam : javaExecutable.getTypeParameters() )
                    {
                        if ( !javaEntityTags.hasJavadocParamTag( "<" + typeParam.getName() + ">" ) )
                        {
                            appendDefaultParamTag( sb, indent, typeParam );
                        }
                    }
                }
            }

            if ( javaExecutable instanceof JavaMethod )
            {
                JavaMethod javaMethod = (JavaMethod) javaExecutable; 
                if ( fixTag( RETURN_TAG ) && StringUtils.isEmpty( javaEntityTags.getJavadocReturnTag() )
                    && javaMethod.getReturns() != null && !javaMethod.getReturns().isVoid() )
                {
                    appendDefaultReturnTag( sb, indent, javaMethod );
                }
            }

            if ( fixTag( THROWS_TAG ) && javaExecutable.getExceptions() != null )
            {
                for ( JavaType exception : javaExecutable.getExceptions() )
                {
                    if ( javaEntityTags.getJavadocThrowsTag( exception.getValue(), true ) == null )
                    {
                        appendDefaultThrowsTag( sb, indent, exception );
                    }
                }
            }
        }
        else
        {
            if ( !javaEntityTags.getNamesTags().contains( AUTHOR_TAG ) )
            {
                appendDefaultAuthorTag( sb, indent );
            }
            if ( !javaEntityTags.getNamesTags().contains( VERSION_TAG ) )
            {
                appendDefaultVersionTag( sb, indent );
            }
        }

        if ( fixTag( SINCE_TAG ) && !javaEntityTags.getNamesTags().contains( SINCE_TAG ) )
        {
            if ( !isJavaExecutable )
            {
                if ( !ignoreClirr )
                {
                    if ( isNewClassFromLastVersion( (JavaClass) entity ) )
                    {
                        appendDefaultSinceTag( sb, indent );
                    }
                }
                else
                {
                    appendDefaultSinceTag( sb, indent );
                    addSinceClasses( (JavaClass) entity );
                }
            }
            else
            {
                if ( !ignoreClirr )
                {
                    if ( isNewMethodFromLastRevision( (JavaExecutable) entity ) )
                    {
                        appendDefaultSinceTag( sb, indent );
                    }
                }
                else if ( sinceClasses != null )
                {
                    if ( entity instanceof JavaMember
                        && !sinceClassesContains( ( (JavaMember) entity ).getDeclaringClass() ) )
                    {
                        appendDefaultSinceTag( sb, indent );
                    }
                    else if ( entity instanceof JavaClass
                        && !sinceClassesContains( ( (JavaClass) entity ).getDeclaringClass() ) )
                    {
                        appendDefaultSinceTag( sb, indent );
                    }
                }
            }
        }
    }

    /**
     * @param sb             not null
     * @param indent         not null
     * @param separatorAdded
     * @return true if separator has been added.
     */
    private boolean appendDefaultAuthorTag( final StringBuilder sb, final String indent, boolean separatorAdded )
    {
        if ( !fixTag( AUTHOR_TAG ) )
        {
            return separatorAdded;
        }

        if ( !separatorAdded )
        {
            appendSeparator( sb, indent );
            separatorAdded = true;
        }

        appendDefaultAuthorTag( sb, indent );
        return separatorAdded;
    }

    /**
     * @param sb     not null
     * @param indent not null
     */
    private void appendDefaultAuthorTag( final StringBuilder sb, final String indent )
    {
        if ( !fixTag( AUTHOR_TAG ) )
        {
            return;
        }

        sb.append( indent ).append( " * @" ).append( AUTHOR_TAG ).append( " " );
        sb.append( defaultAuthor );
        sb.append( EOL );
    }

    /**
     * @param sb             not null
     * @param indent         not null
     * @param separatorAdded
     * @return true if separator has been added.
     */
    private boolean appendDefaultSinceTag( final StringBuilder sb, final String indent, boolean separatorAdded )
    {
        if ( !fixTag( SINCE_TAG ) )
        {
            return separatorAdded;
        }

        if ( !separatorAdded )
        {
            appendSeparator( sb, indent );
            separatorAdded = true;
        }

        appendDefaultSinceTag( sb, indent );
        return separatorAdded;
    }

    /**
     * @param sb     not null
     * @param indent not null
     */
    private void appendDefaultSinceTag( final StringBuilder sb, final String indent )
    {
        if ( !fixTag( SINCE_TAG ) )
        {
            return;
        }

        sb.append( indent ).append( " * @" ).append( SINCE_TAG ).append( " " );
        sb.append( defaultSince );
        sb.append( EOL );
    }

    /**
     * @param sb             not null
     * @param indent         not null
     * @param separatorAdded
     * @return true if separator has been added.
     */
    private boolean appendDefaultVersionTag( final StringBuilder sb, final String indent, boolean separatorAdded )
    {
        if ( !fixTag( VERSION_TAG ) )
        {
            return separatorAdded;
        }

        if ( !separatorAdded )
        {
            appendSeparator( sb, indent );
            separatorAdded = true;
        }

        appendDefaultVersionTag( sb, indent );
        return separatorAdded;
    }

    /**
     * @param sb     not null
     * @param indent not null
     */
    private void appendDefaultVersionTag( final StringBuilder sb, final String indent )
    {
        if ( !fixTag( VERSION_TAG ) )
        {
            return;
        }

        sb.append( indent ).append( " * @" ).append( VERSION_TAG ).append( " " );
        sb.append( defaultVersion );
        sb.append( EOL );
    }

    /**
     * @param sb             not null
     * @param indent         not null
     * @param separatorAdded
     * @param typeParam  not null
     * @return true if separator has been added.
     */
    private boolean appendDefaultParamTag( final StringBuilder sb, final String indent, boolean separatorAdded,
                                           final JavaParameter typeParam )
    {
        if ( !fixTag( PARAM_TAG ) )
        {
            return separatorAdded;
        }

        if ( !separatorAdded )
        {
            appendSeparator( sb, indent );
            separatorAdded = true;
        }

        appendDefaultParamTag( sb, indent, typeParam );
        return separatorAdded;
    }

    /**
     * @param sb             not null
     * @param indent         not null
     * @param separatorAdded
     * @param typeParameter  not null
     * @return true if separator has been added.
     */
    private boolean appendDefaultParamTag( final StringBuilder sb, final String indent, boolean separatorAdded,
                                           final JavaTypeVariable<JavaGenericDeclaration> typeParameter )
    {
        if ( !fixTag( PARAM_TAG ) )
        {
            return separatorAdded;
        }

        if ( !separatorAdded )
        {
            appendSeparator( sb, indent );
            separatorAdded = true;
        }

        appendDefaultParamTag( sb, indent, typeParameter );
        return separatorAdded;
    }

    /**
     * @param sb            not null
     * @param indent        not null
     * @param typeParam not null
     */
    private void appendDefaultParamTag( final StringBuilder sb, final String indent, final JavaParameter typeParam )
    {
        if ( !fixTag( PARAM_TAG ) )
        {
            return;
        }

        sb.append( indent ).append( " * @" ).append( PARAM_TAG ).append( " " );
        sb.append( typeParam.getName() );
        sb.append( " " );
        sb.append( getDefaultJavadocForType( typeParam.getJavaClass() ) );
        sb.append( EOL );
    }

    /**
     * @param sb            not null
     * @param indent        not null
     * @param typeParameter not null
     */
    private void appendDefaultParamTag( final StringBuilder sb, final String indent,
                                        final JavaTypeVariable<JavaGenericDeclaration> typeParameter )
    {
        if ( !fixTag( PARAM_TAG ) )
        {
            return;
        }

        sb.append( indent ).append( " * @" ).append( PARAM_TAG ).append( " " );
        sb.append( "<" ).append( typeParameter.getName() ).append( ">" );
        sb.append( " " );
        sb.append( getDefaultJavadocForType( typeParameter ) );
        sb.append( EOL );
    }

    /**
     * @param sb             not null
     * @param indent         not null
     * @param separatorAdded
     * @param javaMethod     not null
     * @return true if separator has been added.
     */
    private boolean appendDefaultReturnTag( final StringBuilder sb, final String indent, boolean separatorAdded,
                                            final JavaMethod javaMethod )
    {
        if ( !fixTag( RETURN_TAG ) )
        {
            return separatorAdded;
        }

        if ( !separatorAdded )
        {
            appendSeparator( sb, indent );
            separatorAdded = true;
        }

        appendDefaultReturnTag( sb, indent, javaMethod );
        return separatorAdded;
    }

    /**
     * @param sb         not null
     * @param indent     not null
     * @param javaMethod not null
     */
    private void appendDefaultReturnTag( final StringBuilder sb, final String indent, final JavaMethod javaMethod )
    {
        if ( !fixTag( RETURN_TAG ) )
        {
            return;
        }

        sb.append( indent ).append( " * @" ).append( RETURN_TAG ).append( " " );
        sb.append( getDefaultJavadocForType( javaMethod.getReturns() ) );
        sb.append( EOL );
    }

    /**
     * @param sb             not null
     * @param indent         not null
     * @param separatorAdded
     * @param exception      not null
     * @return true if separator has been added.
     */
    private boolean appendDefaultThrowsTag( final StringBuilder sb, final String indent, boolean separatorAdded,
                                            final JavaType exception )
    {
        if ( !fixTag( THROWS_TAG ) )
        {
            return separatorAdded;
        }

        if ( !separatorAdded )
        {
            appendSeparator( sb, indent );
            separatorAdded = true;
        }

        appendDefaultThrowsTag( sb, indent, exception );
        return separatorAdded;
    }

    /**
     * @param sb        not null
     * @param indent    not null
     * @param exception not null
     */
    private void appendDefaultThrowsTag( final StringBuilder sb, final String indent, final JavaType exception )
    {
        if ( !fixTag( THROWS_TAG ) )
        {
            return;
        }

        sb.append( indent ).append( " * @" ).append( THROWS_TAG ).append( " " );
        sb.append( exception.getFullyQualifiedName() );
        sb.append( " if any." );
        sb.append( EOL );
    }

    /**
     * @param sb     not null
     * @param indent not null
     */
    private void appendSeparator( final StringBuilder sb, final String indent )
    {
        sb.append( indent ).append( " *" );
        sb.append( EOL );
    }

    /**
     * Verify if a method has <code>&#64;java.lang.Override()</code> annotation or if it is an inherited method
     * from an interface or a super class. The goal is to handle <code>&#123;&#64;inheritDoc&#125;</code> tag.
     *
     * @param javaMethod not null
     * @return <code>true</code> if the method is inherited, <code>false</code> otherwise.
     * @throws MojoExecutionException if any
     */
    private boolean isInherited( JavaExecutable javaMethod )
        throws MojoExecutionException
    {
        if ( javaMethod.getAnnotations() != null )
        {
            for ( JavaAnnotation annotation : javaMethod.getAnnotations() )
            {
                if ( annotation.toString().equals( "@java.lang.Override()" ) )
                {
                    return true;
                }
            }
        }

        Class<?> clazz = getClass( javaMethod.getDeclaringClass().getFullyQualifiedName() );

        List<Class<?>> interfaces = ClassUtils.getAllInterfaces( clazz );
        for ( Class<?> intface : interfaces )
        {
            if ( isInherited( intface, javaMethod ) )
            {
                return true;
            }
        }

        List<Class<?>> classes = ClassUtils.getAllSuperclasses( clazz );
        for ( Class<?> superClass : classes )
        {
            if ( isInherited( superClass, javaMethod ) )
            {
                return true;
            }
        }

        return false;
    }

    /**
     * @param clazz      the Java class object, not null
     * @param javaMethod the QDox JavaMethod object not null
     * @return <code>true</code> if <code>javaMethod</code> exists in the given <code>clazz</code>,
     *         <code>false</code> otherwise.
     * @see #isInherited(JavaExecutable)
     */
    private boolean isInherited( Class<?> clazz, JavaExecutable javaMethod )
    {
        for ( Method method : clazz.getDeclaredMethods() )
        {
            if ( !method.getName().equals( javaMethod.getName() ) )
            {
                continue;
            }

            if ( method.getParameterTypes().length != javaMethod.getParameters().size() )
            {
                continue;
            }

            boolean found = false;
            int j = 0;
            for ( Class<?> paramType : method.getParameterTypes() )
            {
                String name1 = paramType.getName();
                String name2 = javaMethod.getParameters().get( j++ ).getType().getFullyQualifiedName();
                found = name1.equals( name2 ); // TODO check algo, seems broken (only takes in account the last param)
            }

            return found;
        }

        return false;
    }

    /**
     * @param clazz
     * @return
     */
    private String getDefaultJavadocForType( JavaClass clazz )
    {
        StringBuilder sb = new StringBuilder();

        if ( !JavaTypeVariable.class.isAssignableFrom( clazz.getClass() ) && clazz.isPrimitive() )
        {
            if ( clazz.isArray() )
            {
                sb.append( "an array of " ).append( clazz.getComponentType().getCanonicalName() );
            }
            else
            {
                sb.append( "a " ).append( clazz.getCanonicalName() );
            }
            return sb.toString();
        }

        StringBuilder javadocLink = new StringBuilder();
        try
        {
            getClass( clazz.getCanonicalName() );

            javadocLink.append( "{@link " );
            
            if ( clazz.isArray() )
            {
                javadocLink.append( clazz.getComponentType().getCanonicalName() );
            }
            else
            {
                javadocLink.append( clazz.getCanonicalName() );
            }
            javadocLink.append( "}" );
        }
        catch ( Exception e )
        {
            javadocLink.append( clazz.getValue() );
        }

        if ( clazz.isArray() )
        {
            sb.append( "an array of " ).append( javadocLink ).append( " objects" );
        }
        else
        {
            sb.append( "a " ).append( javadocLink ).append( " object" );
        }

        return sb.toString();
    }
    
    private String getDefaultJavadocForType( JavaTypeVariable<JavaGenericDeclaration> typeParameter )
    {
        return "a " + typeParameter.getName() + " class";
    }

    /**
     * Check under Clirr if this given class is newer from the last version.
     *
     * @param javaClass a given class not null
     * @return <code>true</code> if Clirr said that this class is added from the last version,
     *         <code>false</code> otherwise or if {@link #clirrNewClasses} is null.
     */
    private boolean isNewClassFromLastVersion( JavaClass javaClass )
    {
        return ( clirrNewClasses != null ) && clirrNewClasses.contains( javaClass.getFullyQualifiedName() );
    }

    /**
     * Check under Clirr if this given method is newer from the last version.
     *
     * @param javaExecutable a given method not null
     * @return <code>true</code> if Clirr said that this method is added from the last version,
     *         <code>false</code> otherwise or if {@link #clirrNewMethods} is null.
     * @throws MojoExecutionException if any
     */
    private boolean isNewMethodFromLastRevision( JavaExecutable javaExecutable )
        throws MojoExecutionException
    {
        if ( clirrNewMethods == null )
        {
            return false;
        }

        List<String> clirrMethods = clirrNewMethods.get( javaExecutable.getDeclaringClass().getFullyQualifiedName() );
        if ( clirrMethods == null )
        {
            return false;
        }

        for ( String clirrMethod : clirrMethods )
        {
            // see net.sf.clirr.core.internal.checks.MethodSetCheck#getMethodId(JavaType clazz, Method method)
            String retrn = "";
            if ( javaExecutable instanceof JavaMethod && ( (JavaMethod) javaExecutable ).getReturns() != null )
            {
                retrn = ( (JavaMethod) javaExecutable ).getReturns().getFullyQualifiedName();
            }
            StringBuilder params = new StringBuilder();
            for ( JavaParameter parameter : javaExecutable.getParameters() )
            {
                if ( params.length() > 0 )
                {
                    params.append( ", " );
                }
                params.append( parameter.getResolvedFullyQualifiedName() );
            }
            if ( clirrMethod.contains( retrn + " " ) && clirrMethod.contains( javaExecutable.getName() + "(" )
                && clirrMethod.contains( "(" + params.toString() + ")" ) )
            {
                return true;
            }
        }

        return false;
    }

    /**
     * @param className not null
     * @return the Class corresponding to the given class name using the project classloader.
     * @throws MojoExecutionException if class not found
     * @see ClassUtils#getClass(ClassLoader, String, boolean)
     * @see #getProjectClassLoader()
     */
    private Class<?> getClass( String className )
        throws MojoExecutionException
    {
        try
        {
            return ClassUtils.getClass( getProjectClassLoader(), className, false );
        }
        catch ( ClassNotFoundException e )
        {
            throw new MojoExecutionException( "ClassNotFoundException: " + e.getMessage(), e );
        }
    }

    /**
     * Returns the Class object assignable for {@link RuntimeException} class and associated with the given
     * exception class name.
     *
     * @param currentClass       not null
     * @param exceptionClassName not null, an exception class name defined as:
     *                           <ul>
     *                           <li>exception class fully qualified</li>
     *                           <li>exception class in the same package</li>
     *                           <li>exception inner class</li>
     *                           <li>exception class in java.lang package</li>
     *                           </ul>
     * @return the class if found, otherwise {@code null}.
     * @see #getClass(String)
     */
    private Class<?> getClass( JavaClass currentClass, String exceptionClassName )
    {
        String[] potentialClassNames =
            new String[]{ exceptionClassName, currentClass.getPackage().getName() + "." + exceptionClassName,
                currentClass.getPackage().getName() + "." + currentClass.getName() + "$" + exceptionClassName,
                "java.lang." + exceptionClassName };

        Class<?> clazz = null;
        for ( String potentialClassName : potentialClassNames )
        {
            try
            {
                clazz = getClass( potentialClassName );
            }
            catch ( MojoExecutionException e )
            {
                // nop
            }
            if ( clazz != null )
            {
                return clazz;
            }
        }

        return null;
    }

    /**
     * @param javaClass not null
     */
    private void addSinceClasses( JavaClass javaClass )
    {
        if ( sinceClasses == null )
        {
            sinceClasses = new ArrayList<>();
        }
        sinceClasses.add( javaClass.getFullyQualifiedName() );
    }

    private boolean sinceClassesContains( JavaClass javaClass )
    {
        return sinceClasses.contains( javaClass.getFullyQualifiedName() );
    }

    // ----------------------------------------------------------------------
    // Static methods
    // ----------------------------------------------------------------------

    /**
     * Write content into the given javaFile and using the given encoding.
     * All line separators will be unified.
     *
     * @param javaFile not null
     * @param encoding not null
     * @param content  not null
     * @throws IOException if any
     */
    private static void writeFile( final File javaFile, final String encoding, final String content )
        throws IOException
    {
        String unified = StringUtils.unifyLineSeparators( content );
        FileUtils.fileWrite( javaFile, encoding, unified );
    }

    /**
     * @return the full clirr goal, i.e. <code>groupId:artifactId:version:goal</code>. The clirr-plugin version
     *         could be load from the pom.properties in the clirr-maven-plugin dependency.
     */
    private static String getFullClirrGoal()
    {
        StringBuilder sb = new StringBuilder();

        sb.append( CLIRR_MAVEN_PLUGIN_GROUPID ).append( ":" ).append( CLIRR_MAVEN_PLUGIN_ARTIFACTID ).append( ":" );

        String clirrVersion = CLIRR_MAVEN_PLUGIN_VERSION;
        
        String resource = "META-INF/maven/" + CLIRR_MAVEN_PLUGIN_GROUPID + "/" + CLIRR_MAVEN_PLUGIN_ARTIFACTID
                        + "/pom.properties";
         
        try ( InputStream resourceAsStream =
            AbstractFixJavadocMojo.class.getClassLoader().getResourceAsStream( resource ) )
        {

            if ( resourceAsStream != null )
            {
                Properties properties = new Properties();
                properties.load( resourceAsStream );
                if ( StringUtils.isNotEmpty( properties.getProperty( "version" ) ) )
                {
                    clirrVersion = properties.getProperty( "version" );
                }
            }
        }
        catch ( IOException e )
        {
            // nop
        }

        sb.append( clirrVersion ).append( ":" ).append( CLIRR_MAVEN_PLUGIN_GOAL );

        return sb.toString();
    }

    /**
     * Default comment for class.
     *
     * @param javaClass not null
     * @return a default comment for class.
     */
    private static String getDefaultClassJavadocComment( final JavaClass javaClass )
    {
        StringBuilder sb = new StringBuilder();

        sb.append( "<p>" );
        if ( javaClass.isAbstract() )
        {
            sb.append( "Abstract " );
        }

        sb.append( javaClass.getName() );

        if ( !javaClass.isInterface() )
        {
            sb.append( " class." );
        }
        else
        {
            sb.append( " interface." );
        }

        sb.append( "</p>" );

        return sb.toString();
    }

    /**
     * Default comment for method with taking care of getter/setter in the javaMethod name.
     *
     * @param javaExecutable not null
     * @return a default comment for method
     */
    private static String getDefaultMethodJavadocComment( final JavaExecutable javaExecutable )
    {
        if ( javaExecutable instanceof JavaConstructor )
        {
            return "<p>Constructor for " + javaExecutable.getName() + ".</p>";
        }
        
        if ( javaExecutable.getName().length() > 3 && ( javaExecutable.getName().startsWith( "get" )
            || javaExecutable.getName().startsWith( "set" ) ) )
        {
            String field = StringUtils.lowercaseFirstLetter( javaExecutable.getName().substring( 3 ) );

            JavaClass clazz = javaExecutable.getDeclaringClass();

            if ( clazz.getFieldByName( field ) == null )
            {
                return "<p>" + javaExecutable.getName() + ".</p>";
            }

            StringBuilder sb = new StringBuilder();

            sb.append( "<p>" );
            if ( javaExecutable.getName().startsWith( "get" ) )
            {
                sb.append( "Getter " );
            }
            else if ( javaExecutable.getName().startsWith( "set" ) )
            {
                sb.append( "Setter " );
            }
            sb.append( "for the field <code>" ).append( field ).append( "</code>.</p>" );

            return sb.toString();
        }

        return "<p>" + javaExecutable.getName() + ".</p>";
    }

    /**
     * Try to find if a Javadoc comment has an {@link #INHERITED_TAG} for instance:
     * <pre>
     * &#47;&#42;&#42; {&#64;inheritDoc} &#42;&#47;
     * </pre>
     * or
     * <pre>
     * &#47;&#42;&#42;
     * &#32;&#42; {&#64;inheritDoc}
     * &#32;&#42;&#47;
     * </pre>
     *
     * @param content not null
     * @return <code>true</code> if the content has an inherited tag, <code>false</code> otherwise.
     */
    private static boolean hasInheritedTag( final String content )
    {
        final String inheritedTagPattern =
            "^\\s*(\\/\\*\\*)?(\\s*(\\*)?)*(\\{)@inheritDoc\\s*(\\})(\\s*(\\*)?)*(\\*\\/)?$";
        return Pattern.matches( inheritedTagPattern, StringUtils.removeDuplicateWhitespace( content ) );
    }

    /**
     * Workaround for QDOX-146 about whitespace.
     * Ideally we want to use <code>entity.getComment()</code>
     * <br/>
     * For instance, with the following snippet:
     * <br/>
     * <p/>
     * <code>
     * <font color="#808080">1</font>&nbsp;<font color="#ffffff"></font><br />
     * <font color="#808080">2</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;</font>
     * <font color="#3f5fbf">&#47;&#42;&#42;</font><br />
     * <font color="#808080">3</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</font>
     * <font color="#3f5fbf">&#42;&nbsp;Dummy&nbsp;Javadoc&nbsp;comment.</font><br />
     * <font color="#808080">4</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</font>
     * <font color="#3f5fbf">&#42;&nbsp;</font><font color="#7f9fbf">@param&nbsp;</font>
     * <font color="#3f5fbf">s&nbsp;a&nbsp;String</font><br />
     * <font color="#808080">5</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</font>
     * <font color="#3f5fbf">&#42;&#47;</font><br />
     * <font color="#808080">6</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;</font>
     * <font color="#7f0055"><b>public&nbsp;</b></font><font color="#7f0055"><b>void&nbsp;</b></font>
     * <font color="#000000">dummyMethod</font><font color="#000000">(&nbsp;</font>
     * <font color="#000000">String&nbsp;s&nbsp;</font><font color="#000000">){}</font><br />
     * </code>
     * <p/>
     * <br/>
     * The return will be:
     * <br/>
     * <p/>
     * <code>
     * <font color="#808080">1</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</font>
     * <font color="#3f5fbf">&#42;&nbsp;Dummy&nbsp;Javadoc&nbsp;comment.</font><br />
     * </code>
     *
     * @param javaClassContent original class content not null
     * @param entity           not null
     * @return the javadoc comment for the entity without any tags.
     * @throws IOException if any
     */
    static String getJavadocComment( final String javaClassContent, final JavaAnnotatedElement entity )
        throws IOException
    {
        if ( entity.getComment() == null )
        {
            return "";
        }

        String originalJavadoc = extractOriginalJavadocContent( javaClassContent, entity );

        StringBuilder sb = new StringBuilder();
        BufferedReader lr = new BufferedReader( new StringReader( originalJavadoc ) );
        String line;
        while ( ( line = lr.readLine() ) != null )
        {
            String l = StringUtils.removeDuplicateWhitespace( line.trim() );
            if ( l.startsWith( "* @" ) || l.startsWith( "*@" ) )
            {
                break;
            }
            sb.append( line ).append( EOL );
        }

        return trimRight( sb.toString() );
    }

    /**
     * Work around for QDOX-146 about whitespace.
     * Ideally we want to use <code>docletTag.getValue()</code>
     * <br/>
     * For instance, with the following snippet:
     * <br/>
     * <p/>
     * <code>
     * <font color="#808080">1</font>&nbsp;<font color="#ffffff"></font><br />
     * <font color="#808080">2</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;</font>
     * <font color="#3f5fbf">&#47;&#42;&#42;</font><br />
     * <font color="#808080">3</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</font>
     * <font color="#3f5fbf">&#42;&nbsp;Dummy&nbsp;Javadoc&nbsp;comment.</font><br />
     * <font color="#808080">4</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</font>
     * <font color="#3f5fbf">&#42;&nbsp;</font><font color="#7f9fbf">@param&nbsp;</font>
     * <font color="#3f5fbf">s&nbsp;a&nbsp;String</font><br />
     * <font color="#808080">5</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</font>
     * <font color="#3f5fbf">&#42;&#47;</font><br />
     * <font color="#808080">6</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;</font>
     * <font color="#7f0055"><b>public&nbsp;</b></font><font color="#7f0055"><b>void&nbsp;</b></font>
     * <font color="#000000">dummyMethod</font><font color="#000000">(&nbsp;</font>
     * <font color="#000000">String&nbsp;s&nbsp;</font><font color="#000000">){}</font><br />
     * </code>
     * <p/>
     * <br/>
     * The return will be:
     * <br/>
     * <p/>
     * <code>
     * <font color="#808080">1</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</font>
     * <font color="#3f5fbf">&#42;&nbsp;</font><font color="#7f9fbf">@param&nbsp;</font>
     * <font color="#3f5fbf">s&nbsp;a&nbsp;String</font><br />
     * </code>
     *
     * @param javaClassContent original class content not null
     * @param entity           not null
     * @param docletTag        not null
     * @return the javadoc comment for the entity without Javadoc tags.
     * @throws IOException if any
     */
    String getJavadocComment( final String javaClassContent, final JavaAnnotatedElement entity,
                                      final DocletTag docletTag )
        throws IOException
    {
        if ( docletTag.getValue() == null || docletTag.getParameters().isEmpty() )
        {
            return "";
        }

        String originalJavadoc = extractOriginalJavadocContent( javaClassContent, entity );

        StringBuilder sb = new StringBuilder();
        BufferedReader lr = new BufferedReader( new StringReader( originalJavadoc ) );
        String line;
        boolean found = false;
        
        // matching first line of doclettag
        Pattern p = Pattern.compile( "(\\s*\\*\\s?@" + docletTag.getName() + ")\\s+"
            + "(\\Q" + docletTag.getValue().split( "\r\n|\r|\n" )[0] + "\\E)" );
        
        while ( ( line = lr.readLine() ) != null )
        {
            Matcher m = p.matcher( line );
            if ( m.matches() )
            {
                if ( fixTag( LINK_TAG ) )
                {
                    line = replaceLinkTags( line, entity );
                }
                sb.append( line ).append( EOL );
                found = true;
            }
            else
            {
                if ( line.trim().startsWith( "* @" ) || line.trim().startsWith( "*@" ) )
                {
                    found = false;
                }
                if ( found )
                {
                    if ( fixTag( LINK_TAG ) )
                    {
                        line = replaceLinkTags( line, entity );
                    }
                    sb.append( line ).append( EOL );
                }
            }
        }

        return trimRight( sb.toString() );
    }

    /**
     * Extract the original Javadoc and others comments up to {@link #START_JAVADOC} form the entity. This method
     * takes care of the Javadoc indentation. All javadoc lines will be trimmed on right.
     * <br/>
     * For instance, with the following snippet:
     * <br/>
     * <p/>
     * <code>
     * <font color="#808080">1</font>&nbsp;<font color="#ffffff"></font><br />
     * <font color="#808080">2</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;</font>
     * <font color="#3f5fbf">&#47;&#42;&#42;</font><br />
     * <font color="#808080">3</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</font>
     * <font color="#3f5fbf">&#42;&nbsp;Dummy&nbsp;Javadoc&nbsp;comment.</font><br />
     * <font color="#808080">4</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</font>
     * <font color="#3f5fbf">&#42;&nbsp;</font><font color="#7f9fbf">@param&nbsp;</font>
     * <font color="#3f5fbf">s&nbsp;a&nbsp;String</font><br />
     * <font color="#808080">5</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</font>
     * <font color="#3f5fbf">&#42;&#47;</font><br />
     * <font color="#808080">6</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;</font>
     * <font color="#7f0055"><b>public&nbsp;</b></font><font color="#7f0055"><b>void&nbsp;</b></font>
     * <font color="#000000">dummyMethod</font><font color="#000000">(&nbsp;</font>
     * <font color="#000000">String&nbsp;s&nbsp;</font><font color="#000000">){}</font><br />
     * </code>
     * <p/>
     * <br/>
     * The return will be:
     * <br/>
     * <p/>
     * <code>
     * <font color="#808080">1</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;</font>
     * <font color="#3f5fbf">&#47;&#42;&#42;</font><br />
     * <font color="#808080">2</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</font>
     * <font color="#3f5fbf">&#42;&nbsp;Dummy&nbsp;Javadoc&nbsp;comment.</font><br />
     * <font color="#808080">3</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</font>
     * <font color="#3f5fbf">&#42;&nbsp;</font><font color="#7f9fbf">@param&nbsp;</font>
     * <font color="#3f5fbf">s&nbsp;a&nbsp;String</font><br />
     * <font color="#808080">4</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</font>
     * <font color="#3f5fbf">&#42;&#47;</font><br />
     * </code>
     *
     * @param javaClassContent not null
     * @param entity           not null
     * @return return the original javadoc as String for the current entity
     * @throws IOException if any
     */
    static String extractOriginalJavadoc( final String javaClassContent, final JavaAnnotatedElement entity )
        throws IOException
    {
        if ( entity.getComment() == null )
        {
            return "";
        }

        String[] javaClassContentLines = getLines( javaClassContent );
        List<String> list = new LinkedList<>();
        for ( int i = entity.getLineNumber() - 2; i >= 0; i-- )
        {
            String line = javaClassContentLines[i];

            list.add( trimRight( line ) );
            if ( line.trim().startsWith( START_JAVADOC ) )
            {
                break;
            }
        }

        Collections.reverse( list );

        return StringUtils.join( list.iterator(), EOL );
    }

    /**
     * Extract the Javadoc comment between {@link #START_JAVADOC} and {@link #END_JAVADOC} form the entity. This method
     * takes care of the Javadoc indentation. All javadoc lines will be trimmed on right.
     * <br/>
     * For instance, with the following snippet:
     * <br/>
     * <p/>
     * <code>
     * <font color="#808080">1</font>&nbsp;<font color="#ffffff"></font><br />
     * <font color="#808080">2</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;</font>
     * <font color="#3f5fbf">&#47;&#42;&#42;</font><br />
     * <font color="#808080">3</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</font>
     * <font color="#3f5fbf">&#42;&nbsp;Dummy&nbsp;Javadoc&nbsp;comment.</font><br />
     * <font color="#808080">4</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</font>
     * <font color="#3f5fbf">&#42;&nbsp;</font><font color="#7f9fbf">@param&nbsp;</font>
     * <font color="#3f5fbf">s&nbsp;a&nbsp;String</font><br />
     * <font color="#808080">5</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</font>
     * <font color="#3f5fbf">&#42;&#47;</font><br />
     * <font color="#808080">6</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;</font>
     * <font color="#7f0055"><b>public&nbsp;</b></font><font color="#7f0055"><b>void&nbsp;</b></font>
     * <font color="#000000">dummyMethod</font><font color="#000000">(&nbsp;</font>
     * <font color="#000000">String&nbsp;s&nbsp;</font><font color="#000000">){}</font><br />
     * </code>
     * <p/>
     * <br/>
     * The return will be:
     * <br/>
     * <p/>
     * <code>
     * <font color="#808080">1</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</font>
     * <font color="#3f5fbf">&#42;&nbsp;Dummy&nbsp;Javadoc&nbsp;comment.</font><br />
     * <font color="#808080">2</font>&nbsp;<font color="#ffffff">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</font>
     * <font color="#3f5fbf">&#42;&nbsp;</font><font color="#7f9fbf">@param&nbsp;</font>
     * <font color="#3f5fbf">s&nbsp;a&nbsp;String</font><br />
     * </code>
     *
     * @param javaClassContent not null
     * @param entity           not null
     * @return return the original javadoc as String for the current entity
     * @throws IOException if any
     */
    static String extractOriginalJavadocContent( final String javaClassContent,
                                                         final JavaAnnotatedElement entity )
        throws IOException
    {
        if ( entity.getComment() == null )
        {
            return "";
        }

        String originalJavadoc = extractOriginalJavadoc( javaClassContent, entity );
        int index = originalJavadoc.indexOf( START_JAVADOC );
        if ( index != -1 )
        {
            originalJavadoc = originalJavadoc.substring( index + START_JAVADOC.length() );
        }
        index = originalJavadoc.indexOf( END_JAVADOC );
        if ( index != -1 )
        {
            originalJavadoc = originalJavadoc.substring( 0, index );
        }
        if ( originalJavadoc.startsWith( "\r\n" ) )
        {
            originalJavadoc = originalJavadoc.substring( 2 );
        }
        else if ( originalJavadoc.startsWith( "\n" ) || originalJavadoc.startsWith( "\r" ) )
        {
            originalJavadoc = originalJavadoc.substring( 1 );
        }

        return trimRight( originalJavadoc );
    }

    /**
     * @param content not null
     * @return the content without last lines containing javadoc separator (ie <code> * </code>)
     * @throws IOException if any
     * @see #getJavadocComment(String, JavaAnnotatedElement, DocletTag)
     */
    private static String removeLastEmptyJavadocLines( final String content )
        throws IOException
    {
        if ( !content.contains( EOL ) )
        {
            return content;
        }

        String[] lines = getLines( content );
        if ( lines.length == 1 )
        {
            return content;
        }

        List<String> linesList = new LinkedList<>( Arrays.asList( lines ) );

        Collections.reverse( linesList );

        for ( Iterator<String> it = linesList.iterator(); it.hasNext(); )
        {
            String line = it.next();

            if ( line.trim().equals( "*" ) )
            {
                it.remove();
            }
            else
            {
                break;
            }
        }

        Collections.reverse( linesList );

        return StringUtils.join( linesList.iterator(), EOL );
    }

    /**
     * @param content not null
     * @return the javadoc comment with the given indentation
     * @throws IOException if any
     * @see #getJavadocComment(String, JavaAnnotatedElement, DocletTag)
     */
    private static String alignIndentationJavadocLines( final String content, final String indent )
        throws IOException
    {
        StringBuilder sb = new StringBuilder();
        for ( String line : getLines( content ) )
        {
            if ( sb.length() > 0 )
            {
                sb.append( EOL );
            }
            if ( !line.trim().startsWith( "*" ) )
            {
                line = "*" + line;
            }
            sb.append( indent ).append( " " ).append( trimLeft( line ) );
        }

        return sb.toString();
    }

    /**
     * Autodetect the indentation of a given line:
     * <pre>
     * autodetectIndentation( null ) = "";
     * autodetectIndentation( "a" ) = "";
     * autodetectIndentation( "    a" ) = "    ";
     * autodetectIndentation( "\ta" ) = "\t";
     * </pre>
     *
     * @param line not null
     * @return the indentation for the given line.
     */
    private static String autodetectIndentation( final String line )
    {
        if ( StringUtils.isEmpty( line ) )
        {
            return "";
        }

        return line.substring( 0, line.indexOf( trimLeft( line ) ) );
    }

    /**
     * @param content not null
     * @return an array of all content lines
     * @throws IOException if any
     */
    private static String[] getLines( final String content )
        throws IOException
    {
        List<String> lines = new LinkedList<>();

        BufferedReader reader = new BufferedReader( new StringReader( content ) );
        String line = reader.readLine();
        while ( line != null )
        {
            lines.add( line );
            line = reader.readLine();
        }

        return lines.toArray( new String[lines.size()] );
    }

    /**
     * Trim a given line on the left:
     * <pre>
     * trimLeft( null ) = "";
     * trimLeft( "  " ) = "";
     * trimLeft( "a" ) = "a";
     * trimLeft( "    a" ) = "a";
     * trimLeft( "\ta" ) = "a";
     * trimLeft( "    a    " ) = "a    ";
     * </pre>
     *
     * @param text
     * @return the text trimmed on left side or empty if text is null.
     */
    private static String trimLeft( final String text )
    {
        if ( StringUtils.isEmpty( text ) || StringUtils.isEmpty( text.trim() ) )
        {
            return "";
        }

        String textTrimmed = text.trim();
        return text.substring( text.indexOf( textTrimmed ) );
    }

    /**
     * Trim a given line on the right:
     * <pre>
     * trimRight( null ) = "";
     * trimRight( "  " ) = "";
     * trimRight( "a" ) = "a";
     * trimRight( "a\t" ) = "a";
     * trimRight( "    a    " ) = "    a";
     * </pre>
     *
     * @param text
     * @return the text trimmed on tight side or empty if text is null.
     */
    private static String trimRight( final String text )
    {
        if ( StringUtils.isEmpty( text ) || StringUtils.isEmpty( text.trim() ) )
        {
            return "";
        }

        String textTrimmed = text.trim();
        return text.substring( 0, text.indexOf( textTrimmed ) + textTrimmed.length() );
    }


    /**
     * Wrapper class for the entity's tags.
     */
    class JavaEntityTags
    {
        private final JavaAnnotatedElement entity;

        private final boolean isJavaMethod;

        /**
         * List of tag names.
         */
        private List<String> namesTags;

        /**
         * Map with java parameter as key and original Javadoc lines as values.
         */
        private Map<String, String> tagParams;
        
        private Set<String> documentedParams = new HashSet<>();

        /**
         * Original javadoc lines.
         */
        private String tagReturn;

        /**
         * Map with java throw as key and original Javadoc lines as values.
         */
        private Map<String, String> tagThrows;

        /**
         * Original javadoc lines for unknown tags.
         */
        private List<String> unknownsTags;

        JavaEntityTags( JavaAnnotatedElement entity, boolean isJavaMethod )
        {
            this.entity = entity;
            this.isJavaMethod = isJavaMethod;
            this.namesTags = new LinkedList<>();
            this.tagParams = new LinkedHashMap<>();
            this.tagThrows = new LinkedHashMap<>();
            this.unknownsTags = new LinkedList<>();
        }

        public List<String> getNamesTags()
        {
            return namesTags;
        }

        public String getJavadocReturnTag()
        {
            return tagReturn;
        }

        public void setJavadocReturnTag( String s )
        {
            tagReturn = s;
        }

        public List<String> getUnknownTags()
        {
            return unknownsTags;
        }

        public void putJavadocParamTag( String paramName, String paramValue, String originalJavadocTag )
        {
            documentedParams.add( paramName );
            tagParams.put( paramValue, originalJavadocTag );
        }

        public String getJavadocParamTag( String paramValue )
        {
            String originalJavadocTag = tagParams.get( paramValue );
            if ( originalJavadocTag == null && getLog().isWarnEnabled() )
            {
                getLog().warn( getMessage( paramValue, "javaEntityTags.tagParams" ) );
            }
            return originalJavadocTag;
        }

        public boolean hasJavadocParamTag( String paramName )
        {
            return documentedParams.contains( paramName );
        }

        public void putJavadocThrowsTag( String paramName, String originalJavadocTag )
        {
            tagThrows.put( paramName, originalJavadocTag );
        }

        public String getJavadocThrowsTag( String paramName )
        {
            return getJavadocThrowsTag( paramName, false );
        }

        public String getJavadocThrowsTag( String paramName, boolean nullable )
        {
            String originalJavadocTag = tagThrows.get( paramName );
            if ( !nullable && originalJavadocTag == null && getLog().isWarnEnabled() )
            {
                getLog().warn( getMessage( paramName, "javaEntityTags.tagThrows" ) );
            }

            return originalJavadocTag;
        }

        private String getMessage( String paramName, String mapName )
        {
            StringBuilder msg = new StringBuilder();
            msg.append( "No param '" ).append( paramName ).append( "' key found in " ).append( mapName )
            .append( " for the entity: " );
            if ( isJavaMethod )
            {
                JavaMethod javaMethod = (JavaMethod) entity;
                msg.append( getJavaMethodAsString( javaMethod ) );
            }
            else
            {
                JavaClass javaClass = (JavaClass) entity;
                msg.append( javaClass.getFullyQualifiedName() );
            }

            return msg.toString();
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public String toString()
        {
            StringBuilder sb = new StringBuilder();

            sb.append( "namesTags=" ).append( namesTags ).append( "\n" );
            sb.append( "tagParams=" ).append( tagParams ).append( "\n" );
            sb.append( "tagReturn=" ).append( tagReturn ).append( "\n" );
            sb.append( "tagThrows=" ).append( tagThrows ).append( "\n" );
            sb.append( "unknownsTags=" ).append( unknownsTags ).append( "\n" );

            return sb.toString();
        }
    }
}
