diff --git a/maven-core/src/main/java/org/apache/maven/internal/impl/DefaultProjectBuilder.java b/maven-core/src/main/java/org/apache/maven/internal/impl/DefaultProjectBuilder.java index cd41925c9a..fc45b79192 100644 --- a/maven-core/src/main/java/org/apache/maven/internal/impl/DefaultProjectBuilder.java +++ b/maven-core/src/main/java/org/apache/maven/internal/impl/DefaultProjectBuilder.java @@ -150,10 +150,7 @@ public class DefaultProjectBuilder implements ProjectBuilder { public String getLocation() { StringBuilder buffer = new StringBuilder(256); - if (getSource().length() > 0) { - if (buffer.length() > 0) { - buffer.append(", "); - } + if (!getSource().isEmpty()) { buffer.append(getSource()); } @@ -205,13 +202,6 @@ public class DefaultProjectBuilder implements ProjectBuilder { public Node getRoot() { return session.getNode(r.getDependencyGraph()); } - - // @Override - // public List - // getArtifactResults() - // { - // return Collections.emptyList(); - // } }); } }; diff --git a/maven-core/src/main/java/org/apache/maven/project/DefaultProjectBuilder.java b/maven-core/src/main/java/org/apache/maven/project/DefaultProjectBuilder.java index 341dee9261..4008ca50b4 100644 --- a/maven-core/src/main/java/org/apache/maven/project/DefaultProjectBuilder.java +++ b/maven-core/src/main/java/org/apache/maven/project/DefaultProjectBuilder.java @@ -25,8 +25,13 @@ 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; @@ -45,6 +50,7 @@ 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; @@ -81,6 +87,8 @@ import org.slf4j.LoggerFactory; @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; @@ -122,164 +130,13 @@ public class DefaultProjectBuilder implements ProjectBuilder { @Override public ProjectBuildingResult build(File pomFile, ProjectBuildingRequest request) throws ProjectBuildingException { - InternalConfig config = new InternalConfig(request, null, modelBuilder.newTransformerContextBuilder()); - return build(pomFile, new FileModelSource(pomFile), config); + return new BuildSession(request, false).build(pomFile, new FileModelSource(pomFile)); } @Override public ProjectBuildingResult build(ModelSource modelSource, ProjectBuildingRequest request) throws ProjectBuildingException { - return build(null, modelSource, new InternalConfig(request, null, null)); - } - - private ProjectBuildingResult build(File pomFile, ModelSource modelSource, InternalConfig config) - throws ProjectBuildingException { - ClassLoader oldContextClassLoader = Thread.currentThread().getContextClassLoader(); - - try { - ProjectBuildingRequest projectBuildingRequest = config.request; - - MavenProject project = projectBuildingRequest.getProject(); - - List modelProblems = null; - Throwable error = null; - - if (project == null) { - ModelBuildingRequest request = getModelBuildingRequest(config); - - project = new MavenProject(); - project.setFile(pomFile); - - DefaultModelBuildingListener listener = - new DefaultModelBuildingListener(project, projectBuildingHelper, projectBuildingRequest); - - 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(), true, result, new HashMap<>(), projectBuildingRequest); - } else if (projectBuildingRequest.isResolveDependencies()) { - projectBuildingHelper.selectProjectRealm(project); - } - - DependencyResolutionResult resolutionResult = null; - - if (projectBuildingRequest.isResolveDependencies()) { - resolutionResult = resolveDependencies(project, config.session); - } - - 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); - } - } - - private DependencyResolutionResult resolveDependencies(MavenProject project, RepositorySystemSession session) { - DependencyResolutionResult resolutionResult; - - try { - DefaultDependencyResolutionRequest resolution = new DefaultDependencyResolutionRequest(project, session); - resolutionResult = dependencyResolver.resolve(resolution); - } catch (DependencyResolutionException e) { - resolutionResult = e.getResult(); - } - - Set 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 getProfileIds(List profiles) { - return profiles.stream().map(org.apache.maven.model.Profile::getId).collect(Collectors.toList()); - } - - private ModelBuildingRequest getModelBuildingRequest(InternalConfig config) { - ProjectBuildingRequest configuration = config.request; - - ModelBuildingRequest request = new DefaultModelBuildingRequest(); - - RequestTrace trace = RequestTrace.newChild(null, configuration).newChild(request); - - ModelResolver resolver = new ProjectModelResolver( - config.session, - trace, - repoSystem, - repositoryManager, - config.repositories, - configuration.getRepositoryMerging(), - config.modelPool); - - request.setValidationLevel(configuration.getValidationLevel()); - request.setProcessPlugins(configuration.isProcessPlugins()); - request.setProfiles(configuration.getProfiles()); - request.setActiveProfileIds(configuration.getActiveProfileIds()); - request.setInactiveProfileIds(configuration.getInactiveProfileIds()); - request.setSystemProperties(configuration.getSystemProperties()); - request.setUserProperties(configuration.getUserProperties()); - request.setBuildStartTime(configuration.getBuildStartTime()); - request.setModelResolver(resolver); - // this is a hint that we want to build 1 file, so don't cache. See MNG-7063 - if (config.modelPool != null) { - request.setModelCache(modelCacheFactory.createCache(config.session)); - } - request.setTransformerContextBuilder(config.transformerContextBuilder); - DefaultSession session = (DefaultSession) config.session.getData().get(DefaultSession.class); - if (session != null) { - try { - request.setRootDirectory(session.getRootDirectory()); - } catch (IllegalStateException e) { - // can happen if root directory cannot be found, just ignore - } - } - - return request; + return new BuildSession(request, false).build(null, modelSource); } @Override @@ -291,269 +148,13 @@ public class DefaultProjectBuilder implements ProjectBuilder { @Override public ProjectBuildingResult build(Artifact artifact, boolean allowStubModel, ProjectBuildingRequest request) throws ProjectBuildingException { - org.eclipse.aether.artifact.Artifact pomArtifact = RepositoryUtils.toArtifact(artifact); - pomArtifact = ArtifactDescriptorUtils.toPomArtifact(pomArtifact); - - InternalConfig config = new InternalConfig(request, null, null); - - boolean localProject; - - try { - ArtifactRequest pomRequest = new ArtifactRequest(); - pomRequest.setArtifact(pomArtifact); - pomRequest.setRepositories(config.repositories); - ArtifactResult pomResult = repoSystem.resolveArtifact(config.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), config); - } - 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), config); - } else { - return build( - null, - new ArtifactModelSource( - pomFile, artifact.getGroupId(), artifact.getArtifactId(), artifact.getVersion()), - config); - } - } - - private ModelSource createStubModelSource(Artifact artifact) { - StringBuilder buffer = new StringBuilder(1024); - - buffer.append(""); - buffer.append(""); - buffer.append("4.0.0"); - buffer.append("").append(artifact.getGroupId()).append(""); - buffer.append("").append(artifact.getArtifactId()).append(""); - buffer.append("").append(artifact.getBaseVersion()).append(""); - buffer.append("").append(artifact.getType()).append(""); - buffer.append(""); - - return new StringModelSource(buffer.toString(), artifact.getId()); + return new BuildSession(request, false).build(artifact, allowStubModel); } @Override public List build(List pomFiles, boolean recursive, ProjectBuildingRequest request) throws ProjectBuildingException { - List results = new ArrayList<>(); - - List interimResults = new ArrayList<>(); - - ReactorModelPool pool = new ReactorModelPool(); - - InternalConfig config = new InternalConfig(request, pool, modelBuilder.newTransformerContextBuilder()); - - Map projectIndex = new HashMap<>(256); - - // phase 1: get file Models from the reactor. - boolean noErrors = build( - results, interimResults, projectIndex, pomFiles, new LinkedHashSet<>(), true, recursive, config, pool); - - ClassLoader oldContextClassLoader = Thread.currentThread().getContextClassLoader(); - - try { - // Phase 2: get effective models from the reactor - noErrors = build( - results, - new ArrayList<>(), - projectIndex, - interimResults, - request, - new HashMap<>(), - config.session) - && noErrors; - } finally { - Thread.currentThread().setContextClassLoader(oldContextClassLoader); - } - - if (Features.buildConsumer(request.getUserProperties())) { - request.getRepositorySession() - .getData() - .set(TransformerContext.KEY, config.transformerContextBuilder.build()); - } - - if (!noErrors) { - throw new ProjectBuildingException(results); - } - - return results; - } - - @SuppressWarnings("checkstyle:parameternumber") - private boolean build( - List results, - List interimResults, - Map projectIndex, - List pomFiles, - Set aggregatorFiles, - boolean root, - boolean recursive, - InternalConfig config, - ReactorModelPool pool) { - boolean noErrors = true; - - for (File pomFile : pomFiles) { - aggregatorFiles.add(pomFile); - - if (!build( - results, interimResults, projectIndex, pomFile, aggregatorFiles, root, recursive, config, pool)) { - noErrors = false; - } - - aggregatorFiles.remove(pomFile); - } - - return noErrors; - } - - @SuppressWarnings("checkstyle:parameternumber") - private boolean build( - List results, - List interimResults, - Map projectIndex, - File pomFile, - Set aggregatorFiles, - boolean isRoot, - boolean recursive, - InternalConfig config, - ReactorModelPool pool) { - boolean noErrors = true; - - MavenProject project = new MavenProject(); - project.setFile(pomFile); - - project.setRootDirectory(rootLocator.findRoot(pomFile.getParentFile().toPath())); - - ModelBuildingRequest request = getModelBuildingRequest(config) - .setPomFile(pomFile) - .setTwoPhaseBuilding(true) - .setLocationTracking(true); - - DefaultModelBuildingListener listener = - new DefaultModelBuildingListener(project, projectBuildingHelper, config.request); - request.setModelBuildingListener(listener); - - ModelBuildingResult result; - try { - result = modelBuilder.build(request); - } catch (ModelBuildingException e) { - result = e.getResult(); - if (result == null || result.getFileModel() == null) { - results.add(new DefaultProjectBuildingResult(e.getModelId(), pomFile, e.getProblems())); - - return false; - } - // validation error, continue project building and delay failing to help IDEs - // result.getProblems().addAll(e.getProblems()) ? - noErrors = false; - } - - Model model = request.getFileModel(); - - pool.put(model.getPomFile().toPath(), model); - - InterimResult interimResult = new InterimResult(pomFile, request, result, listener, isRoot); - interimResults.add(interimResult); - - if (recursive) { - File basedir = pomFile.getParentFile(); - List 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 " + module + " of " + pomFile + " does not exist", - ModelProblem.Severity.ERROR, - ModelProblem.Version.BASE, - model, - -1, - -1, - null); - result.getProblems().add(problem); - - noErrors = false; - - 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); - - noErrors = false; - - continue; - } - - moduleFiles.add(moduleFile); - } - - interimResult.modules = new ArrayList<>(); - - if (!build( - results, - interimResult.modules, - projectIndex, - moduleFiles, - aggregatorFiles, - false, - recursive, - config, - pool)) { - noErrors = false; - } - } - - projectIndex.put(pomFile, project); - - return noErrors; + return new BuildSession(request, true).build(pomFiles, recursive); } static class InterimResult { @@ -564,44 +165,417 @@ public class DefaultProjectBuilder implements ProjectBuilder { ModelBuildingResult result; - DefaultModelBuildingListener listener; + MavenProject project; boolean root; List modules = Collections.emptyList(); + ProjectBuildingResult projectBuildingResult; + InterimResult( File pomFile, ModelBuildingRequest request, ModelBuildingResult result, - DefaultModelBuildingListener listener, + MavenProject project, boolean root) { this.pomFile = pomFile; this.request = request; this.result = result; - this.listener = listener; + 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(); + } } - private boolean build( - List results, - List projects, - Map projectIndex, - List interimResults, - ProjectBuildingRequest request, - Map profilesXmls, - RepositorySystemSession session) { - boolean noErrors = true; + class BuildSession { + private final ProjectBuildingRequest request; + private final RepositorySystemSession session; + private final List repositories; + private final ReactorModelPool modelPool; + private final TransformerContextBuilder transformerContextBuilder; + private final ForkJoinPool forkJoinPool; - for (InterimResult interimResult : interimResults) { - MavenProject project = interimResult.listener.getProject(); + BuildSession(ProjectBuildingRequest request, boolean localProjects) { + this.request = request; + this.session = + RepositoryUtils.overlay(request.getLocalRepository(), request.getRepositorySession(), repoSystem); + this.repositories = RepositoryUtils.toRepos(request.getRemoteRepositories()); + if (localProjects) { + this.modelPool = new ReactorModelPool(); + this.transformerContextBuilder = modelBuilder.newTransformerContextBuilder(); + this.forkJoinPool = new ForkJoinPool(getParallelism(request)); + } else { + this.modelPool = null; + this.transformerContextBuilder = null; + this.forkJoinPool = null; + } + } + + 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 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 build(List pomFiles, boolean recursive) throws ProjectBuildingException { + ForkJoinTask> 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 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 doBuild(List pomFiles, boolean recursive) { + Map projectIndex = new ConcurrentHashMap<>(256); + + // phase 1: get file Models from the reactor. + List interimResults = build(projectIndex, pomFiles, new LinkedHashSet<>(), true, recursive); + + ClassLoader oldContextClassLoader = Thread.currentThread().getContextClassLoader(); + + try { + // Phase 2: get effective models from the reactor + List 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 build( + Map projectIndex, + List pomFiles, + Set aggregatorFiles, + boolean root, + boolean recursive) { + List> 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 Set concat(Set set, T elem) { + Set newSet = new HashSet<>(set); + newSet.add(elem); + return newSet; + } + + @SuppressWarnings("checkstyle:parameternumber") + private InterimResult build( + Map projectIndex, + File pomFile, + Set 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 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 build( + Map projectIndex, List 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 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 doBuild(Map 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, true, result, profilesXmls, request); + initProject(project, projectIndex, result); } catch (InvalidArtifactRTException iarte) { result.getProblems() .add(new DefaultModelProblem( @@ -614,297 +588,330 @@ public class DefaultProjectBuilder implements ProjectBuilder { iarte)); } - List modules = new ArrayList<>(); - noErrors = build(results, modules, projectIndex, interimResult.modules, request, profilesXmls, session) - && noErrors; - - projects.addAll(modules); - projects.add(project); + List results = build(projectIndex, interimResult.modules); project.setExecutionRoot(interimResult.root); - project.setCollectedProjects(modules); + project.setCollectedProjects( + results.stream().map(ProjectBuildingResult::getProject).collect(Collectors.toList())); DependencyResolutionResult resolutionResult = null; if (request.isResolveDependencies()) { - resolutionResult = resolveDependencies(project, session); + resolutionResult = resolveDependencies(project); } results.add(new DefaultProjectBuildingResult(project, result.getProblems(), resolutionResult)); + + return results; } catch (ModelBuildingException e) { - DefaultProjectBuildingResult result = null; + 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); } - results.add(result); - - noErrors = false; + return Collections.singletonList(result); } } - return noErrors; - } + @SuppressWarnings("checkstyle:methodlength") + private void initProject(MavenProject project, Map projects, ModelBuildingResult result) { + project.setModel(result.getEffectiveModel()); + project.setOriginalModel(result.getFileModel()); - @SuppressWarnings("checkstyle:methodlength") - private void initProject( - MavenProject project, - Map projects, - boolean buildParentIfNotExisting, - ModelBuildingResult result, - Map profilesXmls, - ProjectBuildingRequest projectBuildingRequest) { - project.setModel(result.getEffectiveModel()); - project.setOriginalModel(result.getFileModel()); + initParent(project, projects, result); - initParent(project, projects, buildParentIfNotExisting, result, projectBuildingRequest); + Artifact projectArtifact = repositorySystem.createArtifact( + project.getGroupId(), project.getArtifactId(), project.getVersion(), null, project.getPackaging()); + project.setArtifact(projectArtifact); - Artifact projectArtifact = repositorySystem.createArtifact( - project.getGroupId(), project.getArtifactId(), project.getVersion(), null, project.getPackaging()); - project.setArtifact(projectArtifact); - - if (project.getFile() != null && buildParentIfNotExisting) // only set those on 2nd phase, ignore on 1st pass - { - Build build = project.getBuild(); - project.addScriptSourceRoot(build.getScriptSourceDirectory()); - project.addCompileSourceRoot(build.getSourceDirectory()); - project.addTestCompileSourceRoot(build.getTestSourceDirectory()); - } - - List 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(projectBuildingRequest); - - // pluginArtifacts - Set pluginArtifacts = new HashSet<>(); - for (Plugin plugin : project.getBuildPlugins()) { - Artifact artifact = repositorySystem.createPluginArtifact(plugin); - - if (artifact != null) { - pluginArtifacts.add(artifact); + // 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()); } - } - project.setPluginArtifacts(pluginArtifacts); - // reportArtifacts - Set reportArtifacts = new HashSet<>(); - for (ReportPlugin report : project.getReportPlugins()) { - Plugin pp = new Plugin(); - pp.setGroupId(report.getGroupId()); - pp.setArtifactId(report.getArtifactId()); - pp.setVersion(report.getVersion()); + List activeProfiles = new ArrayList<>(); + activeProfiles.addAll( + result.getActivePomProfiles(result.getModelIds().get(0))); + activeProfiles.addAll(result.getActiveExternalProfiles()); + project.setActiveProfiles(activeProfiles); - Artifact artifact = repositorySystem.createPluginArtifact(pp); - - if (artifact != null) { - reportArtifacts.add(artifact); + project.setInjectedProfileIds("external", getProfileIds(result.getActiveExternalProfiles())); + for (String modelId : result.getModelIds()) { + project.setInjectedProfileIds(modelId, getProfileIds(result.getActivePomProfiles(modelId))); } - } - project.setReportArtifacts(reportArtifacts); - // extensionArtifacts - Set extensionArtifacts = new HashSet<>(); - List 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(); - } + // + // All the parts that were taken out of MavenProject for Maven 4.0.0 + // - Artifact artifact = - repositorySystem.createArtifact(ext.getGroupId(), ext.getArtifactId(), version, null, "jar"); + project.setProjectBuildingRequest(request); + + // pluginArtifacts + Set pluginArtifacts = new HashSet<>(); + for (Plugin plugin : project.getBuildPlugins()) { + Artifact artifact = repositorySystem.createPluginArtifact(plugin); if (artifact != null) { - extensionArtifacts.add(artifact); + pluginArtifacts.add(artifact); + } + } + project.setPluginArtifacts(pluginArtifacts); + + // reportArtifacts + Set 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 extensionArtifacts = new HashSet<>(); + List 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 map = Collections.emptyMap(); + final DependencyManagement dependencyManagement = project.getDependencyManagement(); + if (dependencyManagement != null + && dependencyManagement.getDependencies() != null + && !dependencyManagement.getDependencies().isEmpty()) { + map = new LazyMap<>(() -> { + Map 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); } } } - project.setExtensionArtifacts(extensionArtifacts); - // managedVersionMap - Map map = null; - if (repositorySystem != null) { - final DependencyManagement dependencyManagement = project.getDependencyManagement(); - if ((dependencyManagement != null) - && ((dependencyManagement.getDependencies()) != null) - && (dependencyManagement.getDependencies().size() > 0)) { - map = new AbstractMap() { - HashMap delegate; + private void initParent(MavenProject project, Map projects, ModelBuildingResult result) { + Model parentModel = result.getModelIds().size() > 1 + && !result.getModelIds().get(1).isEmpty() + ? result.getRawModel(result.getModelIds().get(1)) + : null; - @Override - public Set> entrySet() { - return Collections.unmodifiableSet(compute().entrySet()); - } + if (parentModel != null) { + final String parentGroupId = inheritedGroupId(result, 1); + final String parentVersion = inheritedVersion(result, 1); - @Override - public Set keySet() { - return Collections.unmodifiableSet(compute().keySet()); - } + project.setParentArtifact(repositorySystem.createProjectArtifact( + parentGroupId, parentModel.getArtifactId(), parentVersion)); - @Override - public Collection values() { - return Collections.unmodifiableCollection(compute().values()); - } - - @Override - public boolean containsValue(Object value) { - return compute().containsValue(value); - } - - @Override - public boolean containsKey(Object key) { - return compute().containsKey(key); - } - - @Override - public Artifact get(Object key) { - return compute().get(key); - } - - HashMap compute() { - if (delegate == null) { - delegate = new HashMap<>(); - for (Dependency d : dependencyManagement.getDependencies()) { - Artifact artifact = repositorySystem.createDependencyArtifact(d); - - if (artifact != null) { - delegate.put(d.getManagementKey(), artifact); - } + // 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()); } } - - return delegate; } - }; - } else { - map = Collections.emptyMap(); - } - } - 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(projectBuildingRequest.getRepositorySession(), Arrays.asList(repo)); - repositorySystem.injectAuthentication( - projectBuildingRequest.getRepositorySession(), Arrays.asList(repo)); - project.setReleaseArtifactRepository(repo); } - } catch (InvalidRepositoryException e) { - throw new IllegalStateException( - "Failed to create release distribution repository for " + project.getId(), e); + project.setParent(parent); + if (project.getParentFile() == null && parent != null) { + project.setParentFile(parent.getFile()); + } } } - // 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(projectBuildingRequest.getRepositorySession(), Arrays.asList(repo)); - repositorySystem.injectAuthentication( - projectBuildingRequest.getRepositorySession(), Arrays.asList(repo)); - project.setSnapshotArtifactRepository(repo); - } - } catch (InvalidRepositoryException e) { - throw new IllegalStateException( - "Failed to create snapshot distribution repository for " + project.getId(), e); + 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); + DefaultSession session = (DefaultSession) this.session.getData().get(DefaultSession.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 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 void initParent( - MavenProject project, - Map projects, - boolean buildParentIfNotExisting, - ModelBuildingResult result, - ProjectBuildingRequest projectBuildingRequest) { - Model parentModel = - result.getModelIds().size() > 1 && !result.getModelIds().get(1).isEmpty() - ? result.getRawModel(result.getModelIds().get(1)) - : null; + private List getProfileIds(List profiles) { + return profiles.stream().map(org.apache.maven.model.Profile::getId).collect(Collectors.toList()); + } - if (parentModel != null) { - final String parentGroupId = inheritedGroupId(result, 1); - final String parentVersion = inheritedVersion(result, 1); + private static ModelSource createStubModelSource(Artifact artifact) { + StringBuilder buffer = new StringBuilder(1024); - project.setParentArtifact( - repositorySystem.createProjectArtifact(parentGroupId, parentModel.getArtifactId(), parentVersion)); + buffer.append(""); + buffer.append(""); + buffer.append("4.0.0"); + buffer.append("").append(artifact.getGroupId()).append(""); + buffer.append("").append(artifact.getArtifactId()).append(""); + buffer.append("").append(artifact.getBaseVersion()).append(""); + buffer.append("").append(artifact.getType()).append(""); + buffer.append(""); - // org.apache.maven.its.mng4834:parent:0.1 - String parentModelId = result.getModelIds().get(1); - File parentPomFile = result.getRawModel(parentModelId).getPomFile(); - MavenProject parent = projects.get(parentPomFile); - if (parent == null && buildParentIfNotExisting) { - // - // 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. - // - projectBuildingRequest.setRemoteRepositories(project.getRemoteArtifactRepositories()); - if (parentPomFile != null) { - project.setParentFile(parentPomFile); - try { - parent = build(parentPomFile, projectBuildingRequest).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, projectBuildingRequest).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()); - } - } + return new StringModelSource(buffer, artifact.getId()); } private static String inheritedGroupId(final ModelBuildingResult result, final int modelIndex) { @@ -933,31 +940,28 @@ public class DefaultProjectBuilder implements ProjectBuilder { return version; } - /** - * InternalConfig - */ - class InternalConfig { + static void uncheckedThrow(Throwable t) throws T { + throw (T) t; // rely on vacuous cast + } - private final ProjectBuildingRequest request; + static class LazyMap extends AbstractMap { + private final Supplier> supplier; + private volatile Map delegate; - private final RepositorySystemSession session; + LazyMap(Supplier> supplier) { + this.supplier = supplier; + } - private final List repositories; - - private final ReactorModelPool modelPool; - - private final TransformerContextBuilder transformerContextBuilder; - - InternalConfig( - ProjectBuildingRequest request, - ReactorModelPool modelPool, - TransformerContextBuilder transformerContextBuilder) { - this.request = request; - this.modelPool = modelPool; - this.transformerContextBuilder = transformerContextBuilder; - - session = RepositoryUtils.overlay(request.getLocalRepository(), request.getRepositorySession(), repoSystem); - repositories = RepositoryUtils.toRepos(request.getRemoteRepositories()); + @Override + public Set> entrySet() { + if (delegate == null) { + synchronized (this) { + if (delegate == null) { + delegate = supplier.get(); + } + } + } + return delegate.entrySet(); } } } diff --git a/maven-model-builder/src/main/java/org/apache/maven/model/building/DefaultModelBuilder.java b/maven-model-builder/src/main/java/org/apache/maven/model/building/DefaultModelBuilder.java index 77cb05f89e..e1db4c34de 100644 --- a/maven-model-builder/src/main/java/org/apache/maven/model/building/DefaultModelBuilder.java +++ b/maven-model-builder/src/main/java/org/apache/maven/model/building/DefaultModelBuilder.java @@ -25,15 +25,7 @@ import javax.inject.Singleton; import java.io.File; import java.io.IOException; import java.lang.reflect.Field; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.Iterator; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Properties; +import java.util.*; import java.util.concurrent.Callable; import java.util.concurrent.ForkJoinTask; import java.util.function.Function; @@ -48,7 +40,6 @@ import org.apache.maven.artifact.versioning.InvalidVersionSpecificationException import org.apache.maven.artifact.versioning.VersionRange; import org.apache.maven.building.Source; import org.apache.maven.model.Activation; -import org.apache.maven.model.ActivationFile; import org.apache.maven.model.Build; import org.apache.maven.model.Dependency; import org.apache.maven.model.DependencyManagement; @@ -58,7 +49,6 @@ import org.apache.maven.model.Parent; import org.apache.maven.model.Plugin; import org.apache.maven.model.PluginManagement; import org.apache.maven.model.Profile; -import org.apache.maven.model.Repository; import org.apache.maven.model.building.ModelProblem.Severity; import org.apache.maven.model.building.ModelProblem.Version; import org.apache.maven.model.composition.DependencyManagementImporter; @@ -713,7 +703,7 @@ public class DefaultModelBuilder implements ModelBuilder { profileActivationContext.setUserProperties(profileProps); } - profileActivationContext.setProjectProperties(inputModel.getProperties()); + profileActivationContext.setProjectProperties(inputModel.getDelegate().getProperties()); problems.setSource(inputModel); List activePomProfiles = profileSelector.getActiveProfiles(inputModel.getProfiles(), profileActivationContext, problems); @@ -742,6 +732,9 @@ public class DefaultModelBuilder implements ModelBuilder { DefaultModelProblemCollector problems) throws ModelBuildingException { Model inputModel = readRawModel(request, problems); + if (problems.hasFatalErrors()) { + throw problems.newModelBuildingException(); + } problems.setRootModel(inputModel); @@ -777,50 +770,43 @@ public class DefaultModelBuilder implements ModelBuilder { String modelId = currentData.getId(); result.addModelId(modelId); - Model rawModel = currentData.getModel(); - result.setRawModel(modelId, rawModel); - - profileActivationContext.setProjectProperties(rawModel.getProperties()); - problems.setSource(rawModel); - List activePomProfiles = - profileSelector.getActiveProfiles(rawModel.getProfiles(), profileActivationContext, problems); - result.setActivePomProfiles(modelId, activePomProfiles); - - Model tmpModel = rawModel.clone(); - - problems.setSource(tmpModel); + Model model = currentData.getModel(); + result.setRawModel(modelId, model); + problems.setSource(model); + org.apache.maven.api.model.Model modelv4 = model.getDelegate(); // model normalization - tmpModel = new Model(modelNormalizer.mergeDuplicates(tmpModel.getDelegate(), request, problems)); + modelv4 = modelNormalizer.mergeDuplicates(modelv4, request, problems); - profileActivationContext.setProjectProperties(tmpModel.getProperties()); + // profile activation + profileActivationContext.setProjectProperties(modelv4.getProperties()); - Map interpolatedActivations = - getInterpolatedActivations(rawModel, profileActivationContext, problems); - injectProfileActivations(tmpModel, interpolatedActivations); + List interpolatedProfiles = + interpolateActivations(modelv4.getProfiles(), profileActivationContext, problems); // profile injection - for (Profile activeProfile : result.getActivePomProfiles(modelId)) { - profileInjector.injectProfile(tmpModel, activeProfile, request, problems); - } - + List activePomProfiles = + profileSelector.getActiveProfilesV4(interpolatedProfiles, profileActivationContext, problems); + result.setActivePomProfiles( + modelId, activePomProfiles.stream().map(Profile::new).collect(Collectors.toList())); + modelv4 = profileInjector.injectProfiles(modelv4, activePomProfiles, request, problems); if (currentData == resultData) { for (Profile activeProfile : activeExternalProfiles) { - profileInjector.injectProfile(tmpModel, activeProfile, request, problems); + modelv4 = profileInjector.injectProfile(modelv4, activeProfile.getDelegate(), request, problems); } - result.setEffectiveModel(tmpModel); } - lineage.add(tmpModel); + lineage.add(new Model(modelv4)); if (currentData == superData) { break; } - configureResolver(request.getModelResolver(), tmpModel, problems); + // add repositories specified by the current model so that we can resolve the parent + configureResolver(request.getModelResolver(), modelv4, problems, false); - ModelData parentData = - readParent(currentData.getModel(), currentData.getSource(), request, result, problems); + // we pass a cloned model, so that resolving the parent version does not affect the returned model + ModelData parentData = readParent(new Model(modelv4), currentData.getSource(), request, problems); if (parentData == null) { currentData = superData; @@ -840,7 +826,22 @@ public class DefaultModelBuilder implements ModelBuilder { } } - problems.setSource(result.getRawModel()); + Model tmpModel = lineage.get(0); + + // inject interpolated activations + List interpolated = + interpolateActivations(tmpModel.getDelegate().getProfiles(), profileActivationContext, problems); + if (interpolated != tmpModel.getDelegate().getProfiles()) { + tmpModel.update(tmpModel.getDelegate().withProfiles(interpolated)); + } + + // inject external profile into current model + tmpModel.update(profileInjector.injectProfiles( + tmpModel.getDelegate(), + activeExternalProfiles.stream().map(Profile::getDelegate).collect(Collectors.toList()), + request, + problems)); + checkPluginVersions(lineage, request, problems); // inheritance assembly @@ -860,44 +861,76 @@ public class DefaultModelBuilder implements ModelBuilder { result.setEffectiveModel(resultModel); // Now the fully interpolated model is available: reconfigure the resolver - configureResolver(request.getModelResolver(), resultModel, problems, true); + configureResolver(request.getModelResolver(), resultModel.getDelegate(), problems, true); return resultModel; } - private Map getInterpolatedActivations( - Model rawModel, DefaultProfileActivationContext context, DefaultModelProblemCollector problems) { - Map interpolatedActivations = getProfileActivations(rawModel, true); - for (Activation activation : interpolatedActivations.values()) { - if (activation.getFile() != null) { - replaceWithInterpolatedValue(activation.getFile(), context, problems); + private List interpolateActivations( + List profiles, + DefaultProfileActivationContext context, + DefaultModelProblemCollector problems) { + List newProfiles = null; + for (int index = 0; index < profiles.size(); index++) { + org.apache.maven.api.model.Profile profile = profiles.get(index); + org.apache.maven.api.model.Activation activation = profile.getActivation(); + if (activation != null) { + org.apache.maven.api.model.ActivationFile file = activation.getFile(); + if (file != null) { + String oldExists = file.getExists(); + if (isNotEmpty(oldExists)) { + try { + String newExists = interpolate(oldExists, context); + if (!Objects.equals(oldExists, newExists)) { + if (newProfiles == null) { + newProfiles = new ArrayList<>(profiles); + } + newProfiles.set( + index, profile.withActivation(activation.withFile(file.withExists(newExists)))); + } + } catch (InterpolationException e) { + addInterpolationProblem(problems, file, oldExists, e, "exists"); + } + } else { + String oldMissing = file.getMissing(); + if (isNotEmpty(oldMissing)) { + try { + String newMissing = interpolate(oldMissing, context); + if (!Objects.equals(oldMissing, newMissing)) { + if (newProfiles == null) { + newProfiles = new ArrayList<>(profiles); + } + newProfiles.set( + index, + profile.withActivation(activation.withFile(file.withMissing(newMissing)))); + } + } catch (InterpolationException e) { + addInterpolationProblem(problems, file, oldMissing, e, "missing"); + } + } + } + } } } - return interpolatedActivations; + return newProfiles != null ? newProfiles : profiles; } - private void replaceWithInterpolatedValue( - ActivationFile activationFile, ProfileActivationContext context, DefaultModelProblemCollector problems) { - try { - if (isNotEmpty(activationFile.getExists())) { - String path = activationFile.getExists(); - String absolutePath = profileActivationFilePathInterpolator.interpolate(path, context); - activationFile.setExists(absolutePath); - } else if (isNotEmpty(activationFile.getMissing())) { - String path = activationFile.getMissing(); - String absolutePath = profileActivationFilePathInterpolator.interpolate(path, context); - activationFile.setMissing(absolutePath); - } - } catch (InterpolationException e) { - String path = - isNotEmpty(activationFile.getExists()) ? activationFile.getExists() : activationFile.getMissing(); + private static void addInterpolationProblem( + DefaultModelProblemCollector problems, + org.apache.maven.api.model.ActivationFile file, + String path, + InterpolationException e, + String locationKey) { + problems.add(new ModelProblemCollectorRequest(Severity.ERROR, Version.BASE) + .setMessage("Failed to interpolate file location " + path + ": " + e.getMessage()) + .setLocation(Optional.ofNullable(file.getLocation(locationKey)) + .map(InputLocation::new) + .orElse(null)) + .setException(e)); + } - problems.add(new ModelProblemCollectorRequest(Severity.ERROR, Version.BASE) - .setMessage("Failed to interpolate file location " + path + ": " + e.getMessage()) - .setLocation( - activationFile.getLocation(isNotEmpty(activationFile.getExists()) ? "exists" : "missing")) - .setException(e)); - } + private String interpolate(String path, ProfileActivationContext context) throws InterpolationException { + return isNotEmpty(path) ? profileActivationFilePathInterpolator.interpolate(path, context) : path; } private static boolean isNotEmpty(String string) { @@ -910,6 +943,15 @@ public class DefaultModelBuilder implements ModelBuilder { return build(request, result, new LinkedHashSet<>()); } + public Model buildRawModel(final ModelBuildingRequest request) throws ModelBuildingException { + DefaultModelProblemCollector problems = new DefaultModelProblemCollector(new DefaultModelBuildingResult()); + Model model = readRawModel(request, problems); + if (hasModelErrors(problems)) { + throw problems.newModelBuildingException(); + } + return model; + } + private ModelBuildingResult build( final ModelBuildingRequest request, final ModelBuildingResult phaseOneResult, Collection imports) throws ModelBuildingException { @@ -1127,41 +1169,49 @@ public class DefaultModelBuilder implements ModelBuilder { throws ModelBuildingException { ModelSource modelSource = request.getModelSource(); - ModelData cachedData = cache(request.getModelCache(), modelSource, ModelCacheTag.RAW, () -> { - Model rawModel; - if (Features.buildConsumer(request.getUserProperties()) && modelSource instanceof FileModelSource) { - rawModel = readFileModel(request, problems); - File pomFile = ((FileModelSource) modelSource).getFile(); + ModelData modelData = cache( + request.getModelCache(), + modelSource, + ModelCacheTag.RAW, + () -> doReadRawModel(modelSource, request, problems)); - try { - if (request.getTransformerContextBuilder() != null) { - TransformerContext context = - request.getTransformerContextBuilder().initialize(request, problems); - transformer.transform(pomFile.toPath(), context, rawModel); - } - } catch (TransformerException e) { - problems.add(new ModelProblemCollectorRequest(Severity.FATAL, Version.V40).setException(e)); + return modelData.getModel(); + } + + private ModelData doReadRawModel( + ModelSource modelSource, ModelBuildingRequest request, DefaultModelProblemCollector problems) + throws ModelBuildingException { + Model rawModel; + if (Features.buildConsumer(request.getUserProperties()) && modelSource instanceof FileModelSource) { + rawModel = readFileModel(request, problems); + File pomFile = ((FileModelSource) modelSource).getFile(); + + try { + if (request.getTransformerContextBuilder() != null) { + TransformerContext context = + request.getTransformerContextBuilder().initialize(request, problems); + transformer.transform(pomFile.toPath(), context, rawModel); } - } else if (request.getFileModel() == null) { - rawModel = readFileModel(request, problems); - } else { - rawModel = request.getFileModel().clone(); + } catch (TransformerException e) { + problems.add(new ModelProblemCollectorRequest(Severity.FATAL, Version.V40).setException(e)); } + } else if (request.getFileModel() == null) { + rawModel = readFileModel(request, problems); + } else { + rawModel = request.getFileModel().clone(); + } - modelValidator.validateRawModel(rawModel, request, problems); + modelValidator.validateRawModel(rawModel, request, problems); - if (hasFatalErrors(problems)) { - throw problems.newModelBuildingException(); - } + if (hasFatalErrors(problems)) { + throw problems.newModelBuildingException(); + } - String groupId = getGroupId(rawModel); - String artifactId = rawModel.getArtifactId(); - String version = getVersion(rawModel); + String groupId = getGroupId(rawModel); + String artifactId = rawModel.getArtifactId(); + String version = getVersion(rawModel); - ModelData modelData = new ModelData(modelSource, rawModel, groupId, artifactId, version); - return modelData; - }); - return cachedData != null ? cachedData.getModel() : null; + return new ModelData(modelSource, rawModel, groupId, artifactId, version); } String getGroupId(Model model) { @@ -1204,31 +1254,21 @@ public class DefaultModelBuilder implements ModelBuilder { return context; } - private void configureResolver(ModelResolver modelResolver, Model model, DefaultModelProblemCollector problems) { - configureResolver(modelResolver, model, problems, false); - } - private void configureResolver( ModelResolver modelResolver, - Model model, + org.apache.maven.api.model.Model model, DefaultModelProblemCollector problems, boolean replaceRepositories) { - if (modelResolver == null) { - return; - } - - problems.setSource(model); - - List repositories = model.getRepositories(); - - for (Repository repository : repositories) { - try { - modelResolver.addRepository(repository, replaceRepositories); - } catch (InvalidRepositoryException e) { - problems.add(new ModelProblemCollectorRequest(Severity.ERROR, Version.BASE) - .setMessage("Invalid repository " + repository.getId() + ": " + e.getMessage()) - .setLocation(repository.getLocation("")) - .setException(e)); + if (modelResolver != null) { + for (org.apache.maven.api.model.Repository repository : model.getRepositories()) { + try { + modelResolver.addRepository(repository, replaceRepositories); + } catch (InvalidRepositoryException e) { + problems.add(new ModelProblemCollectorRequest(Severity.ERROR, Version.BASE) + .setMessage("Invalid repository " + repository.getId() + ": " + e.getMessage()) + .setLocation(new InputLocation(repository.getLocation(""))) + .setException(e)); + } } } } @@ -1353,19 +1393,15 @@ public class DefaultModelBuilder implements ModelBuilder { } private ModelData readParent( - Model childModel, - Source childSource, - ModelBuildingRequest request, - ModelBuildingResult result, - DefaultModelProblemCollector problems) + Model childModel, Source childSource, ModelBuildingRequest request, DefaultModelProblemCollector problems) throws ModelBuildingException { ModelData parentData = null; Parent parent = childModel.getParent(); if (parent != null) { - parentData = readParentLocally(childModel, childSource, request, result, problems); + parentData = readParentLocally(childModel, childSource, request, problems); if (parentData == null) { - parentData = readParentExternally(childModel, request, result, problems); + parentData = readParentExternally(childModel, request, problems); } Model parentModel = parentData.getModel(); @@ -1381,11 +1417,7 @@ public class DefaultModelBuilder implements ModelBuilder { } private ModelData readParentLocally( - Model childModel, - Source childSource, - ModelBuildingRequest request, - ModelBuildingResult result, - DefaultModelProblemCollector problems) + Model childModel, Source childSource, ModelBuildingRequest request, DefaultModelProblemCollector problems) throws ModelBuildingException { final Parent parent = childModel.getParent(); final ModelSource2 candidateSource; @@ -1524,10 +1556,7 @@ public class DefaultModelBuilder implements ModelBuilder { } private ModelData readParentExternally( - Model childModel, - ModelBuildingRequest request, - ModelBuildingResult result, - DefaultModelProblemCollector problems) + Model childModel, ModelBuildingRequest request, DefaultModelProblemCollector problems) throws ModelBuildingException { problems.setSource(childModel); @@ -1701,12 +1730,14 @@ public class DefaultModelBuilder implements ModelBuilder { return null; } - org.apache.maven.api.model.DependencyManagement importMgmt = - cache(request.getModelCache(), groupId, artifactId, version, ModelCacheTag.IMPORT, () -> { - DependencyManagement importMgmtV3 = doLoadDependencyManagement( - model, request, problems, dependency, groupId, artifactId, version, importIds); - return importMgmtV3 != null ? importMgmtV3.getDelegate() : null; - }); + org.apache.maven.api.model.DependencyManagement importMgmt = cache( + request.getModelCache(), + groupId, + artifactId, + version, + ModelCacheTag.IMPORT, + () -> doLoadDependencyManagement( + model, request, problems, dependency, groupId, artifactId, version, importIds)); // [MNG-5600] Dependency management import should support exclusions. List exclusions = dependency.getDelegate().getExclusions(); @@ -1732,7 +1763,7 @@ public class DefaultModelBuilder implements ModelBuilder { } @SuppressWarnings("checkstyle:parameternumber") - private DependencyManagement doLoadDependencyManagement( + private org.apache.maven.api.model.DependencyManagement doLoadDependencyManagement( Model model, ModelBuildingRequest request, DefaultModelProblemCollector problems, @@ -1810,7 +1841,7 @@ public class DefaultModelBuilder implements ModelBuilder { if (importMgmt == null) { importMgmt = new DependencyManagement(); } - return importMgmt; + return importMgmt.getDelegate(); } private static T cache( @@ -1863,8 +1894,7 @@ public class DefaultModelBuilder implements ModelBuilder { Model model, ModelBuildingRequest request, ModelProblemCollector problems, - ModelBuildingEventCatapult catapult) - throws ModelBuildingException { + ModelBuildingEventCatapult catapult) { ModelBuildingListener listener = request.getModelBuildingListener(); if (listener != null) { diff --git a/maven-model-builder/src/main/java/org/apache/maven/model/building/DefaultTransformerContextBuilder.java b/maven-model-builder/src/main/java/org/apache/maven/model/building/DefaultTransformerContextBuilder.java index a2dce0e491..6b5de94484 100644 --- a/maven-model-builder/src/main/java/org/apache/maven/model/building/DefaultTransformerContextBuilder.java +++ b/maven-model-builder/src/main/java/org/apache/maven/model/building/DefaultTransformerContextBuilder.java @@ -28,8 +28,6 @@ import java.util.concurrent.ConcurrentHashMap; import org.apache.maven.model.Model; import org.apache.maven.model.building.DefaultTransformerContext.GAKey; import org.apache.maven.model.building.DefaultTransformerContext.Holder; -import org.codehaus.plexus.util.dag.CycleDetectedException; -import org.codehaus.plexus.util.dag.DAG; /** * Builds up the transformer context. @@ -39,7 +37,7 @@ import org.codehaus.plexus.util.dag.DAG; * @since 4.0.0 */ class DefaultTransformerContextBuilder implements TransformerContextBuilder { - private final DAG dag = new DAG(); + private final Graph dag = new Graph(); private final DefaultModelBuilder defaultModelBuilder; private final DefaultTransformerContext context; @@ -134,10 +132,10 @@ class DefaultTransformerContextBuilder implements TransformerContextBuilder { private boolean addEdge(Path from, Path p, DefaultModelProblemCollector problems) { try { synchronized (dag) { - dag.addEdge(from.toString(), p.toString()); + dag.addEdge(dag.addVertex(from.toString()), dag.addVertex(p.toString())); } return true; - } catch (CycleDetectedException e) { + } catch (Graph.CycleDetectedException e) { problems.add(new DefaultModelProblem( "Cycle detected between models at " + from + " and " + p, ModelProblem.Severity.FATAL, diff --git a/maven-model-builder/src/main/java/org/apache/maven/model/building/Graph.java b/maven-model-builder/src/main/java/org/apache/maven/model/building/Graph.java new file mode 100644 index 0000000000..1f8abee697 --- /dev/null +++ b/maven-model-builder/src/main/java/org/apache/maven/model/building/Graph.java @@ -0,0 +1,150 @@ +/* + * 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.model.building; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +class Graph { + private enum DfsState { + VISITING, + VISITED + } + + final Map vertices = new LinkedHashMap<>(); + + public Vertex getVertex(String id) { + return vertices.get(id); + } + + public Collection getVertices() { + return vertices.values(); + } + + Vertex addVertex(String label) { + return vertices.computeIfAbsent(label, Vertex::new); + } + + void addEdge(Vertex from, Vertex to) throws CycleDetectedException { + from.children.add(to); + to.parents.add(from); + List cycle = findCycle(to); + if (cycle != null) { + // remove edge which introduced cycle + removeEdge(from, to); + throw new CycleDetectedException( + "Edge between '" + from.label + "' and '" + to.label + "' introduces to cycle in the graph", cycle); + } + } + + void removeEdge(Vertex from, Vertex to) { + from.children.remove(to); + to.parents.remove(from); + } + + List visitAll() { + return visitAll(vertices.values(), new HashMap<>(), new ArrayList<>()); + } + + List findCycle(Vertex vertex) { + return visitCycle(Collections.singleton(vertex), new HashMap<>(), new LinkedList<>()); + } + + private static List visitAll( + Collection children, Map stateMap, List list) { + for (Vertex v : children) { + DfsState state = stateMap.putIfAbsent(v, DfsState.VISITING); + if (state == null) { + visitAll(v.children, stateMap, list); + stateMap.put(v, DfsState.VISITED); + list.add(v.label); + } + } + return list; + } + + private static List visitCycle( + Collection children, Map stateMap, LinkedList cycle) { + for (Vertex v : children) { + DfsState state = stateMap.putIfAbsent(v, DfsState.VISITING); + if (state == null) { + cycle.addLast(v.label); + List ret = visitCycle(v.children, stateMap, cycle); + if (ret != null) { + return ret; + } + cycle.removeLast(); + stateMap.put(v, DfsState.VISITED); + } else if (state == DfsState.VISITING) { + // we are already visiting this vertex, this mean we have a cycle + int pos = cycle.lastIndexOf(v.label); + List ret = cycle.subList(pos, cycle.size()); + ret.add(v.label); + return ret; + } + } + return null; + } + + static class Vertex { + final String label; + final List children = new ArrayList<>(); + final List parents = new ArrayList<>(); + + Vertex(String label) { + this.label = label; + } + + String getLabel() { + return label; + } + + List getChildren() { + return children; + } + + List getParents() { + return parents; + } + } + + public static class CycleDetectedException extends Exception { + private final List cycle; + + CycleDetectedException(String message, List cycle) { + super(message); + this.cycle = cycle; + } + + public List getCycle() { + return cycle; + } + + @Override + public String getMessage() { + return super.getMessage() + " " + String.join(" --> ", cycle); + } + } +}