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