1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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
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
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
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
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
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
187
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
196 context.ciInfo = detectCI(context);
197
198
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
314
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
325
326 Path topDirectory = requireNonNull(context.cwd);
327 boolean isAltFile = false;
328 for (String arg : context.parserRequest.args()) {
329 if (isAltFile) {
330
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
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
363
364
365 EnvironmentUtils.addEnvVars(systemProperties);
366 SystemProperties.addSystemProperties(systemProperties);
367 systemProperties.putAll(context.systemPropertiesOverrides);
368
369
370
371
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
434
435
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
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
470 warnAboutDeprecatedPropertiesFiles(context);
471
472
473 userProperties.putAll(userSpecifiedProperties);
474
475 return toMap(userProperties);
476 }
477
478 protected abstract Options parseCliOptions(LocalContext context);
479
480
481
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
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
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
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
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
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
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
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 }