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         // core extensions
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         // CI detection
195         context.ciInfo = detectCI(context);
196 
197         // only if not failed so far; otherwise we may have no options to validate
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      * This method is needed to "align" values used later on for interpolations and path calculations.
313      * We enforce "canonical" paths, so IF key and canonical path value disagree, let override it.
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         // We need to locate the top level project which may be pointed at using
324         // the -f/--file option.
325         Path topDirectory = requireNonNull(context.cwd);
326         boolean isAltFile = false;
327         for (String arg : context.parserRequest.args()) {
328             if (isAltFile) {
329                 // this is the argument following -f/--file
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                 // Check if this is the -f/--file option
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         // Load environment and system properties
362         // ----------------------------------------------------------------------
363 
364         EnvironmentUtils.addEnvVars(systemProperties);
365         SystemProperties.addSystemProperties(systemProperties);
366 
367         // ----------------------------------------------------------------------
368         // Properties containing info about the currently running version of Maven
369         // These override any corresponding properties set on the command line
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         // Options that are set on the command line become system properties
407         // and therefore are set in the session properties. System properties
408         // are most dominant.
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         // Load config files
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         // CLI specified properties are most dominant
442         userProperties.putAll(userSpecifiedProperties);
443 
444         return toMap(userProperties);
445     }
446 
447     protected abstract Options parseCliOptions(LocalContext context);
448 
449     /**
450      * Important: This method must return list of {@link CoreExtensions} in precedence order.
451      */
452     protected List<CoreExtensions> readCoreExtensionsDescriptor(LocalContext context) {
453         ArrayList<CoreExtensions> result = new ArrayList<>();
454         Path file;
455         List<CoreExtension> loaded;
456 
457         // project
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         // user
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         // installation
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             // warn
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 }