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
38 import org.apache.maven.api.Constants;
39 import org.apache.maven.api.annotations.Nullable;
40 import org.apache.maven.api.cli.InvokerRequest;
41 import org.apache.maven.api.cli.Options;
42 import org.apache.maven.api.cli.Parser;
43 import org.apache.maven.api.cli.ParserRequest;
44 import org.apache.maven.api.cli.extensions.CoreExtension;
45 import org.apache.maven.api.services.Interpolator;
46 import org.apache.maven.cling.internal.extension.io.CoreExtensionsStaxReader;
47 import org.apache.maven.cling.props.MavenPropertiesLoader;
48 import org.apache.maven.cling.utils.CLIReportingUtils;
49 import org.apache.maven.properties.internal.EnvironmentUtils;
50 import org.apache.maven.properties.internal.SystemProperties;
51
52 import static java.util.Objects.requireNonNull;
53 import static org.apache.maven.cling.invoker.Utils.getCanonicalPath;
54 import static org.apache.maven.cling.invoker.Utils.or;
55 import static org.apache.maven.cling.invoker.Utils.prefix;
56 import static org.apache.maven.cling.invoker.Utils.stripLeadingAndTrailingQuotes;
57 import static org.apache.maven.cling.invoker.Utils.toMap;
58
59 public abstract class BaseParser implements Parser {
60
61 @SuppressWarnings("VisibilityModifier")
62 public static class LocalContext {
63 public final ParserRequest parserRequest;
64 public final Map<String, String> systemPropertiesOverrides;
65
66 public LocalContext(ParserRequest parserRequest) {
67 this.parserRequest = parserRequest;
68 this.systemPropertiesOverrides = new HashMap<>();
69 }
70
71 public boolean parsingFailed = false;
72 public Path cwd;
73 public Path installationDirectory;
74 public Path userHomeDirectory;
75 public Map<String, String> systemProperties;
76 public Map<String, String> userProperties;
77 public Path topDirectory;
78
79 @Nullable
80 public Path rootDirectory;
81
82 public List<CoreExtension> extensions;
83 public Options options;
84
85 public Map<String, String> extraInterpolationSource() {
86 Map<String, String> extra = new HashMap<>();
87 extra.put("session.topDirectory", topDirectory.toString());
88 if (rootDirectory != null) {
89 extra.put("session.rootDirectory", rootDirectory.toString());
90 }
91 return extra;
92 }
93 }
94
95 @Override
96 public InvokerRequest parseInvocation(ParserRequest parserRequest) {
97 requireNonNull(parserRequest);
98
99 LocalContext context = new LocalContext(parserRequest);
100
101
102 try {
103 context.cwd = getCwd(context);
104 } catch (Exception e) {
105 context.parsingFailed = true;
106 context.cwd = getCanonicalPath(Paths.get("."));
107 parserRequest.logger().error("Error determining working directory", e);
108 }
109 try {
110 context.installationDirectory = getInstallationDirectory(context);
111 } catch (Exception e) {
112 context.parsingFailed = true;
113 context.installationDirectory = context.cwd;
114 parserRequest.logger().error("Error determining installation directory", e);
115 }
116 try {
117 context.userHomeDirectory = getUserHomeDirectory(context);
118 } catch (Exception e) {
119 context.parsingFailed = true;
120 context.userHomeDirectory = context.cwd;
121 parserRequest.logger().error("Error determining user home directory", e);
122 }
123
124
125 try {
126 context.topDirectory = getTopDirectory(context);
127 } catch (Exception e) {
128 context.parsingFailed = true;
129 context.topDirectory = context.cwd;
130 parserRequest.logger().error("Error determining top directory", e);
131 }
132 try {
133 context.rootDirectory = getRootDirectory(context);
134 } catch (Exception e) {
135 context.parsingFailed = true;
136 context.rootDirectory = context.cwd;
137 parserRequest.logger().error("Error determining root directory", e);
138 }
139
140
141 List<Options> parsedOptions;
142 try {
143 parsedOptions = parseCliOptions(context);
144 } catch (Exception e) {
145 context.parsingFailed = true;
146 parsedOptions = List.of(emptyOptions());
147 parserRequest.logger().error("Error parsing program arguments", e);
148 }
149
150
151 try {
152 context.options = assembleOptions(parsedOptions);
153 } catch (Exception e) {
154 context.parsingFailed = true;
155 context.options = emptyOptions();
156 parserRequest.logger().error("Error assembling program arguments", e);
157 }
158
159
160 try {
161 context.systemProperties = populateSystemProperties(context);
162 } catch (Exception e) {
163 context.parsingFailed = true;
164 context.systemProperties = new HashMap<>();
165 parserRequest.logger().error("Error populating system properties", e);
166 }
167 try {
168 context.userProperties = populateUserProperties(context);
169 } catch (Exception e) {
170 context.parsingFailed = true;
171 context.userProperties = new HashMap<>();
172 parserRequest.logger().error("Error populating user properties", e);
173 }
174
175
176 context.options = context.options.interpolate(Interpolator.chain(
177 context.extraInterpolationSource()::get, context.userProperties::get, context.systemProperties::get));
178
179
180 try {
181 context.extensions = readCoreExtensionsDescriptor(context);
182 } catch (Exception e) {
183 context.parsingFailed = true;
184 parserRequest.logger().error("Error reading core extensions descriptor", e);
185 }
186
187
188 if (!context.parsingFailed) {
189 validate(context);
190 }
191
192 return getInvokerRequest(context);
193 }
194
195 protected void validate(LocalContext context) {
196 Options options = context.options;
197
198 options.failOnSeverity().ifPresent(severity -> {
199 String c = severity.toLowerCase(Locale.ENGLISH);
200 if (!Arrays.asList("warn", "warning", "error").contains(c)) {
201 context.parsingFailed = true;
202 context.parserRequest
203 .logger()
204 .error("Invalid fail on severity threshold '" + c
205 + "'. Supported values are 'WARN', 'WARNING' and 'ERROR'.");
206 }
207 });
208 options.altUserSettings()
209 .ifPresent(userSettings ->
210 failIfFileNotExists(context, userSettings, "The specified user settings file does not exist"));
211 options.altProjectSettings()
212 .ifPresent(projectSettings -> failIfFileNotExists(
213 context, projectSettings, "The specified project settings file does not exist"));
214 options.altInstallationSettings()
215 .ifPresent(installationSettings -> failIfFileNotExists(
216 context, installationSettings, "The specified installation settings file does not exist"));
217 options.altUserToolchains()
218 .ifPresent(userToolchains -> failIfFileNotExists(
219 context, userToolchains, "The specified user toolchains file does not exist"));
220 options.altInstallationToolchains()
221 .ifPresent(installationToolchains -> failIfFileNotExists(
222 context, installationToolchains, "The specified installation toolchains file does not exist"));
223 options.color().ifPresent(color -> {
224 String c = color.toLowerCase(Locale.ENGLISH);
225 if (!Arrays.asList("always", "yes", "force", "never", "no", "none", "auto", "tty", "if-tty")
226 .contains(c)) {
227 context.parsingFailed = true;
228 context.parserRequest
229 .logger()
230 .error("Invalid color configuration value '" + c
231 + "'. Supported values are 'auto', 'always', 'never'.");
232 }
233 });
234 }
235
236 protected void failIfFileNotExists(LocalContext context, String fileName, String message) {
237 Path path = context.cwd.resolve(fileName);
238 if (!Files.isRegularFile(path)) {
239 context.parsingFailed = true;
240 context.parserRequest.logger().error(message + ": " + path);
241 }
242 }
243
244 protected abstract Options emptyOptions();
245
246 protected abstract InvokerRequest getInvokerRequest(LocalContext context);
247
248 protected Path getCwd(LocalContext context) {
249 if (context.parserRequest.cwd() != null) {
250 Path result = getCanonicalPath(context.parserRequest.cwd());
251 context.systemPropertiesOverrides.put("user.dir", result.toString());
252 return result;
253 } else {
254 Path result = getCanonicalPath(Paths.get(System.getProperty("user.dir")));
255 mayOverrideDirectorySystemProperty(context, "user.dir", result);
256 return result;
257 }
258 }
259
260 protected Path getInstallationDirectory(LocalContext context) {
261 if (context.parserRequest.mavenHome() != null) {
262 Path result = getCanonicalPath(context.parserRequest.mavenHome());
263 context.systemPropertiesOverrides.put(Constants.MAVEN_HOME, result.toString());
264 return result;
265 } else {
266 String mavenHome = System.getProperty(Constants.MAVEN_HOME);
267 if (mavenHome == null) {
268 throw new IllegalStateException(
269 "local mode requires " + Constants.MAVEN_HOME + " Java System Property set");
270 }
271 Path result = getCanonicalPath(Paths.get(mavenHome));
272 mayOverrideDirectorySystemProperty(context, Constants.MAVEN_HOME, result);
273 return result;
274 }
275 }
276
277 protected Path getUserHomeDirectory(LocalContext context) {
278 if (context.parserRequest.userHome() != null) {
279 Path result = getCanonicalPath(context.parserRequest.userHome());
280 context.systemPropertiesOverrides.put("user.home", result.toString());
281 return result;
282 } else {
283 Path result = getCanonicalPath(Paths.get(System.getProperty("user.home")));
284 mayOverrideDirectorySystemProperty(context, "user.home", result);
285 return result;
286 }
287 }
288
289
290
291
292
293 protected void mayOverrideDirectorySystemProperty(LocalContext context, String javaSystemPropertyKey, Path value) {
294 String valueString = value.toString();
295 if (!Objects.equals(System.getProperty(javaSystemPropertyKey), valueString)) {
296 context.systemPropertiesOverrides.put(javaSystemPropertyKey, valueString);
297 }
298 }
299
300 protected Path getTopDirectory(LocalContext context) {
301
302
303 Path topDirectory = requireNonNull(context.cwd);
304 boolean isAltFile = false;
305 for (String arg : context.parserRequest.args()) {
306 if (isAltFile) {
307
308 Path path = topDirectory.resolve(stripLeadingAndTrailingQuotes(arg));
309 if (Files.isDirectory(path)) {
310 topDirectory = path;
311 } else if (Files.isRegularFile(path)) {
312 topDirectory = path.getParent();
313 if (!Files.isDirectory(topDirectory)) {
314 throw new IllegalArgumentException("Directory " + topDirectory
315 + " extracted from the -f/--file command-line argument " + arg + " does not exist");
316 }
317 } else {
318 throw new IllegalArgumentException(
319 "POM file " + arg + " specified with the -f/--file command line argument does not exist");
320 }
321 break;
322 } else {
323
324 isAltFile = arg.equals("-f") || arg.equals("--file");
325 }
326 }
327 return getCanonicalPath(topDirectory);
328 }
329
330 @Nullable
331 protected Path getRootDirectory(LocalContext context) {
332 return Utils.findRoot(context.topDirectory);
333 }
334
335 protected Map<String, String> populateSystemProperties(LocalContext context) {
336 Properties systemProperties = new Properties();
337
338
339
340
341
342 EnvironmentUtils.addEnvVars(systemProperties);
343 SystemProperties.addSystemProperties(systemProperties);
344
345
346
347
348
349
350 Properties buildProperties = CLIReportingUtils.getBuildProperties();
351
352 String mavenVersion = buildProperties.getProperty(CLIReportingUtils.BUILD_VERSION_PROPERTY);
353 systemProperties.setProperty(Constants.MAVEN_VERSION, mavenVersion);
354
355 boolean snapshot = mavenVersion.endsWith("SNAPSHOT");
356 if (snapshot) {
357 mavenVersion = mavenVersion.substring(0, mavenVersion.length() - "SNAPSHOT".length());
358 if (mavenVersion.endsWith("-")) {
359 mavenVersion = mavenVersion.substring(0, mavenVersion.length() - 1);
360 }
361 }
362 String[] versionElements = mavenVersion.split("\\.");
363 if (versionElements.length != 3) {
364 throw new IllegalStateException("Maven version is expected to have 3 segments: '" + mavenVersion + "'");
365 }
366 systemProperties.setProperty(Constants.MAVEN_VERSION_MAJOR, versionElements[0]);
367 systemProperties.setProperty(Constants.MAVEN_VERSION_MINOR, versionElements[1]);
368 systemProperties.setProperty(Constants.MAVEN_VERSION_PATCH, versionElements[2]);
369 systemProperties.setProperty(Constants.MAVEN_VERSION_SNAPSHOT, Boolean.toString(snapshot));
370
371 String mavenBuildVersion = CLIReportingUtils.createMavenVersionString(buildProperties);
372 systemProperties.setProperty(Constants.MAVEN_BUILD_VERSION, mavenBuildVersion);
373
374 Map<String, String> result = toMap(systemProperties);
375 result.putAll(context.systemPropertiesOverrides);
376 return result;
377 }
378
379 protected Map<String, String> populateUserProperties(LocalContext context) {
380 Properties userProperties = new Properties();
381
382
383
384
385
386
387
388 Map<String, String> userSpecifiedProperties =
389 context.options.userProperties().orElse(new HashMap<>());
390
391
392
393
394 Map<String, String> paths = context.extraInterpolationSource();
395 UnaryOperator<String> callback =
396 or(paths::get, prefix("cli.", userSpecifiedProperties::get), context.systemProperties::get);
397
398 Path mavenConf;
399 if (context.systemProperties.get(Constants.MAVEN_INSTALLATION_CONF) != null) {
400 mavenConf = context.installationDirectory.resolve(
401 context.systemProperties.get(Constants.MAVEN_INSTALLATION_CONF));
402 } else if (context.systemProperties.get("maven.conf") != null) {
403 mavenConf = context.installationDirectory.resolve(context.systemProperties.get("maven.conf"));
404 } else if (context.systemProperties.get(Constants.MAVEN_HOME) != null) {
405 mavenConf = context.installationDirectory
406 .resolve(context.systemProperties.get(Constants.MAVEN_HOME))
407 .resolve("conf");
408 } else {
409 mavenConf = context.installationDirectory.resolve("");
410 }
411 Path propertiesFile = mavenConf.resolve("maven.properties");
412 try {
413 MavenPropertiesLoader.loadProperties(userProperties, propertiesFile, callback, false);
414 } catch (IOException e) {
415 throw new IllegalStateException("Error loading properties from " + propertiesFile, e);
416 }
417
418
419 userProperties.putAll(userSpecifiedProperties);
420
421 return toMap(userProperties);
422 }
423
424 protected abstract List<Options> parseCliOptions(LocalContext context);
425
426 protected abstract Options assembleOptions(List<Options> parsedOptions);
427
428 protected List<CoreExtension> readCoreExtensionsDescriptor(LocalContext context) {
429 String installationExtensionsFile = context.userProperties.get(Constants.MAVEN_INSTALLATION_EXTENSIONS);
430 ArrayList<CoreExtension> installationExtensions = new ArrayList<>(readCoreExtensionsDescriptorFromFile(
431 context.installationDirectory.resolve(installationExtensionsFile)));
432
433 String userExtensionsFile = context.userProperties.get(Constants.MAVEN_USER_EXTENSIONS);
434 ArrayList<CoreExtension> userExtensions = new ArrayList<>(
435 readCoreExtensionsDescriptorFromFile(context.userHomeDirectory.resolve(userExtensionsFile)));
436
437 String projectExtensionsFile = context.userProperties.get(Constants.MAVEN_PROJECT_EXTENSIONS);
438 ArrayList<CoreExtension> projectExtensions =
439 new ArrayList<>(readCoreExtensionsDescriptorFromFile(context.cwd.resolve(projectExtensionsFile)));
440
441
442 HashMap<String, String> gas = new HashMap<>();
443 ArrayList<String> conflicts = new ArrayList<>();
444
445 ArrayList<CoreExtension> coreExtensions =
446 new ArrayList<>(installationExtensions.size() + userExtensions.size() + projectExtensions.size());
447 coreExtensions.addAll(mergeExtensions(installationExtensions, installationExtensionsFile, gas, conflicts));
448 coreExtensions.addAll(mergeExtensions(userExtensions, userExtensionsFile, gas, conflicts));
449 coreExtensions.addAll(mergeExtensions(projectExtensions, projectExtensionsFile, gas, conflicts));
450
451 if (!conflicts.isEmpty()) {
452 throw new IllegalStateException("Extension conflicts: " + String.join("; ", conflicts));
453 }
454
455 return coreExtensions;
456 }
457
458 private List<CoreExtension> mergeExtensions(
459 List<CoreExtension> extensions, String extensionsSource, Map<String, String> gas, List<String> conflicts) {
460 for (CoreExtension extension : extensions) {
461 String ga = extension.getGroupId() + ":" + extension.getArtifactId();
462 if (gas.containsKey(ga)) {
463 conflicts.add(ga + " from " + extensionsSource + " already specified in " + gas.get(ga));
464 } else {
465 gas.put(ga, extensionsSource);
466 }
467 }
468 return extensions;
469 }
470
471 protected List<CoreExtension> readCoreExtensionsDescriptorFromFile(Path extensionsFile) {
472 try {
473 if (extensionsFile != null && Files.exists(extensionsFile)) {
474 try (InputStream is = Files.newInputStream(extensionsFile)) {
475 return new CoreExtensionsStaxReader().read(is, true).getExtensions();
476 }
477 }
478 return List.of();
479 } catch (XMLStreamException | IOException e) {
480 throw new IllegalArgumentException("Failed to parse extensions file: " + extensionsFile, e);
481 }
482 }
483 }