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