/*
 * 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.project;

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

import java.io.File;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.function.Supplier;
import java.util.stream.Collectors;

import org.apache.maven.ProjectCycleException;
import org.apache.maven.RepositoryUtils;
import org.apache.maven.api.feature.Features;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.InvalidArtifactRTException;
import org.apache.maven.artifact.InvalidRepositoryException;
import org.apache.maven.artifact.repository.ArtifactRepository;
import org.apache.maven.bridge.MavenRepositorySystem;
import org.apache.maven.internal.impl.InternalSession;
import org.apache.maven.model.Build;
import org.apache.maven.model.Dependency;
import org.apache.maven.model.DependencyManagement;
import org.apache.maven.model.DeploymentRepository;
import org.apache.maven.model.Extension;
import org.apache.maven.model.Model;
import org.apache.maven.model.Plugin;
import org.apache.maven.model.Profile;
import org.apache.maven.model.ReportPlugin;
import org.apache.maven.model.building.ArtifactModelSource;
import org.apache.maven.model.building.DefaultModelBuilder;
import org.apache.maven.model.building.DefaultModelBuildingRequest;
import org.apache.maven.model.building.DefaultModelProblem;
import org.apache.maven.model.building.FileModelSource;
import org.apache.maven.model.building.ModelBuilder;
import org.apache.maven.model.building.ModelBuildingException;
import org.apache.maven.model.building.ModelBuildingRequest;
import org.apache.maven.model.building.ModelBuildingResult;
import org.apache.maven.model.building.ModelProblem;
import org.apache.maven.model.building.ModelProcessor;
import org.apache.maven.model.building.ModelSource;
import org.apache.maven.model.building.StringModelSource;
import org.apache.maven.model.building.TransformerContext;
import org.apache.maven.model.building.TransformerContextBuilder;
import org.apache.maven.model.resolution.ModelResolver;
import org.apache.maven.model.root.RootLocator;
import org.apache.maven.repository.internal.ArtifactDescriptorUtils;
import org.apache.maven.repository.internal.ModelCacheFactory;
import org.apache.maven.utils.Os;
import org.eclipse.aether.RepositorySystem;
import org.eclipse.aether.RepositorySystemSession;
import org.eclipse.aether.RequestTrace;
import org.eclipse.aether.impl.RemoteRepositoryManager;
import org.eclipse.aether.repository.LocalRepositoryManager;
import org.eclipse.aether.repository.RemoteRepository;
import org.eclipse.aether.repository.WorkspaceRepository;
import org.eclipse.aether.resolution.ArtifactRequest;
import org.eclipse.aether.resolution.ArtifactResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * DefaultProjectBuilder
 */
@Named
@Singleton
public class DefaultProjectBuilder implements ProjectBuilder {
    public static final String BUILDER_PARALLELISM = "maven.projectBuilder.parallelism";
    public static final int DEFAULT_BUILDER_PARALLELISM = 4;

    private final Logger logger = LoggerFactory.getLogger(getClass());
    private final ModelBuilder modelBuilder;
    private final ModelProcessor modelProcessor;
    private final ProjectBuildingHelper projectBuildingHelper;
    private final MavenRepositorySystem repositorySystem;
    private final org.eclipse.aether.RepositorySystem repoSystem;
    private final RemoteRepositoryManager repositoryManager;
    private final ProjectDependenciesResolver dependencyResolver;
    private final ModelCacheFactory modelCacheFactory;

    private final RootLocator rootLocator;

    @SuppressWarnings("checkstyle:ParameterNumber")
    @Inject
    public DefaultProjectBuilder(
            ModelBuilder modelBuilder,
            ModelProcessor modelProcessor,
            ProjectBuildingHelper projectBuildingHelper,
            MavenRepositorySystem repositorySystem,
            RepositorySystem repoSystem,
            RemoteRepositoryManager repositoryManager,
            ProjectDependenciesResolver dependencyResolver,
            ModelCacheFactory modelCacheFactory,
            RootLocator rootLocator) {
        this.modelBuilder = modelBuilder;
        this.modelProcessor = modelProcessor;
        this.projectBuildingHelper = projectBuildingHelper;
        this.repositorySystem = repositorySystem;
        this.repoSystem = repoSystem;
        this.repositoryManager = repositoryManager;
        this.dependencyResolver = dependencyResolver;
        this.modelCacheFactory = modelCacheFactory;
        this.rootLocator = rootLocator;
    }
    // ----------------------------------------------------------------------
    // MavenProjectBuilder Implementation
    // ----------------------------------------------------------------------

