View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.cling.invoker.mvn;
20  
21  import java.io.FileNotFoundException;
22  import java.nio.file.Files;
23  import java.nio.file.Path;
24  import java.util.ArrayList;
25  import java.util.LinkedHashMap;
26  import java.util.List;
27  import java.util.Map;
28  import java.util.Optional;
29  import java.util.function.Consumer;
30  import java.util.regex.Matcher;
31  import java.util.regex.Pattern;
32  import java.util.stream.Collectors;
33  
34  import org.apache.maven.InternalErrorException;
35  import org.apache.maven.Maven;
36  import org.apache.maven.api.Constants;
37  import org.apache.maven.api.MonotonicClock;
38  import org.apache.maven.api.annotations.Nullable;
39  import org.apache.maven.api.cli.InvokerRequest;
40  import org.apache.maven.api.cli.Logger;
41  import org.apache.maven.api.cli.mvn.MavenOptions;
42  import org.apache.maven.api.services.BuilderProblem;
43  import org.apache.maven.api.services.Lookup;
44  import org.apache.maven.api.services.Sources;
45  import org.apache.maven.api.services.ToolchainsBuilder;
46  import org.apache.maven.api.services.ToolchainsBuilderRequest;
47  import org.apache.maven.api.services.ToolchainsBuilderResult;
48  import org.apache.maven.api.services.model.ModelProcessor;
49  import org.apache.maven.api.toolchain.PersistedToolchains;
50  import org.apache.maven.cling.event.ExecutionEventLogger;
51  import org.apache.maven.cling.invoker.LookupContext;
52  import org.apache.maven.cling.invoker.LookupInvoker;
53  import org.apache.maven.cling.invoker.Utils;
54  import org.apache.maven.cling.transfer.ConsoleMavenTransferListener;
55  import org.apache.maven.cling.transfer.QuietMavenTransferListener;
56  import org.apache.maven.cling.transfer.SimplexTransferListener;
57  import org.apache.maven.cling.transfer.Slf4jMavenTransferListener;
58  import org.apache.maven.exception.DefaultExceptionHandler;
59  import org.apache.maven.exception.ExceptionHandler;
60  import org.apache.maven.exception.ExceptionSummary;
61  import org.apache.maven.execution.DefaultMavenExecutionRequest;
62  import org.apache.maven.execution.ExecutionListener;
63  import org.apache.maven.execution.MavenExecutionRequest;
64  import org.apache.maven.execution.MavenExecutionResult;
65  import org.apache.maven.execution.ProfileActivation;
66  import org.apache.maven.execution.ProjectActivation;
67  import org.apache.maven.jline.MessageUtils;
68  import org.apache.maven.lifecycle.LifecycleExecutionException;
69  import org.apache.maven.logging.LoggingExecutionListener;
70  import org.apache.maven.logging.MavenTransferListener;
71  import org.apache.maven.project.MavenProject;
72  import org.apache.maven.toolchain.model.ToolchainModel;
73  import org.eclipse.aether.DefaultRepositoryCache;
74  import org.eclipse.aether.transfer.TransferListener;
75  
76  import static java.util.Comparator.comparing;
77  
78  /**
79   * The Maven invoker, that expects whole Maven on classpath and invokes it.
80   */
81  public class MavenInvoker extends LookupInvoker<MavenContext> {
82      public MavenInvoker(Lookup protoLookup) {
83          this(protoLookup, null);
84      }
85  
86      public MavenInvoker(Lookup protoLookup, @Nullable Consumer<LookupContext> contextConsumer) {
87          super(protoLookup, contextConsumer);
88      }
89  
90      @Override
91      protected MavenContext createContext(InvokerRequest invokerRequest) {
92          return new MavenContext(invokerRequest);
93      }
94  
95      @Override
96      protected int execute(MavenContext context) throws Exception {
97          MavenExecutionRequest request = prepareMavenExecutionRequest();
98          toolchains(context, request);
99          populateRequest(context, context.lookup, request);
100         return doExecute(context, request);
101     }
102 
103     protected MavenExecutionRequest prepareMavenExecutionRequest() throws Exception {
104         // explicitly fill in "defaults"?
105         DefaultMavenExecutionRequest mavenExecutionRequest = new DefaultMavenExecutionRequest();
106         mavenExecutionRequest.setRepositoryCache(new DefaultRepositoryCache());
107         mavenExecutionRequest.setInteractiveMode(true);
108         mavenExecutionRequest.setCacheTransferError(false);
109         mavenExecutionRequest.setIgnoreInvalidArtifactDescriptor(true);
110         mavenExecutionRequest.setIgnoreMissingArtifactDescriptor(true);
111         mavenExecutionRequest.setRecursive(true);
112         mavenExecutionRequest.setReactorFailureBehavior(MavenExecutionRequest.REACTOR_FAIL_FAST);
113         mavenExecutionRequest.setStartInstant(MonotonicClock.now());
114         mavenExecutionRequest.setLoggingLevel(MavenExecutionRequest.LOGGING_LEVEL_INFO);
115         mavenExecutionRequest.setDegreeOfConcurrency(1);
116         mavenExecutionRequest.setBuilderId("singlethreaded");
117         return mavenExecutionRequest;
118     }
119 
120     @Override
121     protected void lookup(MavenContext context) throws Exception {
122         if (context.maven == null) {
123             super.lookup(context);
124             context.maven = context.lookup.lookup(Maven.class);
125         }
126     }
127 
128     @Override
129     protected void postCommands(MavenContext context) throws Exception {
130         super.postCommands(context);
131 
132         InvokerRequest invokerRequest = context.invokerRequest;
133         MavenOptions options = (MavenOptions) invokerRequest.options();
134         Logger logger = context.logger;
135         if (options.relaxedChecksums().orElse(false)) {
136             logger.info("Disabling strict checksum verification on all artifact downloads.");
137         } else if (options.strictChecksums().orElse(false)) {
138             logger.info("Enabling strict checksum verification on all artifact downloads.");
139         }
140     }
141 
142     protected void toolchains(MavenContext context, MavenExecutionRequest request) throws Exception {
143         Path userToolchainsFile = null;
144         if (context.invokerRequest.options().altUserToolchains().isPresent()) {
145             userToolchainsFile = context.cwd.resolve(
146                     context.invokerRequest.options().altUserToolchains().get());
147 
148             if (!Files.isRegularFile(userToolchainsFile)) {
149                 throw new FileNotFoundException(
150                         "The specified user toolchains file does not exist: " + userToolchainsFile);
151             }
152         } else {
153             String userToolchainsFileStr =
154                     context.protoSession.getUserProperties().get(Constants.MAVEN_USER_TOOLCHAINS);
155             if (userToolchainsFileStr != null) {
156                 userToolchainsFile = context.cwd.resolve(userToolchainsFileStr);
157             }
158         }
159 
160         Path installationToolchainsFile = null;
161         if (context.invokerRequest.options().altInstallationToolchains().isPresent()) {
162             installationToolchainsFile = context.cwd.resolve(
163                     context.invokerRequest.options().altInstallationToolchains().get());
164 
165             if (!Files.isRegularFile(installationToolchainsFile)) {
166                 throw new FileNotFoundException(
167                         "The specified installation toolchains file does not exist: " + installationToolchainsFile);
168             }
169         } else {
170             String installationToolchainsFileStr =
171                     context.protoSession.getUserProperties().get(Constants.MAVEN_INSTALLATION_TOOLCHAINS);
172             if (installationToolchainsFileStr != null) {
173                 installationToolchainsFile = context.installationDirectory
174                         .resolve(installationToolchainsFileStr)
175                         .normalize();
176             }
177         }
178 
179         request.setInstallationToolchainsFile(
180                 installationToolchainsFile != null ? installationToolchainsFile.toFile() : null);
181         request.setUserToolchainsFile(userToolchainsFile != null ? userToolchainsFile.toFile() : null);
182 
183         ToolchainsBuilderRequest toolchainsRequest = ToolchainsBuilderRequest.builder()
184                 .session(context.protoSession)
185                 .installationToolchainsSource(
186                         installationToolchainsFile != null && Files.isRegularFile(installationToolchainsFile)
187                                 ? Sources.fromPath(installationToolchainsFile)
188                                 : null)
189                 .userToolchainsSource(
190                         userToolchainsFile != null && Files.isRegularFile(userToolchainsFile)
191                                 ? Sources.fromPath(userToolchainsFile)
192                                 : null)
193                 .build();
194 
195         context.eventSpyDispatcher.onEvent(toolchainsRequest);
196 
197         context.logger.debug("Reading installation toolchains from '" + installationToolchainsFile + "'");
198         context.logger.debug("Reading user toolchains from '" + userToolchainsFile + "'");
199 
200         ToolchainsBuilderResult toolchainsResult =
201                 context.lookup.lookup(ToolchainsBuilder.class).build(toolchainsRequest);
202 
203         context.eventSpyDispatcher.onEvent(toolchainsResult);
204 
205         context.effectiveToolchains = toolchainsResult.getEffectiveToolchains();
206 
207         if (toolchainsResult.getProblems().hasWarningProblems()) {
208             int totalProblems = toolchainsResult.getProblems().totalProblemsReported();
209             context.logger.info("");
210             context.logger.info(String.format(
211                     "%s %s encountered while building the effective toolchains (use -e to see details)",
212                     totalProblems, (totalProblems == 1) ? "problem was" : "problems were"));
213 
214             if (context.invokerRequest.options().showErrors().orElse(false)) {
215                 for (BuilderProblem problem :
216                         toolchainsResult.getProblems().problems().toList()) {
217                     context.logger.warn(problem.getMessage() + " @ " + problem.getLocation());
218                 }
219             }
220 
221             context.logger.info("");
222         }
223     }
224 
225     @Override
226     protected void populateRequest(MavenContext context, Lookup lookup, MavenExecutionRequest request)
227             throws Exception {
228         super.populateRequest(context, lookup, request);
229         if (context.invokerRequest.rootDirectory().isEmpty()) {
230             // maven requires this to be set; so default it (and see below at POM)
231             request.setMultiModuleProjectDirectory(
232                     context.invokerRequest.topDirectory().toFile());
233             request.setRootDirectory(context.invokerRequest.topDirectory());
234         }
235 
236         request.setToolchains(
237                 Optional.ofNullable(context.effectiveToolchains).map(PersistedToolchains::getToolchains).stream()
238                         .flatMap(List::stream)
239                         .map(ToolchainModel::new)
240                         .collect(Collectors.groupingBy(ToolchainModel::getType)));
241 
242         MavenOptions options = (MavenOptions) context.invokerRequest.options();
243         request.setNoSnapshotUpdates(options.suppressSnapshotUpdates().orElse(false));
244         request.setGoals(options.goals().orElse(List.of()));
245         request.setReactorFailureBehavior(determineReactorFailureBehaviour(context));
246         request.setRecursive(!options.nonRecursive().orElse(!request.isRecursive()));
247         request.setOffline(options.offline().orElse(request.isOffline()));
248         request.setUpdateSnapshots(options.updateSnapshots().orElse(false));
249         request.setGlobalChecksumPolicy(determineGlobalChecksumPolicy(context));
250 
251         Path pom = determinePom(context, lookup);
252         if (pom != null) {
253             request.setPom(pom.toFile());
254             if (pom.getParent() != null) {
255                 request.setBaseDirectory(pom.getParent().toFile());
256             }
257 
258             // project present, but we could not determine rootDirectory: extra work needed
259             if (context.invokerRequest.rootDirectory().isEmpty()) {
260                 Path rootDirectory = Utils.findMandatoryRoot(context.invokerRequest.topDirectory());
261                 request.setMultiModuleProjectDirectory(rootDirectory.toFile());
262                 request.setRootDirectory(rootDirectory);
263             }
264         }
265 
266         request.setTransferListener(
267                 determineTransferListener(context, options.noTransferProgress().orElse(false)));
268         request.setExecutionListener(determineExecutionListener(context));
269 
270         request.setResumeFrom(options.resumeFrom().orElse(null));
271         request.setResume(options.resume().orElse(false));
272         request.setMakeBehavior(determineMakeBehavior(context));
273         request.setCacheNotFound(options.cacheArtifactNotFound().orElse(true));
274         request.setCacheTransferError(false);
275 
276         if (options.strictArtifactDescriptorPolicy().orElse(false)) {
277             request.setIgnoreMissingArtifactDescriptor(false);
278             request.setIgnoreInvalidArtifactDescriptor(false);
279         } else {
280             request.setIgnoreMissingArtifactDescriptor(true);
281             request.setIgnoreInvalidArtifactDescriptor(true);
282         }
283 
284         request.setIgnoreTransitiveRepositories(
285                 options.ignoreTransitiveRepositories().orElse(false));
286 
287         performProjectActivation(context, request.getProjectActivation());
288         performProfileActivation(context, request.getProfileActivation());
289 
290         //
291         // Builder, concurrency and parallelism
292         //
293         // We preserve the existing methods for builder selection which is to look for various inputs in the threading
294         // configuration. We don't have an easy way to allow a pluggable builder to provide its own configuration
295         // parameters but this is sufficient for now. Ultimately we want components like Builders to provide a way to
296         // extend the command line to accept its own configuration parameters.
297         //
298         if (options.threads().isPresent()) {
299             int degreeOfConcurrency =
300                     calculateDegreeOfConcurrency(options.threads().get());
301             if (degreeOfConcurrency > 1) {
302                 request.setBuilderId("multithreaded");
303                 request.setDegreeOfConcurrency(degreeOfConcurrency);
304             }
305         }
306 
307         //
308         // Allow the builder to be overridden by the user if requested. The builders are now pluggable.
309         //
310         if (options.builder().isPresent()) {
311             request.setBuilderId(options.builder().get());
312         }
313     }
314 
315     protected Path determinePom(MavenContext context, Lookup lookup) {
316         Path current = context.cwd.get();
317         MavenOptions options = (MavenOptions) context.invokerRequest.options();
318         if (options.alternatePomFile().isPresent()) {
319             current = context.cwd.resolve(options.alternatePomFile().get());
320         }
321         ModelProcessor modelProcessor =
322                 lookup.lookupOptional(ModelProcessor.class).orElse(null);
323         if (modelProcessor != null) {
324             return modelProcessor.locateExistingPom(current);
325         } else {
326             return Files.isRegularFile(current) ? current : null;
327         }
328     }
329 
330     protected String determineReactorFailureBehaviour(MavenContext context) {
331         MavenOptions mavenOptions = (MavenOptions) context.invokerRequest.options();
332         if (mavenOptions.failFast().isPresent()) {
333             return MavenExecutionRequest.REACTOR_FAIL_FAST;
334         } else if (mavenOptions.failAtEnd().isPresent()) {
335             return MavenExecutionRequest.REACTOR_FAIL_AT_END;
336         } else if (mavenOptions.failNever().isPresent()) {
337             return MavenExecutionRequest.REACTOR_FAIL_NEVER;
338         } else {
339             return MavenExecutionRequest.REACTOR_FAIL_FAST;
340         }
341     }
342 
343     protected String determineGlobalChecksumPolicy(MavenContext context) {
344         MavenOptions mavenOptions = (MavenOptions) context.invokerRequest.options();
345         if (mavenOptions.strictChecksums().orElse(false)) {
346             return MavenExecutionRequest.CHECKSUM_POLICY_FAIL;
347         } else if (mavenOptions.relaxedChecksums().orElse(false)) {
348             return MavenExecutionRequest.CHECKSUM_POLICY_WARN;
349         } else {
350             return null;
351         }
352     }
353 
354     protected ExecutionListener determineExecutionListener(MavenContext context) {
355         ExecutionListener listener = new ExecutionEventLogger(context.invokerRequest.messageBuilderFactory());
356         if (context.eventSpyDispatcher != null) {
357             listener = context.eventSpyDispatcher.chainListener(listener);
358         }
359         return new LoggingExecutionListener(listener, determineBuildEventListener(context));
360     }
361 
362     protected TransferListener determineTransferListener(MavenContext context, boolean noTransferProgress) {
363         boolean quiet = context.invokerRequest.options().quiet().orElse(false);
364         boolean logFile = context.invokerRequest.options().logFile().isPresent();
365         boolean runningOnCI = isRunningOnCI(context);
366         boolean quietCI = runningOnCI
367                 && !context.invokerRequest.options().forceInteractive().orElse(false);
368 
369         TransferListener delegate;
370         if (quiet || noTransferProgress || quietCI) {
371             delegate = new QuietMavenTransferListener();
372         } else if (context.interactive && !logFile) {
373             SimplexTransferListener simplex = new SimplexTransferListener(new ConsoleMavenTransferListener(
374                     context.invokerRequest.messageBuilderFactory(),
375                     context.terminal.writer(),
376                     context.invokerRequest.options().verbose().orElse(false)));
377             context.closeables.add(simplex);
378             delegate = simplex;
379         } else {
380             delegate = new Slf4jMavenTransferListener();
381         }
382         return new MavenTransferListener(delegate, determineBuildEventListener(context));
383     }
384 
385     protected String determineMakeBehavior(MavenContext context) {
386         MavenOptions mavenOptions = (MavenOptions) context.invokerRequest.options();
387         if (mavenOptions.alsoMake().isPresent()
388                 && mavenOptions.alsoMakeDependents().isEmpty()) {
389             return MavenExecutionRequest.REACTOR_MAKE_UPSTREAM;
390         } else if (mavenOptions.alsoMake().isEmpty()
391                 && mavenOptions.alsoMakeDependents().isPresent()) {
392             return MavenExecutionRequest.REACTOR_MAKE_DOWNSTREAM;
393         } else if (mavenOptions.alsoMake().isPresent()
394                 && mavenOptions.alsoMakeDependents().isPresent()) {
395             return MavenExecutionRequest.REACTOR_MAKE_BOTH;
396         } else {
397             return null;
398         }
399     }
400 
401     protected void performProjectActivation(MavenContext context, ProjectActivation projectActivation) {
402         MavenOptions mavenOptions = (MavenOptions) context.invokerRequest.options();
403         if (mavenOptions.projects().isPresent()
404                 && !mavenOptions.projects().get().isEmpty()) {
405             List<String> optionValues = mavenOptions.projects().get();
406             for (final String optionValue : optionValues) {
407                 for (String token : optionValue.split(",")) {
408                     String selector = token.trim();
409                     boolean active = true;
410                     if (!selector.isEmpty()) {
411                         if (selector.charAt(0) == '-' || selector.charAt(0) == '!') {
412                             active = false;
413                             selector = selector.substring(1);
414                         } else if (token.charAt(0) == '+') {
415                             selector = selector.substring(1);
416                         }
417                     }
418                     boolean optional = false;
419                     if (!selector.isEmpty() && selector.charAt(0) == '?') {
420                         optional = true;
421                         selector = selector.substring(1);
422                     }
423                     projectActivation.addProjectActivation(selector, active, optional);
424                 }
425             }
426         }
427     }
428 
429     protected void performProfileActivation(MavenContext context, ProfileActivation profileActivation) {
430         MavenOptions mavenOptions = (MavenOptions) context.invokerRequest.options();
431         if (mavenOptions.activatedProfiles().isPresent()
432                 && !mavenOptions.activatedProfiles().get().isEmpty()) {
433             List<String> optionValues = mavenOptions.activatedProfiles().get();
434             for (final String optionValue : optionValues) {
435                 for (String token : optionValue.split(",")) {
436                     String profileId = token.trim();
437                     boolean active = true;
438                     if (!profileId.isEmpty()) {
439                         if (profileId.charAt(0) == '-' || profileId.charAt(0) == '!') {
440                             active = false;
441                             profileId = profileId.substring(1);
442                         } else if (token.charAt(0) == '+') {
443                             profileId = profileId.substring(1);
444                         }
445                     }
446                     boolean optional = false;
447                     if (!profileId.isEmpty() && profileId.charAt(0) == '?') {
448                         optional = true;
449                         profileId = profileId.substring(1);
450                     }
451                     profileActivation.addProfileActivation(profileId, active, optional);
452                 }
453             }
454         }
455     }
456 
457     protected int doExecute(MavenContext context, MavenExecutionRequest request) throws Exception {
458         context.eventSpyDispatcher.onEvent(request);
459 
460         MavenExecutionResult result;
461         try {
462             result = context.maven.execute(request);
463             context.eventSpyDispatcher.onEvent(result);
464         } finally {
465             context.eventSpyDispatcher.close();
466         }
467 
468         if (result.hasExceptions()) {
469             ExceptionHandler handler = new DefaultExceptionHandler();
470             Map<String, String> references = new LinkedHashMap<>();
471             List<MavenProject> failedProjects = new ArrayList<>();
472 
473             for (Throwable exception : result.getExceptions()) {
474                 ExceptionSummary summary = handler.handleException(exception);
475                 logSummary(context, summary, references, "");
476 
477                 if (exception instanceof LifecycleExecutionException lifecycleExecutionException) {
478                     failedProjects.add(lifecycleExecutionException.getProject());
479                 }
480             }
481 
482             context.logger.error("");
483 
484             if (!context.invokerRequest.options().showErrors().orElse(false)) {
485                 context.logger.error("To see the full stack trace of the errors, re-run Maven with the '"
486                         + MessageUtils.builder().strong("-e") + "' switch");
487             }
488             if (!context.invokerRequest.options().verbose().orElse(false)) {
489                 context.logger.error("Re-run Maven using the '"
490                         + MessageUtils.builder().strong("-X") + "' switch to enable verbose output");
491             }
492 
493             if (!references.isEmpty()) {
494                 context.logger.error("");
495                 context.logger.error("For more information about the errors and possible solutions"
496                         + ", please read the following articles:");
497 
498                 for (Map.Entry<String, String> entry : references.entrySet()) {
499                     context.logger.error(MessageUtils.builder().strong(entry.getValue()) + " " + entry.getKey());
500                 }
501             }
502 
503             if (result.canResume()) {
504                 logBuildResumeHint(context, "mvn [args] -r");
505             } else if (!failedProjects.isEmpty()) {
506                 List<MavenProject> sortedProjects = result.getTopologicallySortedProjects();
507 
508                 // Sort the failedProjects list in the topologically sorted order.
509                 failedProjects.sort(comparing(sortedProjects::indexOf));
510 
511                 MavenProject firstFailedProject = failedProjects.get(0);
512                 if (!firstFailedProject.equals(sortedProjects.get(0))) {
513                     String resumeFromSelector = getResumeFromSelector(sortedProjects, firstFailedProject);
514                     logBuildResumeHint(context, "mvn [args] -rf " + resumeFromSelector);
515                 }
516             }
517 
518             if (((MavenOptions) context.invokerRequest.options()).failNever().orElse(false)) {
519                 context.logger.info("Build failures were ignored.");
520                 return 0;
521             } else {
522                 return 1;
523             }
524         } else {
525             return 0;
526         }
527     }
528 
529     protected void logBuildResumeHint(MavenContext context, String resumeBuildHint) {
530         context.logger.error("");
531         context.logger.error("After correcting the problems, you can resume the build with the command");
532         context.logger.error(
533                 MessageUtils.builder().a("  ").strong(resumeBuildHint).toString());
534     }
535 
536     /**
537      * A helper method to determine the value to resume the build with {@code -rf} taking into account the edge case
538      *   where multiple modules in the reactor have the same artifactId.
539      * <p>
540      * {@code -rf :artifactId} will pick up the first module which matches, but when multiple modules in the reactor
541      *   have the same artifactId, effective failed module might be later in build reactor.
542      * This means that developer will either have to type groupId or wait for build execution of all modules which
543      *   were fine, but they are still before one which reported errors.
544      * <p>Then the returned value is {@code groupId:artifactId} when there is a name clash and
545      * {@code :artifactId} if there is no conflict.
546      * This method is made package-private for testing purposes.
547      *
548      * @param mavenProjects Maven projects which are part of build execution.
549      * @param firstFailedProject The first project which has failed.
550      * @return Value for -rf flag to resume build exactly from place where it failed ({@code :artifactId} in general
551      * and {@code groupId:artifactId} when there is a name clash).
552      */
553     protected String getResumeFromSelector(List<MavenProject> mavenProjects, MavenProject firstFailedProject) {
554         boolean hasOverlappingArtifactId = mavenProjects.stream()
555                         .filter(project -> firstFailedProject.getArtifactId().equals(project.getArtifactId()))
556                         .count()
557                 > 1;
558 
559         if (hasOverlappingArtifactId) {
560             return firstFailedProject.getGroupId() + ":" + firstFailedProject.getArtifactId();
561         }
562 
563         return ":" + firstFailedProject.getArtifactId();
564     }
565 
566     protected static final Pattern NEXT_LINE = Pattern.compile("\r?\n");
567 
568     protected static final Pattern LAST_ANSI_SEQUENCE = Pattern.compile("(\u001B\\[[;\\d]*[ -/]*[@-~])[^\u001B]*$");
569 
570     protected static final String ANSI_RESET = "\u001B\u005Bm";
571 
572     protected void logSummary(
573             MavenContext context, ExceptionSummary summary, Map<String, String> references, String indent) {
574         String referenceKey = "";
575 
576         if (summary.getReference() != null && !summary.getReference().isEmpty()) {
577             referenceKey =
578                     references.computeIfAbsent(summary.getReference(), k -> "[Help " + (references.size() + 1) + "]");
579         }
580 
581         String msg = summary.getMessage();
582 
583         if (!referenceKey.isEmpty()) {
584             if (msg.indexOf('\n') < 0) {
585                 msg += " -> " + MessageUtils.builder().strong(referenceKey);
586             } else {
587                 msg += "\n-> " + MessageUtils.builder().strong(referenceKey);
588             }
589         }
590 
591         String[] lines = NEXT_LINE.split(msg);
592         String currentColor = "";
593 
594         for (int i = 0; i < lines.length; i++) {
595             // add eventual current color inherited from previous line
596             String line = currentColor + lines[i];
597 
598             // look for last ANSI escape sequence to check if nextColor
599             Matcher matcher = LAST_ANSI_SEQUENCE.matcher(line);
600             String nextColor = "";
601             if (matcher.find()) {
602                 nextColor = matcher.group(1);
603                 if (ANSI_RESET.equals(nextColor)) {
604                     // last ANSI escape code is reset: no next color
605                     nextColor = "";
606                 }
607             }
608 
609             // effective line, with indent and reset if end is colored
610             line = indent + line + ("".equals(nextColor) ? "" : ANSI_RESET);
611 
612             if ((i == lines.length - 1)
613                     && (context.invokerRequest.options().showErrors().orElse(false)
614                             || (summary.getException() instanceof InternalErrorException))) {
615                 context.logger.error(line, summary.getException());
616             } else {
617                 context.logger.error(line);
618             }
619 
620             currentColor = nextColor;
621         }
622 
623         indent += "  ";
624 
625         for (ExceptionSummary child : summary.getChildren()) {
626             logSummary(context, child, references, indent);
627         }
628     }
629 }