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 javax.xml.stream.XMLStreamException;
22  
23  import java.io.IOException;
24  import java.io.InputStream;
25  import java.nio.file.Files;
26  import java.nio.file.Path;
27  import java.nio.file.Paths;
28  import java.util.ArrayList;
29  import java.util.Arrays;
30  import java.util.HashMap;
31  import java.util.List;
32  import java.util.Locale;
33  import java.util.Map;
34  import java.util.Objects;
35  import java.util.Properties;
36  import java.util.function.UnaryOperator;
37  import java.util.stream.Collectors;
38  
39  import org.apache.maven.api.Constants;
40  import org.apache.maven.api.annotations.Nullable;
41  import org.apache.maven.api.cli.CoreExtensions;
42  import org.apache.maven.api.cli.InvokerRequest;
43  import org.apache.maven.api.cli.Options;
44  import org.apache.maven.api.cli.Parser;
45  import org.apache.maven.api.cli.ParserRequest;
46  import org.apache.maven.api.cli.cisupport.CIInfo;
47  import org.apache.maven.api.cli.extensions.CoreExtension;
48  import org.apache.maven.api.cli.extensions.InputLocation;
49  import org.apache.maven.api.cli.extensions.InputSource;
50  import org.apache.maven.api.services.Interpolator;
51  import org.apache.maven.cling.internal.extension.io.CoreExtensionsStaxReader;
52  import org.apache.maven.cling.invoker.cisupport.CIDetectorHelper;
53  import org.apache.maven.cling.props.MavenPropertiesLoader;
54  import org.apache.maven.cling.utils.CLIReportingUtils;
55  import org.apache.maven.properties.internal.EnvironmentUtils;
56  import org.apache.maven.properties.internal.SystemProperties;
57  
58  import static java.util.Objects.requireNonNull;
59  import static org.apache.maven.cling.invoker.CliUtils.createInterpolator;
60  import static org.apache.maven.cling.invoker.CliUtils.getCanonicalPath;
61  import static org.apache.maven.cling.invoker.CliUtils.or;
62  import static org.apache.maven.cling.invoker.CliUtils.prefix;
63  import static org.apache.maven.cling.invoker.CliUtils.stripLeadingAndTrailingQuotes;
64  import static org.apache.maven.cling.invoker.CliUtils.toMap;
65  
66  public abstract class BaseParser implements Parser {
67  
68      @SuppressWarnings("VisibilityModifier")
69      public static class LocalContext {
70          public final ParserRequest parserRequest;
71          public final Map<String, String> systemPropertiesOverrides;
72  
73          public LocalContext(ParserRequest parserRequest) {
74              this.parserRequest = parserRequest;
75              this.systemPropertiesOverrides = new HashMap<>();
76          }
77  
78          public boolean parsingFailed = false;
79          public Path cwd;
80          public Path installationDirectory;
81          public Path userHomeDirectory;
82          public Map<String, String> systemProperties;
83          public Map<String, String> userProperties;
84          public Path topDirectory;
85  
86          @Nullable
87          public Path rootDirectory;
88  
89          @Nullable
90          public List<CoreExtensions> extensions;
91  
92          @Nullable
93          public CIInfo ciInfo;
94  
95          @Nullable
96          public Options options;
97  
98          public Map<String, String> extraInterpolationSource() {
99              Map<String, String> extra = new HashMap<>();
100             extra.put("session.topDirectory", topDirectory.toString());
101             if (rootDirectory != null) {
102                 extra.put("session.rootDirectory", rootDirectory.toString());
103             }
104             return extra;
105         }
106     }
107 
108     @Override
109     public InvokerRequest parseInvocation(ParserRequest parserRequest) {
110         requireNonNull(parserRequest);
111 
112         LocalContext context = new LocalContext(parserRequest);
113 
114         // the basics
115         try {
116             context.cwd = getCwd(context);
117         } catch (Exception e) {
118             context.parsingFailed = true;
119             context.cwd = getCanonicalPath(Paths.get("."));
120             parserRequest.logger().error("Error determining working directory", e);
121         }
122         try {
123             context.installationDirectory = getInstallationDirectory(context);
124         } catch (Exception e) {
125             context.parsingFailed = true;
126             context.installationDirectory = context.cwd;
127             parserRequest.logger().error("Error determining installation directory", e);
128         }
129         try {
130             context.userHomeDirectory = getUserHomeDirectory(context);
131         } catch (Exception e) {
132             context.parsingFailed = true;
133             context.userHomeDirectory = context.cwd;
134             parserRequest.logger().error("Error determining user home directory", e);
135         }
136 
137         // top/root
138         try {
139             context.topDirectory = getTopDirectory(context);
140         } catch (Exception e) {
141             context.parsingFailed = true;
142             context.topDirectory = context.cwd;
143             parserRequest.logger().error("Error determining top directory", e);
144         }
145         try {
146             context.rootDirectory = getRootDirectory(context);
147         } catch (Exception e) {
148             context.parsingFailed = true;
149             context.rootDirectory = context.cwd;
150             parserRequest.logger().error("Error determining root directory", e);
151         }
152 
153         // options
154         try {
155             context.options = parseCliOptions(context);
156         } catch (Exception e) {
157             context.parsingFailed = true;
158             context.options = null;
159             parserRequest.logger().error("Error parsing program arguments", e);
160         }
161 
162         // system and user properties
163         try {
164             context.systemProperties = populateSystemProperties(context);
165         } catch (Exception e) {
166             context.parsingFailed = true;
167             context.systemProperties = new HashMap<>();
168             parserRequest.logger().error("Error populating system properties", e);
169         }
170         try {
171             context.userProperties = populateUserProperties(context);
172         } catch (Exception e) {
173             context.parsingFailed = true;
174             context.userProperties = new HashMap<>();
175             parserRequest.logger().error("Error populating user properties", e);
176         }
177 
178         // options: interpolate
179         if (context.options != null) {
180             context.options = context.options.interpolate(Interpolator.chain(
181                     context.extraInterpolationSource()::get,
182                     context.userProperties::get,
183                     context.systemProperties::get));
184         }
185 
186         // below we use effective properties as both system + user are present
187         // core extensions
188         try {
189             context.extensions = readCoreExtensionsDescriptor(context);
190         } catch (Exception e) {
191             context.parsingFailed = true;
192             parserRequest.logger().error("Error reading core extensions descriptor", e);
193         }
194 
195         // CI detection
196         context.ciInfo = detectCI(context);
197 
198         // only if not failed so far; otherwise we may have no options to validate
199         if (!context.parsingFailed) {
200             validate(context);
201         }
202 
203         return getInvokerRequest(context);
204     }
205 
206     protected void validate(LocalContext context) {
207         Options options = context.options;
208 
209         options.failOnSeverity().ifPresent(severity -> {
210             String c = severity.toLowerCase(Locale.ENGLISH);
211             if (!Arrays.asList("warn", "warning", "error").contains(c)) {
212                 context.parsingFailed = true;
213                 context.parserRequest
214                         .logger()
215                         .error("Invalid fail on severity threshold '" + c
216                                 + "'. Supported values are 'WARN', 'WARNING' and 'ERROR'.");
217             }
218         });
219         options.altUserSettings()
220                 .ifPresent(userSettings ->
221                         failIfFileNotExists(context, userSettings, "The specified user settings file does not exist"));
222         options.altProjectSettings()
223                 .ifPresent(projectSettings -> failIfFileNotExists(
224                         context, projectSettings, "The specified project settings file does not exist"));
225         options.altInstallationSettings()
226                 .ifPresent(installationSettings -> failIfFileNotExists(
227                         context, installationSettings, "The specified installation settings file does not exist"));
228         options.altUserToolchains()
229                 .ifPresent(userToolchains -> failIfFileNotExists(
230                         context, userToolchains, "The specified user toolchains file does not exist"));
231         options.altInstallationToolchains()
232                 .ifPresent(installationToolchains -> failIfFileNotExists(
233                         context, installationToolchains, "The specified installation toolchains file does not exist"));
234         options.color().ifPresent(color -> {
235             String c = color.toLowerCase(Locale.ENGLISH);
236             if (!Arrays.asList("always", "yes", "force", "never", "no", "none", "auto", "tty", "if-tty")
237                     .contains(c)) {
238                 context.parsingFailed = true;
239                 context.parserRequest
240                         .logger()
241                         .error("Invalid color configuration value '" + c
242                                 + "'. Supported values are 'auto', 'always', 'never'.");
243             }
244         });
245     }
246 
247     protected void failIfFileNotExists(LocalContext context, String fileName, String message) {
248         Path path = context.cwd.resolve(fileName);
249         if (!Files.isRegularFile(path)) {
250             context.parsingFailed = true;
251             context.parserRequest.logger().error(message + ": " + path);
252         }
253     }
254 
255     protected InvokerRequest getInvokerRequest(LocalContext context) {
256         return new BaseInvokerRequest(
257                 context.parserRequest,
258                 context.parsingFailed,
259                 context.cwd,
260                 context.installationDirectory,
261                 context.userHomeDirectory,
262                 context.userProperties,
263                 context.systemProperties,
264                 context.topDirectory,
265                 context.rootDirectory,
266                 context.extensions,
267                 context.ciInfo,
268                 context.options);
269     }
270 
271     protected Path getCwd(LocalContext context) {
272         if (context.parserRequest.cwd() != null) {
273             Path result = getCanonicalPath(context.parserRequest.cwd());
274             context.systemPropertiesOverrides.put("user.dir", result.toString());
275             return result;
276         } else {
277             Path result = getCanonicalPath(Paths.get(System.getProperty("user.dir")));
278             mayOverrideDirectorySystemProperty(context, "user.dir", result);
279             return result;
280         }
281     }
282 
283     protected Path getInstallationDirectory(LocalContext context) {
284         if (context.parserRequest.mavenHome() != null) {
285             Path result = getCanonicalPath(context.parserRequest.mavenHome());
286             context.systemPropertiesOverrides.put(Constants.MAVEN_HOME, result.toString());
287             return result;
288         } else {
289             String mavenHome = System.getProperty(Constants.MAVEN_HOME);
290             if (mavenHome == null) {
291                 throw new IllegalStateException(
292                         "local mode requires " + Constants.MAVEN_HOME + " Java System Property set");
293             }
294             Path result = getCanonicalPath(Paths.get(mavenHome));
295             mayOverrideDirectorySystemProperty(context, Constants.MAVEN_HOME, result);
296             return result;
297         }
298     }
299 
300     protected Path getUserHomeDirectory(LocalContext context) {
301         if (context.parserRequest.userHome() != null) {
302             Path result = getCanonicalPath(context.parserRequest.userHome());
303             context.systemPropertiesOverrides.put("user.home", result.toString());
304             return result;
305         } else {
306             Path result = getCanonicalPath(Paths.get(System.getProperty("user.home")));
307             mayOverrideDirectorySystemProperty(context, "user.home", result);
308             return result;
309         }
310     }
311 
312     /**
313      * This method is needed to "align" values used later on for interpolations and path calculations.
314      * We enforce "canonical" paths, so IF key and canonical path value disagree, let override it.
315      */
316     protected void mayOverrideDirectorySystemProperty(LocalContext context, String javaSystemPropertyKey, Path value) {
317         String valueString = value.toString();
318         if (!Objects.equals(System.getProperty(javaSystemPropertyKey), valueString)) {
319             context.systemPropertiesOverrides.put(javaSystemPropertyKey, valueString);
320         }
321     }
322 
323     protected Path getTopDirectory(LocalContext context) {
324         // We need to locate the top level project which may be pointed at using
325         // the -f/--file option.
326         Path topDirectory = requireNonNull(context.cwd);
327         boolean isAltFile = false;
328         for (String arg : context.parserRequest.args()) {
329             if (isAltFile) {
330                 // this is the argument following -f/--file
331                 Path path = topDirectory.resolve(stripLeadingAndTrailingQuotes(arg));
332                 if (Files.isDirectory(path)) {
333                     topDirectory = path;
334                 } else if (Files.isRegularFile(path)) {
335                     topDirectory = path.getParent();
336                     if (!Files.isDirectory(topDirectory)) {
337                         throw new IllegalArgumentException("Directory " + topDirectory
338                                 + " extracted from the -f/--file command-line argument " + arg + " does not exist");
339                     }
340                 } else {
341                     throw new IllegalArgumentException(
342                             "POM file " + arg + " specified with the -f/--file command line argument does not exist");
343                 }
344                 break;
345             } else {
346                 // Check if this is the -f/--file option
347                 isAltFile = arg.equals("-f") || arg.equals("--file");
348             }
349         }
350         return getCanonicalPath(topDirectory);
351     }
352 
353     @Nullable
354     protected Path getRootDirectory(LocalContext context) {
355         return CliUtils.findRoot(context.topDirectory);
356     }
357 
358     protected Map<String, String> populateSystemProperties(LocalContext context) {
359         Properties systemProperties = new Properties();
360 
361         // ----------------------------------------------------------------------
362         // Load environment and system properties
363         // ----------------------------------------------------------------------
364 
365         EnvironmentUtils.addEnvVars(systemProperties);
366         SystemProperties.addSystemProperties(systemProperties);
367         systemProperties.putAll(context.systemPropertiesOverrides);
368 
369         // ----------------------------------------------------------------------
370         // Properties containing info about the currently running version of Maven
371         // These override any corresponding properties set on the command line
372         // ----------------------------------------------------------------------
373 
374         Properties buildProperties = CLIReportingUtils.getBuildProperties();
375 
376         String mavenVersion = buildProperties.getProperty(CLIReportingUtils.BUILD_VERSION_PROPERTY);
377         systemProperties.setProperty(Constants.MAVEN_VERSION, mavenVersion);
378 
379         boolean snapshot = mavenVersion.endsWith("SNAPSHOT");
380         if (snapshot) {
381             mavenVersion = mavenVersion.substring(0, mavenVersion.length() - "SNAPSHOT".length());
382             if (mavenVersion.endsWith("-")) {
383                 mavenVersion = mavenVersion.substring(0, mavenVersion.length() - 1);
384             }
385         }
386         String[] versionElements = mavenVersion.split("\\.");
387         if (versionElements.length != 3) {
388             throw new IllegalStateException("Maven version is expected to have 3 segments: '" + mavenVersion + "'");
389         }
390         systemProperties.setProperty(Constants.MAVEN_VERSION_MAJOR, versionElements[0]);
391         systemProperties.setProperty(Constants.MAVEN_VERSION_MINOR, versionElements[1]);
392         systemProperties.setProperty(Constants.MAVEN_VERSION_PATCH, versionElements[2]);
393         systemProperties.setProperty(Constants.MAVEN_VERSION_SNAPSHOT, Boolean.toString(snapshot));
394 
395         String mavenBuildVersion = CLIReportingUtils.createMavenVersionString(buildProperties);
396         systemProperties.setProperty(Constants.MAVEN_BUILD_VERSION, mavenBuildVersion);
397 
398         Path mavenConf;
399         if (systemProperties.getProperty(Constants.MAVEN_INSTALLATION_CONF) != null) {
400             mavenConf = context.installationDirectory.resolve(
401                     systemProperties.getProperty(Constants.MAVEN_INSTALLATION_CONF));
402         } else if (systemProperties.getProperty("maven.conf") != null) {
403             mavenConf = context.installationDirectory.resolve(systemProperties.getProperty("maven.conf"));
404         } else if (systemProperties.getProperty(Constants.MAVEN_HOME) != null) {
405             mavenConf = context.installationDirectory
406                     .resolve(systemProperties.getProperty(Constants.MAVEN_HOME))
407                     .resolve("conf");
408         } else {
409             mavenConf = context.installationDirectory.resolve("");
410         }
411 
412         UnaryOperator<String> callback = or(
413                 context.extraInterpolationSource()::get,
414                 context.systemPropertiesOverrides::get,
415                 systemProperties::getProperty);
416         Path propertiesFile = mavenConf.resolve("maven-system.properties");
417         try {
418             MavenPropertiesLoader.loadProperties(systemProperties, propertiesFile, callback, false);
419         } catch (IOException e) {
420             throw new IllegalStateException("Error loading properties from " + propertiesFile, e);
421         }
422 
423         Map<String, String> result = toMap(systemProperties);
424         result.putAll(context.systemPropertiesOverrides);
425         return result;
426     }
427 
428     protected Map<String, String> populateUserProperties(LocalContext context) {
429         Properties userProperties = new Properties();
430         Map<String, String> paths = context.extraInterpolationSource();
431 
432         // ----------------------------------------------------------------------
433         // Options that are set on the command line become system properties
434         // and therefore are set in the session properties. System properties
435         // are most dominant.
436         // ----------------------------------------------------------------------
437 
438         Map<String, String> userSpecifiedProperties = context.options != null
439                 ? new HashMap<>(context.options.userProperties().orElse(new HashMap<>()))
440                 : new HashMap<>();
441         createInterpolator().interpolate(userSpecifiedProperties, paths::get);
442 
443         // ----------------------------------------------------------------------
444         // Load config files
445         // ----------------------------------------------------------------------
446         UnaryOperator<String> callback =
447                 or(paths::get, prefix("cli.", userSpecifiedProperties::get), context.systemProperties::get);
448 
449         Path mavenConf;
450         if (context.systemProperties.get(Constants.MAVEN_INSTALLATION_CONF) != null) {
451             mavenConf = context.installationDirectory.resolve(
452                     context.systemProperties.get(Constants.MAVEN_INSTALLATION_CONF));
453         } else if (context.systemProperties.get("maven.conf") != null) {
454             mavenConf = context.installationDirectory.resolve(context.systemProperties.get("maven.conf"));
455         } else if (context.systemProperties.get(Constants.MAVEN_HOME) != null) {
456             mavenConf = context.installationDirectory
457                     .resolve(context.systemProperties.get(Constants.MAVEN_HOME))
458                     .resolve("conf");
459         } else {
460             mavenConf = context.installationDirectory.resolve("");
461         }
462         Path propertiesFile = mavenConf.resolve("maven-user.properties");
463         try {
464             MavenPropertiesLoader.loadProperties(userProperties, propertiesFile, callback, false);
465         } catch (IOException e) {
466             throw new IllegalStateException("Error loading properties from " + propertiesFile, e);
467         }
468 
469         // Warn about deprecated maven.properties files
470         warnAboutDeprecatedPropertiesFiles(context);
471 
472         // CLI specified properties are most dominant
473         userProperties.putAll(userSpecifiedProperties);
474 
475         return toMap(userProperties);
476     }
477 
478     protected abstract Options parseCliOptions(LocalContext context);
479 
480     /**
481      * Important: This method must return list of {@link CoreExtensions} in precedence order.
482      */
483     protected List<CoreExtensions> readCoreExtensionsDescriptor(LocalContext context) {
484         ArrayList<CoreExtensions> result = new ArrayList<>();
485         Path file;
486         List<CoreExtension> loaded;
487 
488         Map<String, String> eff = new HashMap<>(context.systemProperties);
489         eff.putAll(context.userProperties);
490 
491         // project
492         file = context.cwd.resolve(eff.get(Constants.MAVEN_PROJECT_EXTENSIONS));
493         loaded = readCoreExtensionsDescriptorFromFile(file, false);
494         if (!loaded.isEmpty()) {
495             result.add(new CoreExtensions(file, loaded));
496         }
497 
498         // user
499         file = context.userHomeDirectory.resolve(eff.get(Constants.MAVEN_USER_EXTENSIONS));
500         loaded = readCoreExtensionsDescriptorFromFile(file, true);
501         if (!loaded.isEmpty()) {
502             result.add(new CoreExtensions(file, loaded));
503         }
504 
505         // installation
506         file = context.installationDirectory.resolve(eff.get(Constants.MAVEN_INSTALLATION_EXTENSIONS));
507         loaded = readCoreExtensionsDescriptorFromFile(file, true);
508         if (!loaded.isEmpty()) {
509             result.add(new CoreExtensions(file, loaded));
510         }
511 
512         return result.isEmpty() ? null : List.copyOf(result);
513     }
514 
515     protected List<CoreExtension> readCoreExtensionsDescriptorFromFile(Path extensionsFile, boolean allowMetaVersions) {
516         try {
517             if (extensionsFile != null && Files.exists(extensionsFile)) {
518                 try (InputStream is = Files.newInputStream(extensionsFile)) {
519                     return validateCoreExtensionsDescriptorFromFile(
520                             extensionsFile,
521                             List.copyOf(new CoreExtensionsStaxReader()
522                                     .read(is, true, new InputSource(extensionsFile.toString()))
523                                     .getExtensions()),
524                             allowMetaVersions);
525                 }
526             }
527             return List.of();
528         } catch (XMLStreamException | IOException e) {
529             throw new IllegalArgumentException("Failed to parse extensions file: " + extensionsFile, e);
530         }
531     }
532 
533     protected List<CoreExtension> validateCoreExtensionsDescriptorFromFile(
534             Path extensionFile, List<CoreExtension> coreExtensions, boolean allowMetaVersions) {
535         Map<String, List<InputLocation>> gasLocations = new HashMap<>();
536         Map<String, List<InputLocation>> metaVersionLocations = new HashMap<>();
537         for (CoreExtension coreExtension : coreExtensions) {
538             String ga = coreExtension.getGroupId() + ":" + coreExtension.getArtifactId();
539             InputLocation location = coreExtension.getLocation("");
540             gasLocations.computeIfAbsent(ga, k -> new ArrayList<>()).add(location);
541             // TODO: metaversions could be extensible enum with these two values out of the box
542             if ("LATEST".equals(coreExtension.getVersion()) || "RELEASE".equals(coreExtension.getVersion())) {
543                 metaVersionLocations.computeIfAbsent(ga, k -> new ArrayList<>()).add(location);
544             }
545         }
546         if (gasLocations.values().stream().anyMatch(l -> l.size() > 1)) {
547             throw new IllegalStateException("Extension conflicts in file " + extensionFile + ": "
548                     + gasLocations.entrySet().stream()
549                             .map(e -> e.getKey() + " defined on lines "
550                                     + e.getValue().stream()
551                                             .map(l -> String.valueOf(l.getLineNumber()))
552                                             .collect(Collectors.joining(", ")))
553                             .collect(Collectors.joining("; ")));
554         }
555         if (!allowMetaVersions && !metaVersionLocations.isEmpty()) {
556             throw new IllegalStateException("Extension with illegal version in file " + extensionFile + ": "
557                     + metaVersionLocations.entrySet().stream()
558                             .map(e -> e.getKey() + " defined on lines "
559                                     + e.getValue().stream()
560                                             .map(l -> String.valueOf(l.getLineNumber()))
561                                             .collect(Collectors.joining(", ")))
562                             .collect(Collectors.joining("; ")));
563         }
564         return coreExtensions;
565     }
566 
567     @Nullable
568     protected CIInfo detectCI(LocalContext context) {
569         List<CIInfo> detected = CIDetectorHelper.detectCI();
570         if (detected.isEmpty()) {
571             return null;
572         } else if (detected.size() > 1) {
573             // warn
574             context.parserRequest
575                     .logger()
576                     .warn("Multiple CI systems detected: "
577                             + detected.stream().map(CIInfo::name).collect(Collectors.joining(", ")));
578         }
579         return detected.get(0);
580     }
581 
582     private void warnAboutDeprecatedPropertiesFiles(LocalContext context) {
583         Map<String, String> systemProperties = context.systemProperties;
584 
585         // Check for deprecated ~/.m2/maven.properties
586         String userConfig = systemProperties.get("maven.user.conf");
587         Path userMavenProperties = userConfig != null ? Path.of(userConfig).resolve("maven.properties") : null;
588         if (userMavenProperties != null && Files.exists(userMavenProperties)) {
589             context.parserRequest
590                     .logger()
591                     .warn("Loading deprecated properties file: " + userMavenProperties + ". "
592                             + "Please rename to 'maven-user.properties'. "
593                             + "Support for 'maven.properties' will be removed in Maven 4.1.0.");
594         }
595 
596         // Check for deprecated .mvn/maven.properties in project directory
597         String projectConfig = systemProperties.get("maven.project.conf");
598         Path projectMavenProperties =
599                 projectConfig != null ? Path.of(projectConfig).resolve("maven.properties") : null;
600         if (projectMavenProperties != null && Files.exists(projectMavenProperties)) {
601             context.parserRequest
602                     .logger()
603                     .warn("Loading deprecated properties file: " + projectMavenProperties + ". "
604                             + "Please rename to 'maven-user.properties'. "
605                             + "Support for 'maven.properties' will be removed in Maven 4.1.0.");
606         }
607     }
608 }