    @Override
    public ProjectBuildingResult build(File pomFile, ProjectBuildingRequest request) throws ProjectBuildingException {
        try (BuildSession bs = new BuildSession(request, false)) {
            return bs.build(pomFile, new FileModelSource(pomFile));
        }
    }

    @Override
    public ProjectBuildingResult build(ModelSource modelSource, ProjectBuildingRequest request)
            throws ProjectBuildingException {
        try (BuildSession bs = new BuildSession(request, false)) {
            return bs.build(null, modelSource);
        }
    }

    @Override
    public ProjectBuildingResult build(Artifact artifact, ProjectBuildingRequest request)
            throws ProjectBuildingException {
        return build(artifact, false, request);
    }

    @Override
    public ProjectBuildingResult build(Artifact artifact, boolean allowStubModel, ProjectBuildingRequest request)
            throws ProjectBuildingException {
        try (BuildSession bs = new BuildSession(request, false)) {
            return bs.build(artifact, allowStubModel);
        }
    }

    @Override
    public List<ProjectBuildingResult> build(List<File> pomFiles, boolean recursive, ProjectBuildingRequest request)
            throws ProjectBuildingException {
        try (BuildSession bs = new BuildSession(request, true)) {
            return bs.build(pomFiles, recursive);
        }
    }

    static class InterimResult {

        File pomFile;

        ModelBuildingRequest request;

        ModelBuildingResult result;

        MavenProject project;

        boolean root;

        List<InterimResult> modules = Collections.emptyList();

        ProjectBuildingResult projectBuildingResult;

        InterimResult(
                File pomFile,
                ModelBuildingRequest request,
                ModelBuildingResult result,
                MavenProject project,
                boolean root) {
            this.pomFile = pomFile;
            this.request = request;
            this.result = result;
            this.project = project;
            this.root = root;
        }

        InterimResult(ModelBuildingRequest request, ProjectBuildingResult projectBuildingResult) {
            this.request = request;
            this.projectBuildingResult = projectBuildingResult;
            this.pomFile = projectBuildingResult.getPomFile();
            this.project = projectBuildingResult.getProject();
        }
    }

    class BuildSession implements AutoCloseable {
        private final ProjectBuildingRequest request;
        private final RepositorySystemSession session;
        private final List<RemoteRepository> repositories;
        private final ReactorModelPool modelPool;
        private final TransformerContextBuilder transformerContextBuilder;
        private final ForkJoinPool forkJoinPool;

        BuildSession(ProjectBuildingRequest request, boolean localProjects) {
            this.request = request;
            this.session =
                    RepositoryUtils.overlay(request.getLocalRepository(), request.getRepositorySession(), repoSystem);
            this.repositories = RepositoryUtils.toRepos(request.getRemoteRepositories());
            this.forkJoinPool = new ForkJoinPool(getParallelism(request));
            if (localProjects) {
                this.modelPool = new ReactorModelPool();
                this.transformerContextBuilder = modelBuilder.newTransformerContextBuilder();
            } else {
                this.modelPool = null;
                this.transformerContextBuilder = null;
            }
        }

        @Override
        public void close() {
            this.forkJoinPool.shutdownNow();
        }

        private int getParallelism(ProjectBuildingRequest request) {
            int parallelism = DEFAULT_BUILDER_PARALLELISM;
            try {
                String str = request.getUserProperties().getProperty(BUILDER_PARALLELISM);
                if (str == null) {
                    str = request.getSystemProperties().getProperty(BUILDER_PARALLELISM);
                }
                if (str != null) {
                    parallelism = Integer.parseInt(str);
                }
            } catch (Exception e) {
                // ignore
            }
            return Math.max(1, Math.min(parallelism, Runtime.getRuntime().availableProcessors()));
        }

