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