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  
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         // the basics
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         // top/root
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         // options
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         // assemble options if needed
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         // system and user properties
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         // options: interpolate
176         context.options = context.options.interpolate(Interpolator.chain(
177                 context.extraInterpolationSource()::get, context.userProperties::get, context.systemProperties::get));
178 
179         // core extensions
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         // only if not failed so far; otherwise we may have no options to validate
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      * This method is needed to "align" values used later on for interpolations and path calculations.
291      * We enforce "canonical" paths, so IF key and canonical path value disagree, let override it.
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         // We need to locate the top level project which may be pointed at using
302         // the -f/--file option.
303         Path topDirectory = requireNonNull(context.cwd);
304         boolean isAltFile = false;
305         for (String arg : context.parserRequest.args()) {
306             if (isAltFile) {
307                 // this is the argument following -f/--file
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                 // Check if this is the -f/--file option
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         // Load environment and system properties
340         // ----------------------------------------------------------------------
341 
342         EnvironmentUtils.addEnvVars(systemProperties);
343         SystemProperties.addSystemProperties(systemProperties);
344 
345         // ----------------------------------------------------------------------
346         // Properties containing info about the currently running version of Maven
347         // These override any corresponding properties set on the command line
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         // Options that are set on the command line become system properties
384         // and therefore are set in the session properties. System properties
385         // are most dominant.
386         // ----------------------------------------------------------------------
387 
388         Map<String, String> userSpecifiedProperties =
389                 context.options.userProperties().orElse(new HashMap<>());
390 
391         // ----------------------------------------------------------------------
392         // Load config files
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         // CLI specified properties are most dominant
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         // merge these 3 but check for GA uniqueness; we don't want to load up same extension w/ different Vs
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 }