        ProjectBuildingResult build(File pomFile, ModelSource modelSource) throws ProjectBuildingException {
            ClassLoader oldContextClassLoader = Thread.currentThread().getContextClassLoader();

            try {
                MavenProject project = request.getProject();

                List<ModelProblem> modelProblems = null;
                Throwable error = null;

                if (project == null) {
                    ModelBuildingRequest request = getModelBuildingRequest();

                    project = new MavenProject();
                    project.setFile(pomFile);

                    DefaultModelBuildingListener listener =
                            new DefaultModelBuildingListener(project, projectBuildingHelper, this.request);
                    request.setModelBuildingListener(listener);

                    request.setPomFile(pomFile);
                    request.setModelSource(modelSource);
                    request.setLocationTracking(true);

                    if (pomFile != null) {
                        project.setRootDirectory(
                                rootLocator.findRoot(pomFile.getParentFile().toPath()));
                    }

                    ModelBuildingResult result;
                    try {
                        result = modelBuilder.build(request);
                    } catch (ModelBuildingException e) {
                        result = e.getResult();
                        if (result == null || result.getEffectiveModel() == null) {
                            throw new ProjectBuildingException(e.getModelId(), e.getMessage(), pomFile, e);
                        }
                        // validation error, continue project building and delay failing to help IDEs
                        error = e;
                    }

                    modelProblems = result.getProblems();

                    initProject(project, Collections.emptyMap(), result);
                } else if (request.isResolveDependencies()) {
                    projectBuildingHelper.selectProjectRealm(project);
                }

                DependencyResolutionResult resolutionResult = null;

                if (request.isResolveDependencies()) {
                    resolutionResult = resolveDependencies(project);
                }

                ProjectBuildingResult result =
                        new DefaultProjectBuildingResult(project, modelProblems, resolutionResult);

                if (error != null) {
                    ProjectBuildingException e = new ProjectBuildingException(Arrays.asList(result));
                    e.initCause(error);
                    throw e;
                }

                return result;
            } finally {
                Thread.currentThread().setContextClassLoader(oldContextClassLoader);
            }
        }

        ProjectBuildingResult build(Artifact artifact, boolean allowStubModel) throws ProjectBuildingException {
            org.eclipse.aether.artifact.Artifact pomArtifact = RepositoryUtils.toArtifact(artifact);
            pomArtifact = ArtifactDescriptorUtils.toPomArtifact(pomArtifact);

            boolean localProject;

            try {
                ArtifactRequest pomRequest = new ArtifactRequest();
                pomRequest.setArtifact(pomArtifact);
                pomRequest.setRepositories(repositories);
                ArtifactResult pomResult = repoSystem.resolveArtifact(session, pomRequest);

                pomArtifact = pomResult.getArtifact();
                localProject = pomResult.getRepository() instanceof WorkspaceRepository;
            } catch (org.eclipse.aether.resolution.ArtifactResolutionException e) {
                if (e.getResults().get(0).isMissing() && allowStubModel) {
                    return build(null, createStubModelSource(artifact));
                }
                throw new ProjectBuildingException(
                        artifact.getId(), "Error resolving project artifact: " + e.getMessage(), e);
            }

            File pomFile = pomArtifact.getFile();

            if ("pom".equals(artifact.getType())) {
                artifact.selectVersion(pomArtifact.getVersion());
                artifact.setFile(pomFile);
                artifact.setResolved(true);
            }

            if (localProject) {
                return build(pomFile, new FileModelSource(pomFile));
            } else {
                return build(
                        null,
                        new ArtifactModelSource(
                                pomFile, artifact.getGroupId(), artifact.getArtifactId(), artifact.getVersion()));
            }
        }

        List<ProjectBuildingResult> build(List<File> pomFiles, boolean recursive) throws ProjectBuildingException {
            ForkJoinTask<List<ProjectBuildingResult>> task = forkJoinPool.submit(() -> doBuild(pomFiles, recursive));

            // ForkJoinTask.getException rewraps the exception in a weird way
            // which cause an additional layer of exception, so try to unwrap it
            task.quietlyJoin();
            if (task.isCompletedAbnormally()) {
                Throwable e = task.getException();
                Throwable c = e.getCause();
                uncheckedThrow(c != null && c.getClass() == e.getClass() ? c : e);
            }

            List<ProjectBuildingResult> results = task.getRawResult();
            if (results.stream()
                    .flatMap(r -> r.getProblems().stream())
                    .anyMatch(p -> p.getSeverity() != ModelProblem.Severity.WARNING)) {
                ModelProblem cycle = results.stream()
                        .flatMap(r -> r.getProblems().stream())
                        .filter(p -> p.getException() instanceof CycleDetectedException)
                        .findAny()
                        .orElse(null);
                if (cycle != null) {
                    throw new RuntimeException(new ProjectCycleException(
                            "The projects in the reactor contain a cyclic reference: " + cycle.getMessage(),
                            (CycleDetectedException) cycle.getException()));
                }
                throw new ProjectBuildingException(results);
            }

            return results;
        }

