/*
 * 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.
 */
package org.apache.maven.tools.plugin.extractor.annotations;

import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;

import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.stream.Collectors;

import com.thoughtworks.qdox.JavaProjectBuilder;
import com.thoughtworks.qdox.library.SortedClassLibraryBuilder;
import com.thoughtworks.qdox.model.DocletTag;
import com.thoughtworks.qdox.model.JavaAnnotatedElement;
import com.thoughtworks.qdox.model.JavaClass;
import com.thoughtworks.qdox.model.JavaField;
import com.thoughtworks.qdox.model.JavaMember;
import com.thoughtworks.qdox.model.JavaMethod;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.versioning.ComparableVersion;
import org.apache.maven.plugin.descriptor.InvalidParameterException;
import org.apache.maven.plugin.descriptor.InvalidPluginDescriptorException;
import org.apache.maven.plugin.descriptor.MojoDescriptor;
import org.apache.maven.plugin.descriptor.PluginDescriptor;
import org.apache.maven.plugin.descriptor.Requirement;
import org.apache.maven.project.MavenProject;
import org.apache.maven.tools.plugin.ExtendedMojoDescriptor;
import org.apache.maven.tools.plugin.PluginToolsRequest;
import org.apache.maven.tools.plugin.extractor.ExtractionException;
import org.apache.maven.tools.plugin.extractor.GroupKey;
import org.apache.maven.tools.plugin.extractor.MojoDescriptorExtractor;
import org.apache.maven.tools.plugin.extractor.annotations.converter.ConverterContext;
import org.apache.maven.tools.plugin.extractor.annotations.converter.JavaClassConverterContext;
import org.apache.maven.tools.plugin.extractor.annotations.converter.JavadocBlockTagsToXhtmlConverter;
import org.apache.maven.tools.plugin.extractor.annotations.converter.JavadocInlineTagsToXhtmlConverter;
import org.apache.maven.tools.plugin.extractor.annotations.datamodel.ComponentAnnotationContent;
import org.apache.maven.tools.plugin.extractor.annotations.datamodel.ExecuteAnnotationContent;
import org.apache.maven.tools.plugin.extractor.annotations.datamodel.MojoAnnotationContent;
import org.apache.maven.tools.plugin.extractor.annotations.datamodel.ParameterAnnotationContent;
import org.apache.maven.tools.plugin.extractor.annotations.scanner.MojoAnnotatedClass;
import org.apache.maven.tools.plugin.extractor.annotations.scanner.MojoAnnotationsScanner;
import org.apache.maven.tools.plugin.extractor.annotations.scanner.MojoAnnotationsScannerRequest;
import org.apache.maven.tools.plugin.javadoc.JavadocLinkGenerator;
import org.apache.maven.tools.plugin.util.PluginUtils;
import org.codehaus.plexus.archiver.ArchiverException;
import org.codehaus.plexus.archiver.UnArchiver;
import org.codehaus.plexus.archiver.manager.ArchiverManager;
import org.codehaus.plexus.archiver.manager.NoSuchArchiverException;
import org.codehaus.plexus.logging.AbstractLogEnabled;
import org.codehaus.plexus.util.StringUtils;
import org.eclipse.aether.RepositorySystem;
import org.eclipse.aether.artifact.DefaultArtifact;
import org.eclipse.aether.resolution.ArtifactRequest;
import org.eclipse.aether.resolution.ArtifactResolutionException;
import org.eclipse.aether.resolution.ArtifactResult;
import org.objectweb.asm.Opcodes;

/**
 * JavaMojoDescriptorExtractor, a MojoDescriptor extractor to read descriptors from java classes with annotations.
 * Notice that source files are also parsed to get description, since and deprecation information.
 *
 * @author Olivier Lamy
 * @since 3.0
 */
@Named(JavaAnnotationsMojoDescriptorExtractor.NAME)
@Singleton
public class JavaAnnotationsMojoDescriptorExtractor extends AbstractLogEnabled implements MojoDescriptorExtractor {
    public static final String NAME = "java-annotations";

    private static final GroupKey GROUP_KEY = new GroupKey(GroupKey.JAVA_GROUP, 100);

    /**
     *
     * @see <a href="https://docs.oracle.com/javase/specs/jvms/se19/html/jvms-4.html#jvms-4.1">JVMS 4.1</a>
     */
    private static final Map<Integer, String> CLASS_VERSION_TO_JAVA_STRING;

