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 try {
188 context.extensions = readCoreExtensionsDescriptor(context);
189 } catch (Exception e) {
190 context.parsingFailed = true;
191 parserRequest.logger().error("Error reading core extensions descriptor", e);
192 }
193
194
195 context.ciInfo = detectCI(context);
196
197
198 if (!context.parsingFailed) {
199 validate(context);
200 }
201
202 return getInvokerRequest(context);
203 }
204
205 protected void validate(LocalContext context) {
206 Options options = context.options;
207
208 options.failOnSeverity().ifPresent(severity -> {
209 String c = severity.toLowerCase(Locale.ENGLISH);
210 if (!Arrays.asList("warn", "warning", "error").contains(c)) {
211 context.parsingFailed = true;
212 context.parserRequest
213 .logger()
214 .error("Invalid fail on severity threshold '" + c
215 + "'. Supported values are 'WARN', 'WARNING' and 'ERROR'.");
216 }
217 });
218 options.altUserSettings()
219 .ifPresent(userSettings ->
220 failIfFileNotExists(context, userSettings, "The specified user settings file does not exist"));
221 options.altProjectSettings()
222 .ifPresent(projectSettings -> failIfFileNotExists(
223 context, projectSettings, "The specified project settings file does not exist"));
224 options.altInstallationSettings()
225 .ifPresent(installationSettings -> failIfFileNotExists(
226 context, installationSettings, "The specified installation settings file does not exist"));
227 options.altUserToolchains()
228 .ifPresent(userToolchains -> failIfFileNotExists(
229 context, userToolchains, "The specified user toolchains file does not exist"));
230 options.altInstallationToolchains()
231 .ifPresent(installationToolchains -> failIfFileNotExists(
232 context, installationToolchains, "The specified installation toolchains file does not exist"));
233 options.color().ifPresent(color -> {
234 String c = color.toLowerCase(Locale.ENGLISH);
235 if (!Arrays.asList("always", "yes", "force", "never", "no", "none", "auto", "tty", "if-tty")
236 .contains(c)) {
237 context.parsingFailed = true;
238 context.parserRequest
239 .logger()
240 .error("Invalid color configuration value '" + c
241 + "'. Supported values are 'auto', 'always', 'never'.");
242 }
243 });
244 }
245
246 protected void failIfFileNotExists(LocalContext context, String fileName, String message) {
247 Path path = context.cwd.resolve(fileName);
248 if (!Files.isRegularFile(path)) {
249 context.parsingFailed = true;
250 context.parserRequest.logger().error(message + ": " + path);
251 }
252 }
253
254 protected InvokerRequest getInvokerRequest(LocalContext context) {
255 return new BaseInvokerRequest(
256 context.parserRequest,
257 context.parsingFailed,
258 context.cwd,
259 context.installationDirectory,
260 context.userHomeDirectory,
261 context.userProperties,
262 context.systemProperties,
263 context.topDirectory,
264 context.rootDirectory,
265 context.extensions,
266 context.ciInfo,
267 context.options);
268 }
269
270 protected Path getCwd(LocalContext context) {
271 if (context.parserRequest.cwd() != null) {
272 Path result = getCanonicalPath(context.parserRequest.cwd());
273 context.systemPropertiesOverrides.put("user.dir", result.toString());
274 return result;
275 } else {
276 Path result = getCanonicalPath(Paths.get(System.getProperty("user.dir")));
277 mayOverrideDirectorySystemProperty(context, "user.dir", result);
278 return result;
279 }
280 }
281
282 protected Path getInstallationDirectory(LocalContext context) {
283 if (context.parserRequest.mavenHome() != null) {
284 Path result = getCanonicalPath(context.parserRequest.mavenHome());
285 context.systemPropertiesOverrides.put(Constants.MAVEN_HOME, result.toString());
286 return result;
287 } else {
288 String mavenHome = System.getProperty(Constants.MAVEN_HOME);
289 if (mavenHome == null) {
290 throw new IllegalStateException(
291 "local mode requires " + Constants.MAVEN_HOME + " Java System Property set");
292 }
293 Path result = getCanonicalPath(Paths.get(mavenHome));
294 mayOverrideDirectorySystemProperty(context, Constants.MAVEN_HOME, result);
295 return result;
296 }
297 }
298
299 protected Path getUserHomeDirectory(LocalContext context) {
300 if (context.parserRequest.userHome() != null) {
301 Path result = getCanonicalPath(context.parserRequest.userHome());
302 context.systemPropertiesOverrides.put("user.home", result.toString());
303 return result;
304 } else {
305 Path result = getCanonicalPath(Paths.get(System.getProperty("user.home")));
306 mayOverrideDirectorySystemProperty(context, "user.home", result);
307 return result;
308 }
309 }
310
311
312
313
314
315 protected void mayOverrideDirectorySystemProperty(LocalContext context, String javaSystemPropertyKey, Path value) {
316 String valueString = value.toString();
317 if (!Objects.equals(System.getProperty(javaSystemPropertyKey), valueString)) {
318 context.systemPropertiesOverrides.put(javaSystemPropertyKey, valueString);
319 }
320 }
321
322 protected Path getTopDirectory(LocalContext context) {
323
324
325 Path topDirectory = requireNonNull(context.cwd);
326 boolean isAltFile = false;
327 for (String arg : context.parserRequest.args()) {
328 if (isAltFile) {
329
330 Path path = topDirectory.resolve(stripLeadingAndTrailingQuotes(arg));
331 if (Files.isDirectory(path)) {
332 topDirectory = path;
333 } else if (Files.isRegularFile(path)) {
334 topDirectory = path.getParent();
335 if (!Files.isDirectory(topDirectory)) {
336 throw new IllegalArgumentException("Directory " + topDirectory
337 + " extracted from the -f/--file command-line argument " + arg + " does not exist");
338 }
339 } else {
340 throw new IllegalArgumentException(
341 "POM file " + arg + " specified with the -f/--file command line argument does not exist");
342 }
343 break;
344 } else {
345
346 isAltFile = arg.equals("-f") || arg.equals("--file");
347 }
348 }
349 return getCanonicalPath(topDirectory);
350 }
351
352 @Nullable
353 protected Path getRootDirectory(LocalContext context) {
354 return CliUtils.findRoot(context.topDirectory);
355 }
356
357 protected Map<String, String> populateSystemProperties(LocalContext context) {
358 Properties systemProperties = new Properties();
359
360
361
362
363
364 EnvironmentUtils.addEnvVars(systemProperties);
365 SystemProperties.addSystemProperties(systemProperties);
366
367
368
369
370
371
372 Properties buildProperties = CLIReportingUtils.getBuildProperties();
373
374 String mavenVersion = buildProperties.getProperty(CLIReportingUtils.BUILD_VERSION_PROPERTY);
375 systemProperties.setProperty(Constants.MAVEN_VERSION, mavenVersion);
376
377 boolean snapshot = mavenVersion.endsWith("SNAPSHOT");
378 if (snapshot) {
379 mavenVersion = mavenVersion.substring(0, mavenVersion.length() - "SNAPSHOT".length());
380 if (mavenVersion.endsWith("-")) {
381 mavenVersion = mavenVersion.substring(0, mavenVersion.length() - 1);
382 }
383 }
384 String[] versionElements = mavenVersion.split("\\.");
385 if (versionElements.length != 3) {
386 throw new IllegalStateException("Maven version is expected to have 3 segments: '" + mavenVersion + "'");
387 }
388 systemProperties.setProperty(Constants.MAVEN_VERSION_MAJOR, versionElements[0]);
389 systemProperties.setProperty(Constants.MAVEN_VERSION_MINOR, versionElements[1]);
390 systemProperties.setProperty(Constants.MAVEN_VERSION_PATCH, versionElements[2]);
391 systemProperties.setProperty(Constants.MAVEN_VERSION_SNAPSHOT, Boolean.toString(snapshot));
392
393 String mavenBuildVersion = CLIReportingUtils.createMavenVersionString(buildProperties);
394 systemProperties.setProperty(Constants.MAVEN_BUILD_VERSION, mavenBuildVersion);
395
396 Map<String, String> result = toMap(systemProperties);
397 result.putAll(context.systemPropertiesOverrides);
398 return result;
399 }
400
401 protected Map<String, String> populateUserProperties(LocalContext context) {
402 Properties userProperties = new Properties();
403 Map<String, String> paths = context.extraInterpolationSource();
404
405
406
407
408
409
410
411 Map<String, String> userSpecifiedProperties =
412 new HashMap<>(context.options.userProperties().orElse(new HashMap<>()));
413 createInterpolator().interpolate(userSpecifiedProperties, paths::get);
414
415
416
417
418 UnaryOperator<String> callback =
419 or(paths::get, prefix("cli.", userSpecifiedProperties::get), context.systemProperties::get);
420
421 Path mavenConf;
422 if (context.systemProperties.get(Constants.MAVEN_INSTALLATION_CONF) != null) {
423 mavenConf = context.installationDirectory.resolve(
424 context.systemProperties.get(Constants.MAVEN_INSTALLATION_CONF));
425 } else if (context.systemProperties.get("maven.conf") != null) {
426 mavenConf = context.installationDirectory.resolve(context.systemProperties.get("maven.conf"));
427 } else if (context.systemProperties.get(Constants.MAVEN_HOME) != null) {
428 mavenConf = context.installationDirectory
429 .resolve(context.systemProperties.get(Constants.MAVEN_HOME))
430 .resolve("conf");
431 } else {
432 mavenConf = context.installationDirectory.resolve("");
433 }
434 Path propertiesFile = mavenConf.resolve("maven.properties");
435 try {
436 MavenPropertiesLoader.loadProperties(userProperties, propertiesFile, callback, false);
437 } catch (IOException e) {
438 throw new IllegalStateException("Error loading properties from " + propertiesFile, e);
439 }
440
441
442 userProperties.putAll(userSpecifiedProperties);
443
444 return toMap(userProperties);
445 }
446
447 protected abstract Options parseCliOptions(LocalContext context);
448
449
450
451
452 protected List<CoreExtensions> readCoreExtensionsDescriptor(LocalContext context) {
453 ArrayList<CoreExtensions> result = new ArrayList<>();
454 Path file;
455 List<CoreExtension> loaded;
456
457
458 file = context.cwd.resolve(context.userProperties.get(Constants.MAVEN_PROJECT_EXTENSIONS));
459 loaded = readCoreExtensionsDescriptorFromFile(file);
460 if (!loaded.isEmpty()) {
461 result.add(new CoreExtensions(file, loaded));
462 }
463
464
465 file = context.userHomeDirectory.resolve(context.userProperties.get(Constants.MAVEN_USER_EXTENSIONS));
466 loaded = readCoreExtensionsDescriptorFromFile(file);
467 if (!loaded.isEmpty()) {
468 result.add(new CoreExtensions(file, loaded));
469 }
470
471
472 file = context.installationDirectory.resolve(
473 context.userProperties.get(Constants.MAVEN_INSTALLATION_EXTENSIONS));
474 loaded = readCoreExtensionsDescriptorFromFile(file);
475 if (!loaded.isEmpty()) {
476 result.add(new CoreExtensions(file, loaded));
477 }
478
479 return result.isEmpty() ? null : List.copyOf(result);
480 }
481
482 protected List<CoreExtension> readCoreExtensionsDescriptorFromFile(Path extensionsFile) {
483 try {
484 if (extensionsFile != null && Files.exists(extensionsFile)) {
485 try (InputStream is = Files.newInputStream(extensionsFile)) {
486 return validateCoreExtensionsDescriptorFromFile(
487 extensionsFile,
488 List.copyOf(new CoreExtensionsStaxReader()
489 .read(is, true, new InputSource(extensionsFile.toString()))
490 .getExtensions()));
491 }
492 }
493 return List.of();
494 } catch (XMLStreamException | IOException e) {
495 throw new IllegalArgumentException("Failed to parse extensions file: " + extensionsFile, e);
496 }
497 }
498
499 protected List<CoreExtension> validateCoreExtensionsDescriptorFromFile(
500 Path extensionFile, List<CoreExtension> coreExtensions) {
501 Map<String, List<InputLocation>> gasLocations = new HashMap<>();
502 for (CoreExtension coreExtension : coreExtensions) {
503 String ga = coreExtension.getGroupId() + ":" + coreExtension.getArtifactId();
504 InputLocation location = coreExtension.getLocation("");
505 gasLocations.computeIfAbsent(ga, k -> new ArrayList<>()).add(location);
506 }
507 if (gasLocations.values().stream().noneMatch(l -> l.size() > 1)) {
508 return coreExtensions;
509 }
510 throw new IllegalStateException("Extension conflicts in file " + extensionFile + ": "
511 + gasLocations.entrySet().stream()
512 .map(e -> e.getKey() + " defined on lines "
513 + e.getValue().stream()
514 .map(l -> String.valueOf(l.getLineNumber()))
515 .collect(Collectors.joining(", ")))
516 .collect(Collectors.joining("; ")));
517 }
518
519 @Nullable
520 protected CIInfo detectCI(LocalContext context) {
521 List<CIInfo> detected = CIDetectorHelper.detectCI();
522 if (detected.isEmpty()) {
523 return null;
524 } else if (detected.size() > 1) {
525
526 context.parserRequest
527 .logger()
528 .warn("Multiple CI systems detected: "
529 + detected.stream().map(CIInfo::name).collect(Collectors.joining(", ")));
530 }
531 return detected.get(0);
532 }
533 }