        List<ProjectBuildingResult> doBuild(List<File> pomFiles, boolean recursive) {
            Map<File, MavenProject> projectIndex = new ConcurrentHashMap<>(256);

            // phase 1: get file Models from the reactor.
            List<InterimResult> interimResults = build(projectIndex, pomFiles, new LinkedHashSet<>(), true, recursive);

            ClassLoader oldContextClassLoader = Thread.currentThread().getContextClassLoader();

            try {
                // Phase 2: get effective models from the reactor
                List<ProjectBuildingResult> results = build(projectIndex, interimResults);

                if (Features.buildConsumer(request.getUserProperties())) {
                    request.getRepositorySession()
                            .getData()
                            .set(TransformerContext.KEY, transformerContextBuilder.build());
                }

                return results;
            } finally {
                Thread.currentThread().setContextClassLoader(oldContextClassLoader);
            }
        }

        @SuppressWarnings("checkstyle:parameternumber")
        private List<InterimResult> build(
                Map<File, MavenProject> projectIndex,
                List<File> pomFiles,
                Set<File> aggregatorFiles,
                boolean root,
                boolean recursive) {
            List<ForkJoinTask<InterimResult>> tasks = pomFiles.stream()
                    .map(pomFile -> ForkJoinTask.adapt(
                            () -> build(projectIndex, pomFile, concat(aggregatorFiles, pomFile), root, recursive)))
                    .collect(Collectors.toList());

            return ForkJoinTask.invokeAll(tasks).stream()
                    .map(ForkJoinTask::getRawResult)
                    .collect(Collectors.toList());
        }

        private <T> Set<T> concat(Set<T> set, T elem) {
            Set<T> newSet = new HashSet<>(set);
            newSet.add(elem);
            return newSet;
        }

        @SuppressWarnings("checkstyle:parameternumber")
        private InterimResult build(
                Map<File, MavenProject> projectIndex,
                File pomFile,
                Set<File> aggregatorFiles,
                boolean isRoot,
                boolean recursive) {
            MavenProject project = new MavenProject();
            project.setFile(pomFile);

            project.setRootDirectory(
                    rootLocator.findRoot(pomFile.getParentFile().toPath()));

            ModelBuildingRequest modelBuildingRequest = getModelBuildingRequest()
                    .setPomFile(pomFile)
                    .setTwoPhaseBuilding(true)
                    .setLocationTracking(true);

            DefaultModelBuildingListener listener =
                    new DefaultModelBuildingListener(project, projectBuildingHelper, request);
            modelBuildingRequest.setModelBuildingListener(listener);

            ModelBuildingResult result;
            try {
                result = modelBuilder.build(modelBuildingRequest);
            } catch (ModelBuildingException e) {
                result = e.getResult();
                if (result == null || result.getFileModel() == null) {
                    return new InterimResult(
                            modelBuildingRequest,
                            new DefaultProjectBuildingResult(e.getModelId(), pomFile, e.getProblems()));
                }
                // validation error, continue project building and delay failing to help IDEs
                // result.getProblems().addAll(e.getProblems()) ?
            }

            Model model = modelBuildingRequest.getFileModel();

            modelPool.put(model.getPomFile().toPath(), model);

            InterimResult interimResult = new InterimResult(pomFile, modelBuildingRequest, result, project, isRoot);

            if (recursive) {
                File basedir = pomFile.getParentFile();
                List<File> moduleFiles = new ArrayList<>();
                for (String module : model.getModules()) {
                    if (module == null || module.isEmpty()) {
                        continue;
                    }

                    module = module.replace('\\', File.separatorChar).replace('/', File.separatorChar);

                    File moduleFile = modelProcessor.locateExistingPom(new File(basedir, module));

                    if (moduleFile == null) {
                        ModelProblem problem = new DefaultModelProblem(
                                "Child module " + moduleFile + " of " + pomFile + " does not exist",
                                ModelProblem.Severity.ERROR,
                                ModelProblem.Version.BASE,
                                model,
                                -1,
                                -1,
                                null);
                        result.getProblems().add(problem);

                        continue;
                    }

                    if (Os.IS_WINDOWS) {
                        // we don't canonicalize on unix to avoid interfering with symlinks
                        try {
                            moduleFile = moduleFile.getCanonicalFile();
                        } catch (IOException e) {
                            moduleFile = moduleFile.getAbsoluteFile();
                        }
                    } else {
                        moduleFile = new File(moduleFile.toURI().normalize());
                    }

                    if (aggregatorFiles.contains(moduleFile)) {
                        StringBuilder buffer = new StringBuilder(256);
                        for (File aggregatorFile : aggregatorFiles) {
                            buffer.append(aggregatorFile).append(" -> ");
                        }
                        buffer.append(moduleFile);

                        ModelProblem problem = new DefaultModelProblem(
                                "Child module " + moduleFile + " of " + pomFile + " forms aggregation cycle " + buffer,
                                ModelProblem.Severity.ERROR,
                                ModelProblem.Version.BASE,
                                model,
                                -1,
                                -1,
                                null);
                        result.getProblems().add(problem);

                        continue;
                    }

                    moduleFiles.add(moduleFile);
                }

                if (!moduleFiles.isEmpty()) {
                    interimResult.modules = build(projectIndex, moduleFiles, aggregatorFiles, false, recursive);
                }
            }

            projectIndex.put(pomFile, project);

            return interimResult;
        }

