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;
20  
21  import java.io.FileNotFoundException;
22  import java.io.IOException;
23  import java.io.InputStream;
24  import java.io.OutputStream;
25  import java.io.PrintStream;
26  import java.io.PrintWriter;
27  import java.nio.file.Files;
28  import java.nio.file.Path;
29  import java.util.ArrayList;
30  import java.util.HashMap;
31  import java.util.HashSet;
32  import java.util.LinkedHashMap;
33  import java.util.List;
34  import java.util.Locale;
35  import java.util.Map;
36  import java.util.Objects;
37  import java.util.Properties;
38  import java.util.function.Consumer;
39  import java.util.function.UnaryOperator;
40  
41  import org.apache.maven.api.Constants;
42  import org.apache.maven.api.ProtoSession;
43  import org.apache.maven.api.annotations.Nullable;
44  import org.apache.maven.api.cli.Invoker;
45  import org.apache.maven.api.cli.InvokerException;
46  import org.apache.maven.api.cli.InvokerRequest;
47  import org.apache.maven.api.cli.Logger;
48  import org.apache.maven.api.cli.cisupport.CIInfo;
49  import org.apache.maven.api.cli.logging.AccumulatingLogger;
50  import org.apache.maven.api.services.BuilderProblem;
51  import org.apache.maven.api.services.Lookup;
52  import org.apache.maven.api.services.MavenException;
53  import org.apache.maven.api.services.MessageBuilder;
54  import org.apache.maven.api.services.SettingsBuilder;
55  import org.apache.maven.api.services.SettingsBuilderRequest;
56  import org.apache.maven.api.services.SettingsBuilderResult;
57  import org.apache.maven.api.services.Sources;
58  import org.apache.maven.api.settings.Mirror;
59  import org.apache.maven.api.settings.Profile;
60  import org.apache.maven.api.settings.Proxy;
61  import org.apache.maven.api.settings.Repository;
62  import org.apache.maven.api.settings.Server;
63  import org.apache.maven.api.settings.Settings;
64  import org.apache.maven.api.spi.PropertyContributor;
65  import org.apache.maven.artifact.repository.ArtifactRepository;
66  import org.apache.maven.artifact.repository.ArtifactRepositoryPolicy;
67  import org.apache.maven.artifact.repository.MavenArtifactRepository;
68  import org.apache.maven.artifact.repository.layout.DefaultRepositoryLayout;
69  import org.apache.maven.bridge.MavenRepositorySystem;
70  import org.apache.maven.cling.invoker.logging.Slf4jLogger;
71  import org.apache.maven.cling.invoker.logging.SystemLogger;
72  import org.apache.maven.cling.invoker.spi.PropertyContributorsHolder;
73  import org.apache.maven.cling.logging.Slf4jConfiguration;
74  import org.apache.maven.cling.logging.Slf4jConfigurationFactory;
75  import org.apache.maven.cling.utils.CLIReportingUtils;
76  import org.apache.maven.eventspy.internal.EventSpyDispatcher;
77  import org.apache.maven.execution.MavenExecutionRequest;
78  import org.apache.maven.impl.SettingsUtilsV4;
79  import org.apache.maven.jline.FastTerminal;
80  import org.apache.maven.jline.MessageUtils;
81  import org.apache.maven.logging.BuildEventListener;
82  import org.apache.maven.logging.LoggingOutputStream;
83  import org.apache.maven.logging.ProjectBuildLogAppender;
84  import org.apache.maven.logging.SimpleBuildEventListener;
85  import org.apache.maven.logging.api.LogLevelRecorder;
86  import org.apache.maven.slf4j.MavenSimpleLogger;
87  import org.codehaus.plexus.PlexusContainer;
88  import org.jline.terminal.Terminal;
89  import org.jline.terminal.TerminalBuilder;
90  import org.jline.terminal.impl.AbstractPosixTerminal;
91  import org.jline.terminal.spi.TerminalExt;
92  import org.slf4j.LoggerFactory;
93  import org.slf4j.spi.LocationAwareLogger;
94  
95  import static java.util.Objects.requireNonNull;
96  import static org.apache.maven.cling.invoker.CliUtils.toMavenExecutionRequestLoggingLevel;
97  import static org.apache.maven.cling.invoker.CliUtils.toProperties;
98  
99  /**
100  * Lookup invoker implementation, that boots up DI container.
101  *
102  * @param <C> The context type.
103  */
104 public abstract class LookupInvoker<C extends LookupContext> implements Invoker {
105     protected final Lookup protoLookup;
106 
107     @Nullable
108     protected final Consumer<LookupContext> contextConsumer;
109 
110     public LookupInvoker(Lookup protoLookup, @Nullable Consumer<LookupContext> contextConsumer) {
111         this.protoLookup = requireNonNull(protoLookup);
112         this.contextConsumer = contextConsumer;
113     }
114 
115     @Override
116     public final int invoke(InvokerRequest invokerRequest) {
117         requireNonNull(invokerRequest);
118 
119         Properties oldProps = new Properties();
120         oldProps.putAll(System.getProperties());
121         ClassLoader oldCL = Thread.currentThread().getContextClassLoader();
122         try (C context = createContext(invokerRequest)) {
123             if (contextConsumer != null) {
124                 contextConsumer.accept(context);
125             }
126             try {
127                 if (context.containerCapsule != null
128                         && context.containerCapsule.currentThreadClassLoader().isPresent()) {
129                     Thread.currentThread()
130                             .setContextClassLoader(context.containerCapsule
131                                     .currentThreadClassLoader()
132                                     .get());
133                 }
134                 return doInvoke(context);
135             } catch (InvokerException.ExitException e) {
136                 // contract of ExitException is that nothing needed by us
137                 throw e;
138             } catch (Exception e) {
139                 // other exceptions (including InvokerException but sans Exit, see above): we need to inform user
140                 throw handleException(context, e);
141             }
142         } finally {
143             Thread.currentThread().setContextClassLoader(oldCL);
144             System.setProperties(oldProps);
145         }
146     }
147 
148     protected int doInvoke(C context) throws Exception {
149         validate(context);
150         pushCoreProperties(context);
151         pushUserProperties(context);
152         setupGuiceClassLoading(context);
153         configureLogging(context);
154         createTerminal(context);
155         activateLogging(context);
156         helpOrVersionAndMayExit(context);
157         preCommands(context);
158         container(context);
159         postContainer(context);
160         pushUserProperties(context); // after PropertyContributor SPI
161         lookup(context);
162         init(context);
163         postCommands(context);
164         settings(context);
165         return execute(context);
166     }
167 
168     protected InvokerException.ExitException handleException(C context, Exception e) {
169         printErrors(
170                 context,
171                 context.options().showErrors().orElse(false),
172                 List.of(new Logger.Entry(Logger.Level.ERROR, e.getMessage(), e.getCause())),
173                 context.logger);
174         return new InvokerException.ExitException(2);
175     }
176 
177     protected void printErrors(C context, boolean showStackTrace, List<Logger.Entry> entries, Logger logger) {
178         // if accumulating logger passed, this is "early failure", swap logger for stdErr and use that to emit log
179         if (logger instanceof AccumulatingLogger) {
180             logger = new SystemLogger(context.invokerRequest.stdErr().orElse(null));
181         }
182         // this is important message; many Maven IT assert for presence of this message
183         logger.error("Error executing " + context.invokerRequest.parserRequest().commandName() + ".");
184         for (Logger.Entry entry : entries) {
185             if (showStackTrace) {
186                 logger.log(entry.level(), entry.message(), entry.error());
187             } else {
188                 logger.error(entry.message());
189                 for (Throwable cause = entry.error();
190                         cause != null && cause != cause.getCause();
191                         cause = cause.getCause()) {
192                     logger.log(entry.level(), "Caused by: " + cause.getMessage());
193                 }
194             }
195         }
196     }
197 
198     protected abstract C createContext(InvokerRequest invokerRequest);
199 
200     protected void validate(C context) throws Exception {
201         if (context.invokerRequest.parsingFailed()) {
202             // in case of parser errors: report errors and bail out; invokerRequest contents may be incomplete
203             // in case of mvnsh the context.logger != context.invokerRequest.parserRequest.logger
204             List<Logger.Entry> entries =
205                     context.invokerRequest.parserRequest().logger().drain();
206             printErrors(
207                     context,
208                     context.invokerRequest
209                             .parserRequest()
210                             .args()
211                             .contains(CommonsCliOptions.CLIManager.SHOW_ERRORS_CLI_ARG),
212                     entries,
213                     context.logger);
214             // we skip handleException above as we did output
215             throw new InvokerException.ExitException(1);
216         }
217 
218         // warn about deprecated options
219         context.options().warnAboutDeprecatedOptions(context.invokerRequest.parserRequest(), context.logger::warn);
220     }
221 
222     protected void pushCoreProperties(C context) throws Exception {
223         System.setProperty(
224                 Constants.MAVEN_HOME,
225                 context.invokerRequest.installationDirectory().toString());
226     }
227 
228     /**
229      * Note: this method is called twice from {@link #doInvoke(LookupContext)} and modifies context. First invocation
230      * when {@link LookupContext#pushedUserProperties} is null will push user properties IF key does not already
231      * exist among Java System Properties, and collects all they key it pushes. Second invocation happens AFTER
232      * {@link PropertyContributor} SPI invocation, and "refreshes" already pushed user properties by re-writing them
233      * as SPI may have modified them.
234      */
235     protected void pushUserProperties(C context) throws Exception {
236         ProtoSession protoSession = context.protoSession;
237         HashSet<String> sys = new HashSet<>(protoSession.getSystemProperties().keySet());
238         if (context.pushedUserProperties == null) {
239             context.pushedUserProperties = new HashSet<>();
240             protoSession.getUserProperties().entrySet().stream()
241                     .filter(k -> !sys.contains(k.getKey()))
242                     .peek(k -> context.pushedUserProperties.add(k.getKey()))
243                     .forEach(k -> System.setProperty(k.getKey(), k.getValue()));
244         } else {
245             protoSession.getUserProperties().entrySet().stream()
246                     .filter(k -> context.pushedUserProperties.contains(k.getKey()) || !sys.contains(k.getKey()))
247                     .forEach(k -> System.setProperty(k.getKey(), k.getValue()));
248         }
249     }
250 
251     /**
252      * Sets up Guice class loading mode to CHILD, if not already set.
253      * Default Guice class loading mode uses a terminally deprecated JDK memory-access classes.
254      */
255     protected void setupGuiceClassLoading(C context) {
256         if (System.getProperty("guice_custom_class_loading", "").isBlank()) {
257             System.setProperty("guice_custom_class_loading", "CHILD");
258         }
259     }
260 
261     protected void configureLogging(C context) throws Exception {
262         // LOG COLOR
263         Map<String, String> effectiveProperties = context.protoSession.getEffectiveProperties();
264         String styleColor = context.options()
265                 .color()
266                 .orElse(effectiveProperties.getOrDefault(
267                         Constants.MAVEN_STYLE_COLOR_PROPERTY, effectiveProperties.getOrDefault("style.color", "auto")))
268                 .toLowerCase(Locale.ENGLISH);
269         if ("always".equals(styleColor) || "yes".equals(styleColor) || "force".equals(styleColor)) {
270             context.coloredOutput = true;
271         } else if ("never".equals(styleColor) || "no".equals(styleColor) || "none".equals(styleColor)) {
272             context.coloredOutput = false;
273         } else if (!"auto".equals(styleColor) && !"tty".equals(styleColor) && !"if-tty".equals(styleColor)) {
274             throw new IllegalArgumentException(
275                     "Invalid color configuration value '" + styleColor + "'. Supported are 'auto', 'always', 'never'.");
276         } else {
277             boolean isBatchMode = !context.options().forceInteractive().orElse(false)
278                     && context.options().nonInteractive().orElse(false);
279             if (isBatchMode || context.options().logFile().isPresent()) {
280                 context.coloredOutput = false;
281             }
282         }
283 
284         context.loggerFactory = LoggerFactory.getILoggerFactory();
285         context.slf4jConfiguration = Slf4jConfigurationFactory.getConfiguration(context.loggerFactory);
286 
287         if (context.invokerRequest.effectiveVerbose()) {
288             context.loggerLevel = Slf4jConfiguration.Level.DEBUG;
289             context.slf4jConfiguration.setRootLoggerLevel(context.loggerLevel);
290         } else if (context.options().quiet().orElse(false)) {
291             context.loggerLevel = Slf4jConfiguration.Level.ERROR;
292             context.slf4jConfiguration.setRootLoggerLevel(context.loggerLevel);
293         } else {
294             // fall back to default log level specified in conf
295             // see https://issues.apache.org/jira/browse/MNG-2570 and https://github.com/apache/maven/issues/11199
296             context.loggerLevel = Slf4jConfiguration.Level.INFO; // default for display purposes
297         }
298     }
299 
300     protected BuildEventListener determineBuildEventListener(C context) {
301         if (context.buildEventListener == null) {
302             context.buildEventListener = doDetermineBuildEventListener(context);
303         }
304         return context.buildEventListener;
305     }
306 
307     protected BuildEventListener doDetermineBuildEventListener(C context) {
308         Consumer<String> writer = determineWriter(context);
309         return new SimpleBuildEventListener(writer);
310     }
311 
312     protected final void createTerminal(C context) {
313         if (context.terminal == null) {
314             // Create the build log appender; also sets MavenSimpleLogger sink
315             ProjectBuildLogAppender projectBuildLogAppender =
316                     new ProjectBuildLogAppender(determineBuildEventListener(context));
317             context.closeables.add(projectBuildLogAppender);
318 
319             MessageUtils.systemInstall(
320                     builder -> doCreateTerminal(context, builder),
321                     terminal -> doConfigureWithTerminal(context, terminal));
322 
323             context.terminal = MessageUtils.getTerminal();
324             context.closeables.add(MessageUtils::systemUninstall);
325             MessageUtils.registerShutdownHook(); // safety belt
326         } else {
327             doConfigureWithTerminal(context, context.terminal);
328         }
329     }
330 
331     /**
332      * Override this method to create Terminal as you want.
333      *
334      * @see #createTerminal(LookupContext)
335      */
336     protected void doCreateTerminal(C context, TerminalBuilder builder) {
337         if (context.invokerRequest.embedded()) {
338             InputStream in = context.invokerRequest.stdIn().orElse(InputStream.nullInputStream());
339             OutputStream out = context.invokerRequest.stdOut().orElse(OutputStream.nullOutputStream());
340             builder.streams(in, out);
341             builder.provider(TerminalBuilder.PROP_PROVIDER_EXEC);
342             context.coloredOutput = context.coloredOutput != null ? context.coloredOutput : false;
343             context.closeables.add(out::flush);
344         } else {
345             builder.systemOutput(TerminalBuilder.SystemOutput.ForcedSysOut);
346         }
347         if (context.coloredOutput != null) {
348             builder.color(context.coloredOutput);
349         }
350     }
351 
352     /**
353      * Called from {@link #createTerminal(LookupContext)} when Terminal was built.
354      */
355     protected final void doConfigureWithTerminal(C context, Terminal terminal) {
356         context.terminal = terminal;
357         // tricky thing: align what JLine3 detected and Maven thinks:
358         // if embedded, we default to context.coloredOutput=false unless overridden (see above)
359         // if not embedded, JLine3 may detect redirection and will create dumb terminal.
360         // To align Maven with outcomes, we set here color enabled based on these premises.
361         // Note: Maven3 suffers from similar thing: if you do `mvn3 foo > log.txt`, the output will
362         // not be not colored (good), but Maven will print out "Message scheme: color".
363         MessageUtils.setColorEnabled(
364                 context.coloredOutput != null ? context.coloredOutput : !Terminal.TYPE_DUMB.equals(terminal.getType()));
365 
366         // handle rawStreams: some would like to act on true, some on false
367         if (context.options().rawStreams().orElse(false)) {
368             doConfigureWithTerminalWithRawStreamsEnabled(context);
369         } else {
370             doConfigureWithTerminalWithRawStreamsDisabled(context);
371         }
372     }
373 
374     /**
375      * Override this method to add some special handling for "raw streams" <em>enabled</em> option.
376      */
377     protected void doConfigureWithTerminalWithRawStreamsEnabled(C context) {
378         context.invokerRequest.stdIn().ifPresent(System::setIn);
379         context.invokerRequest
380                 .stdOut()
381                 .ifPresent(out -> System.setOut(out instanceof PrintStream pw ? pw : new PrintStream(out, true)));
382         context.invokerRequest
383                 .stdErr()
384                 .ifPresent(err -> System.setErr(err instanceof PrintStream pw ? pw : new PrintStream(err, true)));
385     }
386 
387     /**
388      * Override this method to add some special handling for "raw streams" <em>disabled</em> option.
389      */
390     protected void doConfigureWithTerminalWithRawStreamsDisabled(C context) {
391         MavenSimpleLogger stdout = (MavenSimpleLogger) context.loggerFactory.getLogger("stdout");
392         MavenSimpleLogger stderr = (MavenSimpleLogger) context.loggerFactory.getLogger("stderr");
393         stdout.setLogLevel(LocationAwareLogger.INFO_INT);
394         stderr.setLogLevel(LocationAwareLogger.INFO_INT);
395         PrintStream psOut = new LoggingOutputStream(s -> stdout.info("[stdout] " + s)).printStream();
396         context.closeables.add(() -> LoggingOutputStream.forceFlush(psOut));
397         PrintStream psErr = new LoggingOutputStream(s -> stderr.warn("[stderr] " + s)).printStream();
398         context.closeables.add(() -> LoggingOutputStream.forceFlush(psErr));
399         System.setOut(psOut);
400         System.setErr(psErr);
401         // no need to set them back, this is already handled by MessageUtils.systemUninstall() above
402     }
403 
404     protected Consumer<String> determineWriter(C context) {
405         if (context.writer == null) {
406             context.writer = doDetermineWriter(context);
407         }
408         return context.writer;
409     }
410 
411     protected Consumer<String> doDetermineWriter(C context) {
412         if (context.options().logFile().isPresent()) {
413             Path logFile = context.cwd.resolve(context.options().logFile().get());
414             try {
415                 PrintWriter printWriter = new PrintWriter(Files.newBufferedWriter(logFile), true);
416                 context.closeables.add(printWriter);
417                 return printWriter::println;
418             } catch (IOException e) {
419                 throw new MavenException("Unable to redirect logging to " + logFile, e);
420             }
421         } else {
422             // Given the terminal creation has been offloaded to a different thread,
423             // do not pass directly the terminal writer
424             return msg -> {
425                 PrintWriter pw = context.terminal.writer();
426                 pw.println(msg);
427                 pw.flush();
428             };
429         }
430     }
431 
432     protected void activateLogging(C context) throws Exception {
433         context.slf4jConfiguration.activate();
434         if (context.options().failOnSeverity().isPresent()) {
435             String logLevelThreshold = context.options().failOnSeverity().get();
436             if (context.loggerFactory instanceof LogLevelRecorder recorder) {
437                 LogLevelRecorder.Level level =
438                         switch (logLevelThreshold.toLowerCase(Locale.ENGLISH)) {
439                             case "warn", "warning" -> LogLevelRecorder.Level.WARN;
440                             case "error" -> LogLevelRecorder.Level.ERROR;
441                             default ->
442                                 throw new IllegalArgumentException(
443                                         logLevelThreshold
444                                                 + " is not a valid log severity threshold. Valid severities are WARN/WARNING and ERROR.");
445                         };
446                 recorder.setMaxLevelAllowed(level);
447                 context.logger.info("Enabled to break the build on log level " + logLevelThreshold + ".");
448             } else {
449                 context.logger.warn("Expected LoggerFactory to be of type '" + LogLevelRecorder.class.getName()
450                         + "', but found '"
451                         + context.loggerFactory.getClass().getName() + "' instead. "
452                         + "The --fail-on-severity flag will not take effect.");
453             }
454         }
455 
456         // at this point logging is set up, reply so far accumulated logs, if any and swap logger with real one
457         Logger logger =
458                 new Slf4jLogger(context.loggerFactory.getLogger(getClass().getName()));
459         context.logger.drain().forEach(e -> logger.log(e.level(), e.message(), e.error()));
460         context.logger = logger;
461     }
462 
463     protected void helpOrVersionAndMayExit(C context) throws Exception {
464         if (context.options().help().isPresent()) {
465             Consumer<String> writer = determineWriter(context);
466             context.options().displayHelp(context.invokerRequest.parserRequest(), writer);
467             throw new InvokerException.ExitException(0);
468         }
469         if (context.options().showVersionAndExit().isPresent()) {
470             showVersion(context);
471             throw new InvokerException.ExitException(0);
472         }
473     }
474 
475     protected void showVersion(C context) {
476         Consumer<String> writer = determineWriter(context);
477         if (context.options().quiet().orElse(false)) {
478             writer.accept(CLIReportingUtils.showVersionMinimal());
479         } else if (context.invokerRequest.effectiveVerbose()) {
480             writer.accept(CLIReportingUtils.showVersion(
481                     ProcessHandle.current().info().commandLine().orElse(null), describe(context.terminal)));
482 
483         } else {
484             writer.accept(CLIReportingUtils.showVersion());
485         }
486     }
487 
488     protected String describe(Terminal terminal) {
489         if (terminal == null) {
490             return null;
491         }
492         if (terminal instanceof FastTerminal ft) {
493             terminal = ft.getTerminal();
494         }
495         List<String> subs = new ArrayList<>();
496         subs.add("type=" + terminal.getType());
497         if (terminal instanceof TerminalExt te) {
498             subs.add("provider=" + te.getProvider().name());
499         }
500         if (terminal instanceof AbstractPosixTerminal pt) {
501             subs.add("pty=" + pt.getPty().getClass().getName());
502         }
503         return terminal.getClass().getSimpleName() + " (" + String.join(", ", subs) + ")";
504     }
505 
506     protected void preCommands(C context) throws Exception {
507         boolean verbose = context.invokerRequest.effectiveVerbose();
508         boolean version = context.options().showVersion().orElse(false);
509         if (verbose || version) {
510             showVersion(context);
511         }
512     }
513 
514     protected void container(C context) throws Exception {
515         if (context.lookup == null) {
516             context.containerCapsule = createContainerCapsuleFactory()
517                     .createContainerCapsule(this, context, createCoreExtensionSelector());
518             context.closeables.add(context::closeContainer);
519             context.lookup = context.containerCapsule.getLookup();
520         } else {
521             context.containerCapsule.updateLogging(context);
522         }
523     }
524 
525     protected CoreExtensionSelector<C> createCoreExtensionSelector() {
526         return new PrecedenceCoreExtensionSelector<>();
527     }
528 
529     protected ContainerCapsuleFactory<C> createContainerCapsuleFactory() {
530         return new PlexusContainerCapsuleFactory<>();
531     }
532 
533     protected void postContainer(C context) throws Exception {
534         ProtoSession protoSession = context.protoSession;
535         for (PropertyContributor propertyContributor : context.lookup
536                 .lookup(PropertyContributorsHolder.class)
537                 .getPropertyContributors()
538                 .values()) {
539             protoSession = protoSession.toBuilder()
540                     .withUserProperties(propertyContributor.contribute(protoSession))
541                     .build();
542         }
543         context.protoSession = protoSession;
544     }
545 
546     protected void lookup(C context) throws Exception {
547         if (context.eventSpyDispatcher == null) {
548             context.eventSpyDispatcher = context.lookup.lookup(EventSpyDispatcher.class);
549         }
550     }
551 
552     protected void init(C context) throws Exception {
553         Map<String, Object> data = new HashMap<>();
554         data.put("plexus", context.lookup.lookup(PlexusContainer.class));
555         data.put("workingDirectory", context.cwd.get().toString());
556         data.put("systemProperties", toProperties(context.protoSession.getSystemProperties()));
557         data.put("userProperties", toProperties(context.protoSession.getUserProperties()));
558         data.put("versionProperties", CLIReportingUtils.getBuildProperties());
559         context.eventSpyDispatcher.init(() -> data);
560     }
561 
562     protected void postCommands(C context) throws Exception {
563         Logger logger = context.logger;
564         if (context.options().showErrors().orElse(false)) {
565             logger.info("Error stacktraces are turned on.");
566         }
567         if (context.options().verbose().orElse(false)) {
568             logger.debug("Message scheme: " + (MessageUtils.isColorEnabled() ? "color" : "plain"));
569             if (MessageUtils.isColorEnabled()) {
570                 MessageBuilder buff = MessageUtils.builder();
571                 buff.a("Message styles: ");
572                 buff.trace("trace").a(' ');
573                 buff.debug("debug").a(' ');
574                 buff.info("info").a(' ');
575                 buff.warning("warning").a(' ');
576                 buff.error("error").a(' ');
577                 buff.success("success").a(' ');
578                 buff.failure("failure").a(' ');
579                 buff.strong("strong").a(' ');
580                 buff.mojo("mojo").a(' ');
581                 buff.project("project");
582                 logger.debug(buff.toString());
583             }
584         }
585     }
586 
587     protected void settings(C context) throws Exception {
588         if (context.effectiveSettings == null) {
589             settings(context, true, context.lookup.lookup(SettingsBuilder.class));
590         }
591     }
592 
593     /**
594      * This method is invoked twice during "normal" LookupInvoker level startup: once when (if present) extensions
595      * are loaded up during Plexus DI creation, and once afterward as "normal" boot procedure.
596      * <p>
597      * If there are Maven3 passwords presents in settings, this results in doubled warnings emitted. So Plexus DI
598      * creation call keeps "emitSettingsWarnings" false. If there are fatal issues, it will anyway "die" at that
599      * spot before warnings would be emitted.
600      * <p>
601      * The method returns a "cleaner" runnable, as during extension loading the context needs to be "cleaned", restored
602      * to previous state (as it was before extension loading).
603      */
604     protected Runnable settings(C context, boolean emitSettingsWarnings, SettingsBuilder settingsBuilder)
605             throws Exception {
606         Path userSettingsFile = null;
607         if (context.options().altUserSettings().isPresent()) {
608             userSettingsFile =
609                     context.cwd.resolve(context.options().altUserSettings().get());
610 
611             if (!Files.isRegularFile(userSettingsFile)) {
612                 throw new FileNotFoundException("The specified user settings file does not exist: " + userSettingsFile);
613             }
614         } else {
615             String userSettingsFileStr =
616                     context.protoSession.getEffectiveProperties().get(Constants.MAVEN_USER_SETTINGS);
617             if (userSettingsFileStr != null) {
618                 userSettingsFile =
619                         context.userDirectory.resolve(userSettingsFileStr).normalize();
620             }
621         }
622 
623         Path projectSettingsFile = null;
624         if (context.options().altProjectSettings().isPresent()) {
625             projectSettingsFile =
626                     context.cwd.resolve(context.options().altProjectSettings().get());
627 
628             if (!Files.isRegularFile(projectSettingsFile)) {
629                 throw new FileNotFoundException(
630                         "The specified project settings file does not exist: " + projectSettingsFile);
631             }
632         } else {
633             String projectSettingsFileStr =
634                     context.protoSession.getEffectiveProperties().get(Constants.MAVEN_PROJECT_SETTINGS);
635             if (projectSettingsFileStr != null) {
636                 projectSettingsFile = context.cwd.resolve(projectSettingsFileStr);
637             }
638         }
639 
640         Path installationSettingsFile = null;
641         if (context.options().altInstallationSettings().isPresent()) {
642             installationSettingsFile = context.cwd.resolve(
643                     context.options().altInstallationSettings().get());
644 
645             if (!Files.isRegularFile(installationSettingsFile)) {
646                 throw new FileNotFoundException(
647                         "The specified installation settings file does not exist: " + installationSettingsFile);
648             }
649         } else {
650             String installationSettingsFileStr =
651                     context.protoSession.getEffectiveProperties().get(Constants.MAVEN_INSTALLATION_SETTINGS);
652             if (installationSettingsFileStr != null) {
653                 installationSettingsFile = context.installationDirectory
654                         .resolve(installationSettingsFileStr)
655                         .normalize();
656             }
657         }
658 
659         context.installationSettingsPath = installationSettingsFile;
660         context.projectSettingsPath = projectSettingsFile;
661         context.userSettingsPath = userSettingsFile;
662 
663         UnaryOperator<String> interpolationSource = context.protoSession.getEffectiveProperties()::get;
664         SettingsBuilderRequest settingsRequest = SettingsBuilderRequest.builder()
665                 .session(context.protoSession)
666                 .installationSettingsSource(
667                         installationSettingsFile != null && Files.exists(installationSettingsFile)
668                                 ? Sources.fromPath(installationSettingsFile)
669                                 : null)
670                 .projectSettingsSource(
671                         projectSettingsFile != null && Files.exists(projectSettingsFile)
672                                 ? Sources.fromPath(projectSettingsFile)
673                                 : null)
674                 .userSettingsSource(
675                         userSettingsFile != null && Files.exists(userSettingsFile)
676                                 ? Sources.fromPath(userSettingsFile)
677                                 : null)
678                 .interpolationSource(interpolationSource)
679                 .build();
680 
681         customizeSettingsRequest(context, settingsRequest);
682         if (context.eventSpyDispatcher != null) {
683             context.eventSpyDispatcher.onEvent(settingsRequest);
684         }
685 
686         context.logger.debug("Reading installation settings from '" + installationSettingsFile + "'");
687         context.logger.debug("Reading project settings from '" + projectSettingsFile + "'");
688         context.logger.debug("Reading user settings from '" + userSettingsFile + "'");
689 
690         SettingsBuilderResult settingsResult = settingsBuilder.build(settingsRequest);
691         customizeSettingsResult(context, settingsResult);
692         if (context.eventSpyDispatcher != null) {
693             context.eventSpyDispatcher.onEvent(settingsResult);
694         }
695 
696         context.effectiveSettings = settingsResult.getEffectiveSettings();
697         context.interactive = mayDisableInteractiveMode(context, context.effectiveSettings.isInteractiveMode());
698         context.localRepositoryPath = localRepositoryPath(context);
699 
700         if (emitSettingsWarnings && settingsResult.getProblems().hasWarningProblems()) {
701             int totalProblems = settingsResult.getProblems().totalProblemsReported();
702             context.logger.info("");
703             context.logger.info(String.format(
704                     "%s %s encountered while building the effective settings (use -e to see details)",
705                     totalProblems, (totalProblems == 1) ? "problem was" : "problems were"));
706 
707             if (context.options().showErrors().orElse(false)) {
708                 for (BuilderProblem problem :
709                         settingsResult.getProblems().problems().toList()) {
710                     context.logger.warn(problem.getMessage() + " @ " + problem.getLocation());
711                 }
712             }
713             context.logger.info("");
714         }
715         return () -> {
716             context.installationSettingsPath = null;
717             context.projectSettingsPath = null;
718             context.userSettingsPath = null;
719             context.effectiveSettings = null;
720             context.interactive = true;
721             context.localRepositoryPath = null;
722         };
723     }
724 
725     protected void customizeSettingsRequest(C context, SettingsBuilderRequest settingsBuilderRequest)
726             throws Exception {}
727 
728     protected void customizeSettingsResult(C context, SettingsBuilderResult settingsBuilderResult) throws Exception {}
729 
730     protected boolean mayDisableInteractiveMode(C context, boolean proposedInteractive) {
731         if (!context.options().forceInteractive().orElse(false)) {
732             if (context.options().nonInteractive().orElse(false)) {
733                 return false;
734             } else {
735                 if (context.invokerRequest.ciInfo().isPresent()) {
736                     CIInfo ci = context.invokerRequest.ciInfo().get();
737                     context.logger.info(
738                             "Making this build non-interactive, because CI detected. Disable this detection by adding --force-interactive.");
739                     context.logger.info("Detected CI system: '" + ci.name() + "': " + ci.message());
740                     return false;
741                 }
742             }
743         }
744         return proposedInteractive;
745     }
746 
747     protected Path localRepositoryPath(C context) {
748         // user override
749         String userDefinedLocalRepo =
750                 context.protoSession.getEffectiveProperties().get(Constants.MAVEN_REPO_LOCAL);
751         if (userDefinedLocalRepo == null) {
752             userDefinedLocalRepo = context.protoSession.getEffectiveProperties().get(Constants.MAVEN_REPO_LOCAL);
753             if (userDefinedLocalRepo != null) {
754                 context.logger.warn("The property '" + Constants.MAVEN_REPO_LOCAL
755                         + "' has been set using a JVM system property which is deprecated. "
756                         + "The property can be passed as a Maven argument or in the Maven project configuration file,"
757                         + "usually located at ${session.rootDirectory}/.mvn/maven-user.properties.");
758             }
759         }
760         if (userDefinedLocalRepo != null) {
761             return context.cwd.resolve(userDefinedLocalRepo);
762         }
763         // settings
764         userDefinedLocalRepo = context.effectiveSettings.getLocalRepository();
765         if (userDefinedLocalRepo != null && !userDefinedLocalRepo.isEmpty()) {
766             return context.userDirectory.resolve(userDefinedLocalRepo).normalize();
767         }
768         // defaults
769         return context.userDirectory
770                 .resolve(context.protoSession.getEffectiveProperties().get(Constants.MAVEN_USER_CONF))
771                 .resolve("repository")
772                 .normalize();
773     }
774 
775     protected void populateRequest(C context, Lookup lookup, MavenExecutionRequest request) throws Exception {
776         populateRequestFromSettings(request, context.effectiveSettings);
777 
778         request.setLoggingLevel(toMavenExecutionRequestLoggingLevel(context.loggerLevel));
779         request.setLocalRepositoryPath(context.localRepositoryPath.toFile());
780         request.setLocalRepository(createLocalArtifactRepository(context.localRepositoryPath));
781 
782         request.setInteractiveMode(context.interactive);
783         request.setShowErrors(context.options().showErrors().orElse(false));
784         request.setBaseDirectory(context.invokerRequest.topDirectory().toFile());
785         request.setSystemProperties(toProperties(context.protoSession.getSystemProperties()));
786         request.setUserProperties(toProperties(context.protoSession.getUserProperties()));
787 
788         request.setInstallationSettingsFile(
789                 context.installationSettingsPath != null ? context.installationSettingsPath.toFile() : null);
790         request.setProjectSettingsFile(
791                 context.projectSettingsPath != null ? context.projectSettingsPath.toFile() : null);
792         request.setUserSettingsFile(context.userSettingsPath != null ? context.userSettingsPath.toFile() : null);
793 
794         request.setTopDirectory(context.invokerRequest.topDirectory());
795         if (context.invokerRequest.rootDirectory().isPresent()) {
796             request.setMultiModuleProjectDirectory(
797                     context.invokerRequest.rootDirectory().get().toFile());
798             request.setRootDirectory(context.invokerRequest.rootDirectory().get());
799         }
800 
801         request.addPluginGroup("org.apache.maven.plugins");
802         request.addPluginGroup("org.codehaus.mojo");
803     }
804 
805     /**
806      * TODO: get rid of this!!!
807      */
808     @Deprecated
809     private ArtifactRepository createLocalArtifactRepository(Path baseDirectory) {
810         DefaultRepositoryLayout layout = new DefaultRepositoryLayout();
811         ArtifactRepositoryPolicy blah = new ArtifactRepositoryPolicy(
812                 true, ArtifactRepositoryPolicy.UPDATE_POLICY_ALWAYS, ArtifactRepositoryPolicy.CHECKSUM_POLICY_IGNORE);
813         return new MavenArtifactRepository(
814                 "local", "file://" + baseDirectory.toUri().getRawPath(), layout, blah, blah);
815     }
816 
817     protected void populateRequestFromSettings(MavenExecutionRequest request, Settings settings) throws Exception {
818         if (settings == null) {
819             return;
820         }
821         request.setOffline(settings.isOffline());
822         request.setInteractiveMode(settings.isInteractiveMode());
823         request.setPluginGroups(settings.getPluginGroups());
824         request.setLocalRepositoryPath(settings.getLocalRepository());
825         for (Server server : settings.getServers()) {
826             request.addServer(new org.apache.maven.settings.Server(server));
827         }
828 
829         //  <proxies>
830         //    <proxy>
831         //      <active>true</active>
832         //      <protocol>http</protocol>
833         //      <host>proxy.somewhere.com</host>
834         //      <port>8080</port>
835         //      <username>proxyuser</username>
836         //      <password>somepassword</password>
837         //      <nonProxyHosts>www.google.com|*.somewhere.com</nonProxyHosts>
838         //    </proxy>
839         //  </proxies>
840 
841         for (Proxy proxy : settings.getProxies()) {
842             if (!proxy.isActive()) {
843                 continue;
844             }
845             request.addProxy(new org.apache.maven.settings.Proxy(proxy));
846         }
847 
848         // <mirrors>
849         //   <mirror>
850         //     <id>nexus</id>
851         //     <mirrorOf>*</mirrorOf>
852         //     <url>http://repository.sonatype.org/content/groups/public</url>
853         //   </mirror>
854         // </mirrors>
855 
856         for (Mirror mirror : settings.getMirrors()) {
857             request.addMirror(new org.apache.maven.settings.Mirror(mirror));
858         }
859 
860         // Collect repositories; are sensitive to ordering
861         LinkedHashMap<String, Repository> remoteRepositories = new LinkedHashMap<>();
862         LinkedHashMap<String, Repository> remotePluginRepositories = new LinkedHashMap<>();
863 
864         // settings/repositories
865         for (Repository remoteRepository : settings.getRepositories()) {
866             remoteRepositories.put(remoteRepository.getId(), remoteRepository);
867         }
868         for (Repository pluginRepository : settings.getPluginRepositories()) {
869             remotePluginRepositories.put(pluginRepository.getId(), pluginRepository);
870         }
871 
872         // profiles (if active)
873         for (Profile rawProfile : settings.getProfiles()) {
874             request.addProfile(
875                     new org.apache.maven.model.Profile(SettingsUtilsV4.convertFromSettingsProfile(rawProfile)));
876 
877             if (settings.getActiveProfiles().contains(rawProfile.getId())) {
878                 for (Repository remoteRepository : rawProfile.getRepositories()) {
879                     remoteRepositories.put(remoteRepository.getId(), remoteRepository);
880                 }
881 
882                 for (Repository pluginRepository : rawProfile.getPluginRepositories()) {
883                     remotePluginRepositories.put(pluginRepository.getId(), pluginRepository);
884                 }
885             }
886         }
887 
888         // pour onto request
889         request.setActiveProfiles(settings.getActiveProfiles());
890         request.setRemoteRepositories(remoteRepositories.values().stream()
891                 .map(r -> {
892                     try {
893                         return MavenRepositorySystem.buildArtifactRepository(
894                                 new org.apache.maven.settings.Repository(r));
895                     } catch (Exception e) {
896                         // nothing currently
897                         return null;
898                     }
899                 })
900                 .filter(Objects::nonNull)
901                 .toList());
902         request.setPluginArtifactRepositories(remotePluginRepositories.values().stream()
903                 .map(r -> {
904                     try {
905                         return MavenRepositorySystem.buildArtifactRepository(
906                                 new org.apache.maven.settings.Repository(r));
907                     } catch (Exception e) {
908                         // nothing currently
909                         return null;
910                     }
911                 })
912                 .filter(Objects::nonNull)
913                 .toList());
914     }
915 
916     protected int calculateDegreeOfConcurrency(String threadConfiguration) {
917         try {
918             if (threadConfiguration.endsWith("C")) {
919                 String str = threadConfiguration.substring(0, threadConfiguration.length() - 1);
920                 float coreMultiplier = Float.parseFloat(str);
921 
922                 if (coreMultiplier <= 0.0f) {
923                     throw new IllegalArgumentException("Invalid threads core multiplier value: '" + threadConfiguration
924                             + "'. Value must be positive.");
925                 }
926 
927                 int procs = Runtime.getRuntime().availableProcessors();
928                 int threads = (int) (coreMultiplier * procs);
929                 return threads == 0 ? 1 : threads;
930             } else {
931                 int threads = Integer.parseInt(threadConfiguration);
932                 if (threads <= 0) {
933                     throw new IllegalArgumentException(
934                             "Invalid threads value: '" + threadConfiguration + "'. Value must be positive.");
935                 }
936                 return threads;
937             }
938         } catch (NumberFormatException e) {
939             throw new IllegalArgumentException("Invalid threads value: '" + threadConfiguration
940                     + "'. Supported are int and float values ending with C.");
941         }
942     }
943 
944     protected abstract int execute(C context) throws Exception;
945 }