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