        private List<ProjectBuildingResult> build(
                Map<File, MavenProject> projectIndex, List<InterimResult> interimResults) {
            // The transformation may need to access dependencies raw models,
            // which may cause some re-entrance in the build() method and can
            // actually cause deadlocks.  In order to workaround the problem,
            // we do a first pass by reading all rawModels in order.
            if (modelBuilder instanceof DefaultModelBuilder) {
                List<ProjectBuildingResult> results = new ArrayList<>();
                DefaultModelBuilder dmb = (DefaultModelBuilder) modelBuilder;
                boolean failure = false;
                for (InterimResult r : interimResults) {
                    DefaultProjectBuildingResult res;
                    try {
                        Model model = dmb.buildRawModel(r.request);
                        res = new DefaultProjectBuildingResult(model.getId(), model.getPomFile(), null);
                    } catch (ModelBuildingException e) {
                        failure = true;
                        res = new DefaultProjectBuildingResult(e.getModelId(), r.request.getPomFile(), e.getProblems());
                    }
                    results.add(res);
                }
                if (failure) {
                    return results;
                }
            }

            return interimResults.parallelStream()
                    .map(interimResult -> doBuild(projectIndex, interimResult))
                    .flatMap(List::stream)
                    .collect(Collectors.toList());
        }

        private List<ProjectBuildingResult> doBuild(Map<File, MavenProject> projectIndex, InterimResult interimResult) {
            if (interimResult.projectBuildingResult != null) {
                return Collections.singletonList(interimResult.projectBuildingResult);
            }
            MavenProject project = interimResult.project;
            try {
                ModelBuildingResult result = modelBuilder.build(interimResult.request, interimResult.result);

                // 2nd pass of initialization: resolve and build parent if necessary
                try {
                    initProject(project, projectIndex, result);
                } catch (InvalidArtifactRTException iarte) {
                    result.getProblems()
                            .add(new DefaultModelProblem(
                                    null,
                                    ModelProblem.Severity.ERROR,
                                    null,
                                    result.getEffectiveModel(),
                                    -1,
                                    -1,
                                    iarte));
                }

                List<ProjectBuildingResult> results = build(projectIndex, interimResult.modules);

                project.setExecutionRoot(interimResult.root);
                project.setCollectedProjects(
                        results.stream().map(ProjectBuildingResult::getProject).collect(Collectors.toList()));
                DependencyResolutionResult resolutionResult = null;
                if (request.isResolveDependencies()) {
                    resolutionResult = resolveDependencies(project);
                }

                results.add(new DefaultProjectBuildingResult(project, result.getProblems(), resolutionResult));

                return results;
            } catch (ModelBuildingException e) {
                DefaultProjectBuildingResult result;
                if (project == null || interimResult.result.getEffectiveModel() == null) {
                    result = new DefaultProjectBuildingResult(e.getModelId(), interimResult.pomFile, e.getProblems());
                } else {
                    project.setModel(interimResult.result.getEffectiveModel());
                    result = new DefaultProjectBuildingResult(project, e.getProblems(), null);
                }
                return Collections.singletonList(result);
            }
        }