    static {
        CLASS_VERSION_TO_JAVA_STRING = new HashMap<>();
        CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V1_1, "1.1");
        CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V1_2, "1.2");
        CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V1_3, "1.3");
        CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V1_4, "1.4");
        CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V1_5, "1.5");
        CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V1_6, "1.6");
        CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V1_7, "1.7");
        CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V1_8, "1.8");
        CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V9, "9");
        CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V10, "10");
        CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V11, "11");
        CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V12, "12");
        CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V13, "13");
        CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V14, "14");
        CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V15, "15");
        CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V16, "16");
        CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V17, "17");
        CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V18, "18");
        CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V19, "19");
        CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V20, "20");
        CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V21, "21");
        CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V22, "22");
    }

    @Inject
    MojoAnnotationsScanner mojoAnnotationsScanner;

    @Inject
    private RepositorySystem repositorySystem;

    @Inject
    private ArchiverManager archiverManager;

    @Inject
    private JavadocInlineTagsToXhtmlConverter javadocInlineTagsToHtmlConverter;

    @Inject
    private JavadocBlockTagsToXhtmlConverter javadocBlockTagsToHtmlConverter;

    @Override
    public String getName() {
        return NAME;
    }

    @Override
    public boolean isDeprecated() {
        return false; // this is the "current way" to write Java Mojos
    }

    @Override
    public GroupKey getGroupKey() {
        return GROUP_KEY;
    }

    /**
     * Compares class file format versions.
     * @see <a href="https://docs.oracle.com/javase/specs/jvms/se19/html/jvms-4.html#jvms-4.1">JVMS 4.1</a>
     *
     */
    @SuppressWarnings("checkstyle:magicnumber")
    static final class ClassVersionComparator implements Comparator<Integer> {
        @Override
        public int compare(Integer classVersion1, Integer classVersion2) {
            // first compare major version (
            int result = Integer.compare(classVersion1 & 0x00FF, classVersion2 & 0x00FF);
            if (result == 0) {
                // compare minor version if major is equal
                result = Integer.compare(classVersion1, classVersion2);
            }
            return result;
        }
    }

    @Override
    public List<MojoDescriptor> execute(PluginToolsRequest request)
            throws ExtractionException, InvalidPluginDescriptorException {
        Map<String, MojoAnnotatedClass> mojoAnnotatedClasses = scanAnnotations(request);

        Optional<Integer> maxClassVersion = mojoAnnotatedClasses.values().stream()
                .map(MojoAnnotatedClass::getClassVersion)
                .max(new ClassVersionComparator());
        if (maxClassVersion.isPresent()) {
            String requiredJavaVersion = CLASS_VERSION_TO_JAVA_STRING.get(maxClassVersion.get());
            if (StringUtils.isBlank(request.getRequiredJavaVersion())
                    || new ComparableVersion(request.getRequiredJavaVersion())
                                    .compareTo(new ComparableVersion(requiredJavaVersion))
                            < 0) {
                request.setRequiredJavaVersion(requiredJavaVersion);
            }
        }
        JavaProjectBuilder builder = scanJavadoc(request, mojoAnnotatedClasses.values());
        Map<String, JavaClass> javaClassesMap = discoverClasses(builder);

        final JavadocLinkGenerator linkGenerator;
        if (request.getInternalJavadocBaseUrl() != null
                || (request.getExternalJavadocBaseUrls() != null
                        && !request.getExternalJavadocBaseUrls().isEmpty())) {
            linkGenerator = new JavadocLinkGenerator(
                    request.getInternalJavadocBaseUrl(),
                    request.getInternalJavadocVersion(),
                    request.getExternalJavadocBaseUrls(),
                    request.getSettings());
        } else {
            linkGenerator = null;
        }

        populateDataFromJavadoc(builder, mojoAnnotatedClasses, javaClassesMap, linkGenerator);

        return toMojoDescriptors(mojoAnnotatedClasses, request.getPluginDescriptor());
    }

    private Map<String, MojoAnnotatedClass> scanAnnotations(PluginToolsRequest request) throws ExtractionException {
        MojoAnnotationsScannerRequest mojoAnnotationsScannerRequest = new MojoAnnotationsScannerRequest();

        File output = new File(request.getProject().getBuild().getOutputDirectory());
        mojoAnnotationsScannerRequest.setClassesDirectories(Arrays.asList(output));

        mojoAnnotationsScannerRequest.setDependencies(request.getDependencies());

        mojoAnnotationsScannerRequest.setProject(request.getProject());

        Map<String, MojoAnnotatedClass> result = mojoAnnotationsScanner.scan(mojoAnnotationsScannerRequest);
        request.setUsedMavenApiVersion(mojoAnnotationsScannerRequest.getMavenApiVersion());
        return result;
    }

    private JavaProjectBuilder scanJavadoc(
            PluginToolsRequest request, Collection<MojoAnnotatedClass> mojoAnnotatedClasses)
            throws ExtractionException {
        // found artifact from reactors to scan sources
        // we currently only scan sources from reactors
        List<MavenProject> mavenProjects = new ArrayList<>();

        // if we need to scan sources from external artifacts
        Set<Artifact> externalArtifacts = new HashSet<>();

        JavaProjectBuilder builder = new JavaProjectBuilder(new SortedClassLibraryBuilder());
        builder.setEncoding(request.getEncoding());
        extendJavaProjectBuilder(builder, request.getProject());

        for (MojoAnnotatedClass mojoAnnotatedClass : mojoAnnotatedClasses) {
            if (Objects.equals(
                    mojoAnnotatedClass.getArtifact().getArtifactId(),
                    request.getProject().getArtifact().getArtifactId())) {
                continue;
            }

            if (!isMojoAnnnotatedClassCandidate(mojoAnnotatedClass)) {
                // we don't scan sources for classes without mojo annotations
                continue;
            }

            MavenProject mavenProject =
                    getFromProjectReferences(mojoAnnotatedClass.getArtifact(), request.getProject());

            if (mavenProject != null) {
                mavenProjects.add(mavenProject);
            } else {
                externalArtifacts.add(mojoAnnotatedClass.getArtifact());
            }
        }

        // try to get artifact with sources classifier, extract somewhere then scan for @since, @deprecated
        for (Artifact artifact : externalArtifacts) {
            // parameter for test-sources too ?? olamy I need that for it test only
            if (StringUtils.equalsIgnoreCase("tests", artifact.getClassifier())) {
                extendJavaProjectBuilderWithSourcesJar(builder, artifact, request, "test-sources");
            } else {
                extendJavaProjectBuilderWithSourcesJar(builder, artifact, request, "sources");
            }
        }

        for (MavenProject mavenProject : mavenProjects) {
            extendJavaProjectBuilder(builder, mavenProject);
        }

        return builder;
    }

    private boolean isMojoAnnnotatedClassCandidate(MojoAnnotatedClass mojoAnnotatedClass) {
        return mojoAnnotatedClass != null && mojoAnnotatedClass.hasAnnotations();
    }

    /**
     * from sources scan to get @since and @deprecated and description of classes and fields.
     */
    protected void populateDataFromJavadoc(
            JavaProjectBuilder javaProjectBuilder,
            Map<String, MojoAnnotatedClass> mojoAnnotatedClasses,
            Map<String, JavaClass> javaClassesMap,
            JavadocLinkGenerator linkGenerator) {

        for (Map.Entry<String, MojoAnnotatedClass> entry : mojoAnnotatedClasses.entrySet()) {
            JavaClass javaClass = javaClassesMap.get(entry.getKey());
            if (javaClass == null) {
                continue;
            }
            // populate class-level content
            MojoAnnotationContent mojoAnnotationContent = entry.getValue().getMojo();
            if (mojoAnnotationContent != null) {
                JavaClassConverterContext context = new JavaClassConverterContext(
                        javaClass, javaProjectBuilder, mojoAnnotatedClasses, linkGenerator, javaClass.getLineNumber());
                mojoAnnotationContent.setDescription(getDescriptionFromElement(javaClass, context));

                DocletTag since = findInClassHierarchy(javaClass, "since");
                if (since != null) {
                    mojoAnnotationContent.setSince(getRawValueFromTaglet(since, context));
                }

                DocletTag deprecated = findInClassHierarchy(javaClass, "deprecated");
                if (deprecated != null) {
                    mojoAnnotationContent.setDeprecated(getRawValueFromTaglet(deprecated, context));
                }
            }

            Map<String, JavaAnnotatedElement> fieldsMap = extractFieldsAnnotations(javaClass, javaClassesMap);
            Map<String, JavaAnnotatedElement> methodsMap = extractMethodsAnnotations(javaClass, javaClassesMap);

            // populate parameters
            Map<String, ParameterAnnotationContent> parameters =
                    getParametersParentHierarchy(entry.getValue(), mojoAnnotatedClasses);
            parameters = new TreeMap<>(parameters);
            for (Map.Entry<String, ParameterAnnotationContent> parameter : parameters.entrySet()) {
                JavaAnnotatedElement element;
                if (parameter.getValue().isAnnotationOnMethod()) {
                    element = methodsMap.get(parameter.getKey());
                } else {
                    element = fieldsMap.get(parameter.getKey());
                }

                if (element == null) {
                    continue;
                }

                JavaClassConverterContext context = new JavaClassConverterContext(
                        javaClass, ((JavaMember) element).getDeclaringClass(),
                        javaProjectBuilder, mojoAnnotatedClasses,
                        linkGenerator, element.getLineNumber());
                ParameterAnnotationContent parameterAnnotationContent = parameter.getValue();
                parameterAnnotationContent.setDescription(getDescriptionFromElement(element, context));

                DocletTag deprecated = element.getTagByName("deprecated");
                if (deprecated != null) {
                    parameterAnnotationContent.setDeprecated(getRawValueFromTaglet(deprecated, context));
                }

                DocletTag since = element.getTagByName("since");
                if (since != null) {
                    parameterAnnotationContent.setSince(getRawValueFromTaglet(since, context));
                }
            }

            // populate components
            Map<String, ComponentAnnotationContent> components =
                    entry.getValue().getComponents();
            for (Map.Entry<String, ComponentAnnotationContent> component : components.entrySet()) {
                JavaAnnotatedElement element = fieldsMap.get(component.getKey());
                if (element == null) {
                    continue;
                }

                JavaClassConverterContext context = new JavaClassConverterContext(
                        javaClass, ((JavaMember) element).getDeclaringClass(),
                        javaProjectBuilder, mojoAnnotatedClasses,
                        linkGenerator, javaClass.getLineNumber());
                ComponentAnnotationContent componentAnnotationContent = component.getValue();
                componentAnnotationContent.setDescription(getDescriptionFromElement(element, context));

                DocletTag deprecated = element.getTagByName("deprecated");
                if (deprecated != null) {
                    componentAnnotationContent.setDeprecated(getRawValueFromTaglet(deprecated, context));
                }

                DocletTag since = element.getTagByName("since");
                if (since != null) {
                    componentAnnotationContent.setSince(getRawValueFromTaglet(since, context));
                }
            }
        }
    }

    /**
     * Returns the XHTML description from the given element.
     * This may refer to either goal, parameter or component.
     * @param element the element for which to generate the description
     * @param context the context with which to call the converter
     * @return the generated description
     */
    String getDescriptionFromElement(JavaAnnotatedElement element, JavaClassConverterContext context) {

        String comment = element.getComment();
        if (comment == null) {
            return null;
        }
        StringBuilder description = new StringBuilder(javadocInlineTagsToHtmlConverter.convert(comment, context));
        for (DocletTag docletTag : element.getTags()) {
            // also consider see block tags
            if ("see".equals(docletTag.getName())) {
                description.append(javadocBlockTagsToHtmlConverter.convert(docletTag, context));
            }
        }
        return description.toString();
    }

    String getRawValueFromTaglet(DocletTag docletTag, ConverterContext context) {
        // just resolve inline tags and convert to XHTML
        return javadocInlineTagsToHtmlConverter.convert(docletTag.getValue(), context);
    }

    /**
     * @param javaClass not null
     * @param tagName   not null
     * @return docletTag instance
     */
    private DocletTag findInClassHierarchy(JavaClass javaClass, String tagName) {
        try {
            DocletTag tag = javaClass.getTagByName(tagName);

            if (tag == null) {
                JavaClass superClass = javaClass.getSuperJavaClass();

                if (superClass != null) {
                    tag = findInClassHierarchy(superClass, tagName);
                }
            }

            return tag;
        } catch (NoClassDefFoundError e) {
            if (e.getMessage().replace('/', '.').contains(MojoAnnotationsScanner.V4_API_PLUGIN_PACKAGE)) {
                return null;
            }
            String str;
            try {
                str = javaClass.getFullyQualifiedName();
            } catch (Throwable t) {
                str = javaClass.getValue();
            }
            getLogger().warn("Failed extracting tag '" + tagName + "' from class " + str);
            throw (NoClassDefFoundError) new NoClassDefFoundError(e.getMessage()).initCause(e);
        }
    }

    /**
     * extract fields that are either parameters or components.
     *
     * @param javaClass not null
     * @return map with Mojo parameters names as keys
     */
    private Map<String, JavaAnnotatedElement> extractFieldsAnnotations(
            JavaClass javaClass, Map<String, JavaClass> javaClassesMap) {
        try {
            Map<String, JavaAnnotatedElement> rawParams = new TreeMap<>();

            // we have to add the parent fields first, so that they will be overwritten by the local fields if
            // that actually happens...
            JavaClass superClass = javaClass.getSuperJavaClass();

            if (superClass != null) {
                if (!superClass.getFields().isEmpty()) {
                    rawParams = extractFieldsAnnotations(superClass, javaClassesMap);
                }
                // maybe sources comes from scan of sources artifact
                superClass = javaClassesMap.get(superClass.getFullyQualifiedName());
                if (superClass != null && !superClass.getFields().isEmpty()) {
                    rawParams = extractFieldsAnnotations(superClass, javaClassesMap);
                }
            } else {

                rawParams = new TreeMap<>();
            }

            for (JavaField field : javaClass.getFields()) {
                rawParams.put(field.getName(), field);
            }

            return rawParams;
        } catch (NoClassDefFoundError e) {
            getLogger().warn("Failed extracting parameters from " + javaClass);
            throw e;
        }
    }

    /**
     * extract methods that are parameters.
     *
     * @param javaClass not null
     * @return map with Mojo parameters names as keys
     */
    private Map<String, JavaAnnotatedElement> extractMethodsAnnotations(
            JavaClass javaClass, Map<String, JavaClass> javaClassesMap) {
        try {
            Map<String, JavaAnnotatedElement> rawParams = new TreeMap<>();

            // we have to add the parent methods first, so that they will be overwritten by the local methods if
            // that actually happens...
            JavaClass superClass = javaClass.getSuperJavaClass();

            if (superClass != null) {
                if (!superClass.getMethods().isEmpty()) {
                    rawParams = extractMethodsAnnotations(superClass, javaClassesMap);
                }
                // maybe sources comes from scan of sources artifact
                superClass = javaClassesMap.get(superClass.getFullyQualifiedName());
                if (superClass != null && !superClass.getMethods().isEmpty()) {
                    rawParams = extractMethodsAnnotations(superClass, javaClassesMap);
                }
            } else {

                rawParams = new TreeMap<>();
            }

            for (JavaMethod method : javaClass.getMethods()) {
                if (isPublicSetterMethod(method)) {
                    rawParams.put(
                            StringUtils.lowercaseFirstLetter(method.getName().substring(3)), method);
                }
            }

            return rawParams;
        } catch (NoClassDefFoundError e) {
            if (e.getMessage().replace('/', '.').contains(MojoAnnotationsScanner.V4_API_PLUGIN_PACKAGE)) {
                return new TreeMap<>();
            }
            String str;
            try {
                str = javaClass.getFullyQualifiedName();
            } catch (Throwable t) {
                str = javaClass.getValue();
            }
            getLogger().warn("Failed extracting methods from " + str);
            throw (NoClassDefFoundError) new NoClassDefFoundError(e.getMessage()).initCause(e);
        }
    }

    private boolean isPublicSetterMethod(JavaMethod method) {
        return method.isPublic()
                && !method.isStatic()
                && method.getName().length() > 3
                && (method.getName().startsWith("add") || method.getName().startsWith("set"))
                && "void".equals(method.getReturnType().getValue())
                && method.getParameters().size() == 1;
    }

    protected Map<String, JavaClass> discoverClasses(JavaProjectBuilder builder) {
        Collection<JavaClass> javaClasses = builder.getClasses();

        if (javaClasses == null || javaClasses.size() < 1) {
            return Collections.emptyMap();
        }

        Map<String, JavaClass> javaClassMap = new HashMap<>(javaClasses.size());

        for (JavaClass javaClass : javaClasses) {
            javaClassMap.put(javaClass.getFullyQualifiedName(), javaClass);
        }

        return javaClassMap;
    }

    protected void extendJavaProjectBuilderWithSourcesJar(
            JavaProjectBuilder builder, Artifact artifact, PluginToolsRequest request, String classifier)
            throws ExtractionException {
        try {
            org.eclipse.aether.artifact.Artifact sourcesArtifact = new DefaultArtifact(
                    artifact.getGroupId(),
                    artifact.getArtifactId(),
                    classifier,
                    artifact.getArtifactHandler().getExtension(),
                    artifact.getVersion());

            ArtifactRequest resolveRequest =
                    new ArtifactRequest(sourcesArtifact, request.getProject().getRemoteProjectRepositories(), null);
            try {
                ArtifactResult result = repositorySystem.resolveArtifact(request.getRepoSession(), resolveRequest);
                sourcesArtifact = result.getArtifact();
            } catch (ArtifactResolutionException e) {
                String message = "Unable to get sources artifact for " + artifact.getId()
                        + ". Some javadoc tags (@since, @deprecated and comments) won't be used";
                if (getLogger().isDebugEnabled()) {
                    getLogger().warn(message, e);
                } else {
                    getLogger().warn(message);
                }
                return;
            }

            if (sourcesArtifact.getFile() == null || !sourcesArtifact.getFile().exists()) {
                // could not get artifact sources
                return;
            }

            if (sourcesArtifact.getFile().isFile()) {
                // extract sources to target/maven-plugin-plugin-sources/${groupId}/${artifact}/sources
                File extractDirectory = new File(
                        request.getProject().getBuild().getDirectory(),
                        "maven-plugin-plugin-sources/" + sourcesArtifact.getGroupId() + "/"
                                + sourcesArtifact.getArtifactId() + "/" + sourcesArtifact.getVersion()
                                + "/" + sourcesArtifact.getClassifier());
                extractDirectory.mkdirs();

                UnArchiver unArchiver = archiverManager.getUnArchiver("jar");
                unArchiver.setSourceFile(sourcesArtifact.getFile());
                unArchiver.setDestDirectory(extractDirectory);
                unArchiver.extract();

                extendJavaProjectBuilder(builder, Arrays.asList(extractDirectory), request.getDependencies());
            } else if (sourcesArtifact.getFile().isDirectory()) {
                extendJavaProjectBuilder(builder, Arrays.asList(sourcesArtifact.getFile()), request.getDependencies());
            }
        } catch (ArchiverException | NoSuchArchiverException e) {
            throw new ExtractionException(e.getMessage(), e);
        }
    }

    private void extendJavaProjectBuilder(JavaProjectBuilder builder, final MavenProject project) {
        List<File> sources = new ArrayList<>();

        for (String source : project.getCompileSourceRoots()) {
            sources.add(new File(source));
        }

        // TODO be more dynamic
        File generatedPlugin = new File(project.getBasedir(), "target/generated-sources/plugin");
        if (!project.getCompileSourceRoots().contains(generatedPlugin.getAbsolutePath()) && generatedPlugin.exists()) {
            sources.add(generatedPlugin);
        }
        extendJavaProjectBuilder(builder, sources, project.getArtifacts());
    }

    private void extendJavaProjectBuilder(
            JavaProjectBuilder builder, List<File> sourceDirectories, Set<Artifact> artifacts) {

        // Build isolated Classloader with only the artifacts of the project (none of this plugin)
        List<URL> urls = new ArrayList<>(artifacts.size());
        for (Artifact artifact : artifacts) {
            try {
                urls.add(artifact.getFile().toURI().toURL());
            } catch (MalformedURLException e) {
                // noop
            }
        }
        builder.addClassLoader(new URLClassLoader(urls.toArray(new URL[0]), ClassLoader.getSystemClassLoader()));

        for (File source : sourceDirectories) {
            builder.addSourceTree(source);
        }
    }

    private List<MojoDescriptor> toMojoDescriptors(
            Map<String, MojoAnnotatedClass> mojoAnnotatedClasses, PluginDescriptor pluginDescriptor)
            throws InvalidPluginDescriptorException {
        List<MojoDescriptor> mojoDescriptors = new ArrayList<>(mojoAnnotatedClasses.size());
        for (MojoAnnotatedClass mojoAnnotatedClass : mojoAnnotatedClasses.values()) {
            // no mojo so skip it
            if (mojoAnnotatedClass.getMojo() == null) {
                continue;
            }

            ExtendedMojoDescriptor mojoDescriptor = new ExtendedMojoDescriptor(true);

            // mojoDescriptor.setRole( mojoAnnotatedClass.getClassName() );
            // mojoDescriptor.setRoleHint( "default" );
            mojoDescriptor.setImplementation(mojoAnnotatedClass.getClassName());
            mojoDescriptor.setLanguage("java");

            mojoDescriptor.setV4Api(mojoAnnotatedClass.isV4Api());

            MojoAnnotationContent mojo = mojoAnnotatedClass.getMojo();

            mojoDescriptor.setDescription(mojo.getDescription());
            mojoDescriptor.setSince(mojo.getSince());
            mojo.setDeprecated(mojo.getDeprecated());

            mojoDescriptor.setProjectRequired(mojo.requiresProject());

            mojoDescriptor.setRequiresReports(mojo.requiresReports());

            mojoDescriptor.setComponentConfigurator(mojo.configurator());

            mojoDescriptor.setInheritedByDefault(mojo.inheritByDefault());

            mojoDescriptor.setInstantiationStrategy(mojo.instantiationStrategy().id());

            mojoDescriptor.setAggregator(mojo.aggregator());
            mojoDescriptor.setDependencyResolutionRequired(
                    mojo.requiresDependencyResolution().id());
            mojoDescriptor.setDependencyCollectionRequired(
                    mojo.requiresDependencyCollection().id());

            mojoDescriptor.setDirectInvocationOnly(mojo.requiresDirectInvocation());
            mojoDescriptor.setDeprecated(mojo.getDeprecated());
            mojoDescriptor.setThreadSafe(mojo.threadSafe());

            MojoAnnotatedClass mojoAnnotatedClassWithExecute =
                    findClassWithExecuteAnnotationInParentHierarchy(mojoAnnotatedClass, mojoAnnotatedClasses);
            if (mojoAnnotatedClassWithExecute != null && mojoAnnotatedClassWithExecute.getExecute() != null) {
                ExecuteAnnotationContent execute = mojoAnnotatedClassWithExecute.getExecute();
                mojoDescriptor.setExecuteGoal(execute.goal());
                mojoDescriptor.setExecuteLifecycle(execute.lifecycle());
                if (execute.phase() != null) {
                    mojoDescriptor.setExecutePhase(execute.phase().id());
                    if (StringUtils.isNotEmpty(execute.customPhase())) {
                        throw new InvalidPluginDescriptorException(
                                "@Execute annotation must only use either 'phase' "
                                        + "or 'customPhase' but not both. Both are used though on "
                                        + mojoAnnotatedClassWithExecute.getClassName(),
                                null);
                    }
                } else if (StringUtils.isNotEmpty(execute.customPhase())) {
                    mojoDescriptor.setExecutePhase(execute.customPhase());
                }
            }

            mojoDescriptor.setExecutionStrategy(mojo.executionStrategy());
            // ???
            // mojoDescriptor.alwaysExecute(mojo.a)

            mojoDescriptor.setGoal(mojo.name());
            mojoDescriptor.setOnlineRequired(mojo.requiresOnline());

            mojoDescriptor.setPhase(mojo.defaultPhase().id());

            // Parameter annotations
            Map<String, ParameterAnnotationContent> parameters =
                    getParametersParentHierarchy(mojoAnnotatedClass, mojoAnnotatedClasses);

            for (ParameterAnnotationContent parameterAnnotationContent : new TreeSet<>(parameters.values())) {
                org.apache.maven.plugin.descriptor.Parameter parameter =
                        new org.apache.maven.plugin.descriptor.Parameter();
                String name = StringUtils.isEmpty(parameterAnnotationContent.name())
                        ? parameterAnnotationContent.getFieldName()
                        : parameterAnnotationContent.name();
                parameter.setName(name);
                parameter.setAlias(parameterAnnotationContent.alias());
                parameter.setDefaultValue(parameterAnnotationContent.defaultValue());
                parameter.setDeprecated(parameterAnnotationContent.getDeprecated());
                parameter.setDescription(parameterAnnotationContent.getDescription());
                parameter.setEditable(!parameterAnnotationContent.readonly());
                String property = parameterAnnotationContent.property();
                if (StringUtils.contains(property, '$')
                        || StringUtils.contains(property, '{')
                        || StringUtils.contains(property, '}')) {
                    throw new InvalidParameterException(
                            "Invalid property for parameter '" + parameter.getName() + "', "
                                    + "forbidden characters ${}: " + property,
                            null);
                }
                parameter.setExpression((property == null || property.isEmpty()) ? "" : "${" + property + "}");
                StringBuilder type = new StringBuilder(parameterAnnotationContent.getClassName());
                if (!parameterAnnotationContent.getTypeParameters().isEmpty()) {
                    type.append(parameterAnnotationContent.getTypeParameters().stream()
                            .collect(Collectors.joining(", ", "<", ">")));
                }
                parameter.setType(type.toString());
                parameter.setSince(parameterAnnotationContent.getSince());
                parameter.setRequired(parameterAnnotationContent.required());

                mojoDescriptor.addParameter(parameter);
            }

            // Component annotations
            Map<String, ComponentAnnotationContent> components =
                    getComponentsParentHierarchy(mojoAnnotatedClass, mojoAnnotatedClasses);

            for (ComponentAnnotationContent componentAnnotationContent : new TreeSet<>(components.values())) {
                org.apache.maven.plugin.descriptor.Parameter parameter =
                        new org.apache.maven.plugin.descriptor.Parameter();
                parameter.setName(componentAnnotationContent.getFieldName());

                // recognize Maven-injected objects as components annotations instead of parameters
                String expression = PluginUtils.MAVEN_COMPONENTS.get(componentAnnotationContent.getRoleClassName());
                if (expression == null) {
                    // normal component
                    parameter.setRequirement(new Requirement(
                            componentAnnotationContent.getRoleClassName(), componentAnnotationContent.hint()));
                } else {
                    // not a component but a Maven object to be transformed into an expression/property: deprecated
                    getLogger()
                            .warn("Deprecated @Component annotation for '" + parameter.getName() + "' field in "
                                    + mojoAnnotatedClass.getClassName()
                                    + ": replace with @Parameter( defaultValue = \"" + expression
                                    + "\", readonly = true )");
                    parameter.setDefaultValue(expression);
                    parameter.setType(componentAnnotationContent.getRoleClassName());
                    parameter.setRequired(true);
                }
                parameter.setDeprecated(componentAnnotationContent.getDeprecated());
                parameter.setSince(componentAnnotationContent.getSince());

                // same behaviour as JavaMojoDescriptorExtractor
                // parameter.setRequired( ... );
                parameter.setEditable(false);

                mojoDescriptor.addParameter(parameter);
            }

            mojoDescriptor.setPluginDescriptor(pluginDescriptor);

            mojoDescriptors.add(mojoDescriptor);
        }
        return mojoDescriptors;
    }

    protected MojoAnnotatedClass findClassWithExecuteAnnotationInParentHierarchy(
            MojoAnnotatedClass mojoAnnotatedClass, Map<String, MojoAnnotatedClass> mojoAnnotatedClasses) {
        if (mojoAnnotatedClass.getExecute() != null) {
            return mojoAnnotatedClass;
        }
        String parentClassName = mojoAnnotatedClass.getParentClassName();
        if (parentClassName == null || parentClassName.isEmpty()) {
            return null;
        }
        MojoAnnotatedClass parent = mojoAnnotatedClasses.get(parentClassName);
        if (parent == null) {
            return null;
        }
        return findClassWithExecuteAnnotationInParentHierarchy(parent, mojoAnnotatedClasses);
    }

    protected Map<String, ParameterAnnotationContent> getParametersParentHierarchy(
            MojoAnnotatedClass mojoAnnotatedClass, Map<String, MojoAnnotatedClass> mojoAnnotatedClasses) {
        List<ParameterAnnotationContent> parameterAnnotationContents = new ArrayList<>();

        parameterAnnotationContents =
                getParametersParent(mojoAnnotatedClass, parameterAnnotationContents, mojoAnnotatedClasses);

        // move to parent first to build the Map
        Collections.reverse(parameterAnnotationContents);

        Map<String, ParameterAnnotationContent> map = new HashMap<>(parameterAnnotationContents.size());

        for (ParameterAnnotationContent parameterAnnotationContent : parameterAnnotationContents) {
            map.put(parameterAnnotationContent.getFieldName(), parameterAnnotationContent);
        }
        return map;
    }

    protected List<ParameterAnnotationContent> getParametersParent(
            MojoAnnotatedClass mojoAnnotatedClass,
            List<ParameterAnnotationContent> parameterAnnotationContents,
            Map<String, MojoAnnotatedClass> mojoAnnotatedClasses) {
        parameterAnnotationContents.addAll(mojoAnnotatedClass.getParameters().values());
        String parentClassName = mojoAnnotatedClass.getParentClassName();
        if (parentClassName != null) {
            MojoAnnotatedClass parent = mojoAnnotatedClasses.get(parentClassName);
            if (parent != null) {
                return getParametersParent(parent, parameterAnnotationContents, mojoAnnotatedClasses);
            }
        }
        return parameterAnnotationContents;
    }

    protected Map<String, ComponentAnnotationContent> getComponentsParentHierarchy(
            MojoAnnotatedClass mojoAnnotatedClass, Map<String, MojoAnnotatedClass> mojoAnnotatedClasses) {
        List<ComponentAnnotationContent> componentAnnotationContents = new ArrayList<>();

        componentAnnotationContents =
                getComponentParent(mojoAnnotatedClass, componentAnnotationContents, mojoAnnotatedClasses);

        // move to parent first to build the Map
        Collections.reverse(componentAnnotationContents);

        Map<String, ComponentAnnotationContent> map = new HashMap<>(componentAnnotationContents.size());

        for (ComponentAnnotationContent componentAnnotationContent : componentAnnotationContents) {
            map.put(componentAnnotationContent.getFieldName(), componentAnnotationContent);
        }
        return map;
    }

    protected List<ComponentAnnotationContent> getComponentParent(
            MojoAnnotatedClass mojoAnnotatedClass,
            List<ComponentAnnotationContent> componentAnnotationContents,
            Map<String, MojoAnnotatedClass> mojoAnnotatedClasses) {
        componentAnnotationContents.addAll(mojoAnnotatedClass.getComponents().values());
        String parentClassName = mojoAnnotatedClass.getParentClassName();
        if (parentClassName != null) {
            MojoAnnotatedClass parent = mojoAnnotatedClasses.get(parentClassName);
            if (parent != null) {
                return getComponentParent(parent, componentAnnotationContents, mojoAnnotatedClasses);
            }
        }
        return componentAnnotationContents;
    }

    protected MavenProject getFromProjectReferences(Artifact artifact, MavenProject project) {
        if (project.getProjectReferences() == null
                || project.getProjectReferences().isEmpty()) {
            return null;
        }
        Collection<MavenProject> mavenProjects = project.getProjectReferences().values();
        for (MavenProject mavenProject : mavenProjects) {
            if (Objects.equals(mavenProject.getId(), artifact.getId())) {
                return mavenProject;
            }
        }
        return null;
    }
}