        @SuppressWarnings("checkstyle:methodlength")
        private void initProject(MavenProject project, Map<File, MavenProject> projects, ModelBuildingResult result) {
            project.setModel(result.getEffectiveModel());
            project.setOriginalModel(result.getFileModel());

            initParent(project, projects, result);

            Artifact projectArtifact = repositorySystem.createArtifact(
                    project.getGroupId(), project.getArtifactId(), project.getVersion(), null, project.getPackaging());
            project.setArtifact(projectArtifact);

            // only set those on 2nd phase, ignore on 1st pass
            if (project.getFile() != null) {
                Build build = project.getBuild();
                project.addScriptSourceRoot(build.getScriptSourceDirectory());
                project.addCompileSourceRoot(build.getSourceDirectory());
                project.addTestCompileSourceRoot(build.getTestSourceDirectory());
            }

            List<Profile> activeProfiles = new ArrayList<>();
            activeProfiles.addAll(
                    result.getActivePomProfiles(result.getModelIds().get(0)));
            activeProfiles.addAll(result.getActiveExternalProfiles());
            project.setActiveProfiles(activeProfiles);

            project.setInjectedProfileIds("external", getProfileIds(result.getActiveExternalProfiles()));
            for (String modelId : result.getModelIds()) {
                project.setInjectedProfileIds(modelId, getProfileIds(result.getActivePomProfiles(modelId)));
            }

            //
            // All the parts that were taken out of MavenProject for Maven 4.0.0
            //

            project.setProjectBuildingRequest(request);

            // pluginArtifacts
            Set<Artifact> pluginArtifacts = new HashSet<>();
            for (Plugin plugin : project.getBuildPlugins()) {
                Artifact artifact = repositorySystem.createPluginArtifact(plugin);

                if (artifact != null) {
                    pluginArtifacts.add(artifact);
                }
            }
            project.setPluginArtifacts(pluginArtifacts);

            // reportArtifacts
            Set<Artifact> reportArtifacts = new HashSet<>();
            for (ReportPlugin report : project.getReportPlugins()) {
                Plugin pp = new Plugin();
                pp.setGroupId(report.getGroupId());
                pp.setArtifactId(report.getArtifactId());
                pp.setVersion(report.getVersion());

                Artifact artifact = repositorySystem.createPluginArtifact(pp);

                if (artifact != null) {
                    reportArtifacts.add(artifact);
                }
            }
            project.setReportArtifacts(reportArtifacts);

            // extensionArtifacts
            Set<Artifact> extensionArtifacts = new HashSet<>();
            List<Extension> extensions = project.getBuildExtensions();
            if (extensions != null) {
                for (Extension ext : extensions) {
                    String version;
                    if (ext.getVersion() == null || ext.getVersion().isEmpty()) {
                        version = "RELEASE";
                    } else {
                        version = ext.getVersion();
                    }

                    Artifact artifact = repositorySystem.createArtifact(
                            ext.getGroupId(), ext.getArtifactId(), version, null, "jar");

                    if (artifact != null) {
                        extensionArtifacts.add(artifact);
                    }
                }
            }
            project.setExtensionArtifacts(extensionArtifacts);

            // managedVersionMap
            Map<String, Artifact> map = Collections.emptyMap();
            final DependencyManagement dependencyManagement = project.getDependencyManagement();
            if (dependencyManagement != null
                    && dependencyManagement.getDependencies() != null
                    && !dependencyManagement.getDependencies().isEmpty()) {
                map = new LazyMap<>(() -> {
                    Map<String, Artifact> tmp = new HashMap<>();
                    for (Dependency d : dependencyManagement.getDependencies()) {
                        Artifact artifact = repositorySystem.createDependencyArtifact(d);
                        if (artifact != null) {
                            tmp.put(d.getManagementKey(), artifact);
                        }
                    }
                    return Collections.unmodifiableMap(tmp);
                });
            }
            project.setManagedVersionMap(map);

            // release artifact repository
            if (project.getDistributionManagement() != null
                    && project.getDistributionManagement().getRepository() != null) {
                try {
                    DeploymentRepository r = project.getDistributionManagement().getRepository();
                    if (r.getId() != null
                            && !r.getId().isEmpty()
                            && r.getUrl() != null
                            && !r.getUrl().isEmpty()) {
                        ArtifactRepository repo = MavenRepositorySystem.buildArtifactRepository(r);
                        repositorySystem.injectProxy(request.getRepositorySession(), Arrays.asList(repo));
                        repositorySystem.injectAuthentication(request.getRepositorySession(), Arrays.asList(repo));
                        project.setReleaseArtifactRepository(repo);
                    }
                } catch (InvalidRepositoryException e) {
                    throw new IllegalStateException(
                            "Failed to create release distribution repository for " + project.getId(), e);
                }
            }

            // snapshot artifact repository
            if (project.getDistributionManagement() != null
                    && project.getDistributionManagement().getSnapshotRepository() != null) {
                try {
                    DeploymentRepository r = project.getDistributionManagement().getSnapshotRepository();
                    if (r.getId() != null
                            && !r.getId().isEmpty()
                            && r.getUrl() != null
                            && !r.getUrl().isEmpty()) {
                        ArtifactRepository repo = MavenRepositorySystem.buildArtifactRepository(r);
                        repositorySystem.injectProxy(request.getRepositorySession(), Arrays.asList(repo));
                        repositorySystem.injectAuthentication(request.getRepositorySession(), Arrays.asList(repo));
                        project.setSnapshotArtifactRepository(repo);
                    }
                } catch (InvalidRepositoryException e) {
                    throw new IllegalStateException(
                            "Failed to create snapshot distribution repository for " + project.getId(), e);
                }
            }
        }

        private void initParent(MavenProject project, Map<File, MavenProject> projects, ModelBuildingResult result) {
            Model parentModel = result.getModelIds().size() > 1
                            && !result.getModelIds().get(1).isEmpty()
                    ? result.getRawModel(result.getModelIds().get(1))
                    : null;

            if (parentModel != null) {
                final String parentGroupId = inheritedGroupId(result, 1);
                final String parentVersion = inheritedVersion(result, 1);

                project.setParentArtifact(repositorySystem.createProjectArtifact(
                        parentGroupId, parentModel.getArtifactId(), parentVersion));

                // org.apache.maven.its.mng4834:parent:0.1
                String parentModelId = result.getModelIds().get(1);
                File parentPomFile = result.getRawModel(parentModelId).getPomFile();
                MavenProject parent = parentPomFile != null ? projects.get(parentPomFile) : null;
                if (parent == null) {
                    //
                    // At this point the DefaultModelBuildingListener has fired and it populates the
                    // remote repositories with those found in the pom.xml, along with the existing externally
                    // defined repositories.
                    //
                    request.setRemoteRepositories(project.getRemoteArtifactRepositories());
                    if (parentPomFile != null) {
                        project.setParentFile(parentPomFile);
                        try {
                            parent = build(parentPomFile, new FileModelSource(parentPomFile))
                                    .getProject();
                        } catch (ProjectBuildingException e) {
                            // MNG-4488 where let invalid parents slide on by
                            if (logger.isDebugEnabled()) {
                                // Message below is checked for in the MNG-2199 core IT.
                                logger.warn("Failed to build parent project for " + project.getId(), e);
                            } else {
                                // Message below is checked for in the MNG-2199 core IT.
                                logger.warn("Failed to build parent project for " + project.getId());
                            }
                        }
                    } else {
                        Artifact parentArtifact = project.getParentArtifact();
                        try {
                            parent = build(parentArtifact, false).getProject();
                        } catch (ProjectBuildingException e) {
                            // MNG-4488 where let invalid parents slide on by
                            if (logger.isDebugEnabled()) {
                                // Message below is checked for in the MNG-2199 core IT.
                                logger.warn("Failed to build parent project for " + project.getId(), e);
                            } else {
                                // Message below is checked for in the MNG-2199 core IT.
                                logger.warn("Failed to build parent project for " + project.getId());
                            }
                        }
                    }
                }
                project.setParent(parent);
                if (project.getParentFile() == null && parent != null) {
                    project.setParentFile(parent.getFile());
                }
            }
        }

        private ModelBuildingRequest getModelBuildingRequest() {
            ModelBuildingRequest modelBuildingRequest = new DefaultModelBuildingRequest();

            RequestTrace trace = RequestTrace.newChild(null, request).newChild(modelBuildingRequest);

            ModelResolver resolver = new ProjectModelResolver(
                    session,
                    trace,
                    repoSystem,
                    repositoryManager,
                    repositories,
                    request.getRepositoryMerging(),
                    modelPool);

            modelBuildingRequest.setValidationLevel(request.getValidationLevel());
            modelBuildingRequest.setProcessPlugins(request.isProcessPlugins());
            modelBuildingRequest.setProfiles(request.getProfiles());
            modelBuildingRequest.setActiveProfileIds(request.getActiveProfileIds());
            modelBuildingRequest.setInactiveProfileIds(request.getInactiveProfileIds());
            modelBuildingRequest.setSystemProperties(request.getSystemProperties());
            modelBuildingRequest.setUserProperties(request.getUserProperties());
            modelBuildingRequest.setBuildStartTime(request.getBuildStartTime());
            modelBuildingRequest.setModelResolver(resolver);
            // this is a hint that we want to build 1 file, so don't cache. See MNG-7063
            if (modelPool != null) {
                modelBuildingRequest.setModelCache(modelCacheFactory.createCache(session));
            }
            modelBuildingRequest.setTransformerContextBuilder(transformerContextBuilder);
            InternalSession session = (InternalSession) this.session.getData().get(InternalSession.class);
            if (session != null) {
                try {
                    modelBuildingRequest.setRootDirectory(session.getRootDirectory());
                } catch (IllegalStateException e) {
                    // can happen if root directory cannot be found, just ignore
                }
            }

            return modelBuildingRequest;
        }

        private DependencyResolutionResult resolveDependencies(MavenProject project) {
            DependencyResolutionResult resolutionResult;

            try {
                DefaultDependencyResolutionRequest resolution =
                        new DefaultDependencyResolutionRequest(project, session);
                resolutionResult = dependencyResolver.resolve(resolution);
            } catch (DependencyResolutionException e) {
                resolutionResult = e.getResult();
            }

            Set<Artifact> artifacts = new LinkedHashSet<>();
            if (resolutionResult.getDependencyGraph() != null) {
                RepositoryUtils.toArtifacts(
                        artifacts,
                        resolutionResult.getDependencyGraph().getChildren(),
                        Collections.singletonList(project.getArtifact().getId()),
                        null);

                // Maven 2.x quirk: an artifact always points at the local repo, regardless whether resolved or not
                LocalRepositoryManager lrm = session.getLocalRepositoryManager();
                for (Artifact artifact : artifacts) {
                    if (!artifact.isResolved()) {
                        String path = lrm.getPathForLocalArtifact(RepositoryUtils.toArtifact(artifact));
                        artifact.setFile(new File(lrm.getRepository().getBasedir(), path));
                    }
                }
            }
            project.setResolvedArtifacts(artifacts);
            project.setArtifacts(artifacts);

            return resolutionResult;
        }
    }

    private List<String> getProfileIds(List<org.apache.maven.model.Profile> profiles) {
        return profiles.stream().map(org.apache.maven.model.Profile::getId).collect(Collectors.toList());
    }

    private static ModelSource createStubModelSource(Artifact artifact) {
        StringBuilder buffer = new StringBuilder(1024);

        buffer.append("<?xml version='1.0'?>");
        buffer.append("<project>");
        buffer.append("<modelVersion>4.0.0</modelVersion>");
        buffer.append("<groupId>").append(artifact.getGroupId()).append("</groupId>");
        buffer.append("<artifactId>").append(artifact.getArtifactId()).append("</artifactId>");
        buffer.append("<version>").append(artifact.getBaseVersion()).append("</version>");
        buffer.append("<packaging>").append(artifact.getType()).append("</packaging>");
        buffer.append("</project>");

        return new StringModelSource(buffer, artifact.getId());
    }

    private static String inheritedGroupId(final ModelBuildingResult result, final int modelIndex) {
        String groupId = null;
        final String modelId = result.getModelIds().get(modelIndex);

        if (!modelId.isEmpty()) {
            final Model model = result.getRawModel(modelId);
            groupId = model.getGroupId() != null ? model.getGroupId() : inheritedGroupId(result, modelIndex + 1);
        }

        return groupId;
    }

    private static String inheritedVersion(final ModelBuildingResult result, final int modelIndex) {
        String version = null;
        final String modelId = result.getModelIds().get(modelIndex);

        if (!modelId.isEmpty()) {
            version = result.getRawModel(modelId).getVersion();
            if (version == null) {
                version = inheritedVersion(result, modelIndex + 1);
            }
        }

        return version;
    }

    static <T extends Throwable> void uncheckedThrow(Throwable t) throws T {
        throw (T) t; // rely on vacuous cast
    }

    static class LazyMap<K, V> extends AbstractMap<K, V> {
        private final Supplier<Map<K, V>> supplier;
        private volatile Map<K, V> delegate;

        LazyMap(Supplier<Map<K, V>> supplier) {
            this.supplier = supplier;
        }

        @Override
        public Set<Entry<K, V>> entrySet() {
            if (delegate == null) {
                synchronized (this) {
                    if (delegate == null) {
                        delegate = supplier.get();
                    }
                }
            }
            return delegate.entrySet();
        }
    }
}
