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.io.PrintWriter;
26  import java.nio.file.Files;
27  import java.nio.file.Path;
28  import java.nio.file.Paths;
29  import java.util.ArrayList;
30  import java.util.Arrays;
31  import java.util.HashMap;
32  import java.util.List;
33  import java.util.Map;
34  import java.util.Objects;
35  import java.util.Properties;
36  import java.util.function.Function;
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.InvokerRequest;
42  import org.apache.maven.api.cli.Options;
43  import org.apache.maven.api.cli.Parser;
44  import org.apache.maven.api.cli.ParserException;
45  import org.apache.maven.api.cli.ParserRequest;
46  import org.apache.maven.api.cli.extensions.CoreExtension;
47  import org.apache.maven.cling.internal.extension.io.CoreExtensionsStaxReader;
48  import org.apache.maven.cling.props.MavenPropertiesLoader;
49  import org.apache.maven.cling.utils.CLIReportingUtils;
50  import org.apache.maven.properties.internal.EnvironmentUtils;
51  import org.apache.maven.properties.internal.SystemProperties;
52  
53  import static java.util.Objects.requireNonNull;
54  import static org.apache.maven.cling.invoker.Utils.getCanonicalPath;
55  import static org.apache.maven.cling.invoker.Utils.or;
56  import static org.apache.maven.cling.invoker.Utils.prefix;
57  import static org.apache.maven.cling.invoker.Utils.stripLeadingAndTrailingQuotes;
58  import static org.apache.maven.cling.invoker.Utils.toMap;
59  
60  public abstract class BaseParser implements Parser {
61  
62      @SuppressWarnings("VisibilityModifier")
63      public static class LocalContext {
64          public final ParserRequest parserRequest;
65          public final Map<String, String> systemPropertiesOverrides;
66  
67          public LocalContext(ParserRequest parserRequest) {
68              this.parserRequest = parserRequest;
69              this.systemPropertiesOverrides = new HashMap<>();
70          }
71  
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) throws ParserException, IOException {
97          requireNonNull(parserRequest);
98  
99          LocalContext context = new LocalContext(parserRequest);
100 
101         // the basics
102         context.cwd = requireNonNull(getCwd(context));
103         context.installationDirectory = requireNonNull(getInstallationDirectory(context));
104         context.userHomeDirectory = requireNonNull(getUserHomeDirectory(context));
105 
106         // top/root
107         context.topDirectory = requireNonNull(getTopDirectory(context));
108         context.rootDirectory = getRootDirectory(context);
109 
110         // options
111         List<Options> parsedOptions = parseCliOptions(context);
112 
113         // warn about deprecated options
114         PrintWriter printWriter = new PrintWriter(parserRequest.out() != null ? parserRequest.out() : System.out, true);
115         parsedOptions.forEach(o -> o.warnAboutDeprecatedOptions(parserRequest, printWriter::println));
116 
117         // assemble options if needed
118         context.options = assembleOptions(parsedOptions);
119 
120         // system and user properties
121         context.systemProperties = populateSystemProperties(context);
122         context.userProperties = populateUserProperties(context);
123 
124         // options: interpolate
125         context.options = context.options.interpolate(
126                 Arrays.asList(context.extraInterpolationSource(), context.userProperties, context.systemProperties));
127 
128         // core extensions
129         context.extensions = readCoreExtensionsDescriptor(context);
130 
131         return getInvokerRequest(context);
132     }
133 
134     protected abstract InvokerRequest getInvokerRequest(LocalContext context);
135 
136     protected Path getCwd(LocalContext context) throws ParserException {
137         if (context.parserRequest.cwd() != null) {
138             Path result = getCanonicalPath(context.parserRequest.cwd());
139             context.systemPropertiesOverrides.put("user.dir", result.toString());
140             return result;
141         } else {
142             Path result = getCanonicalPath(Paths.get(System.getProperty("user.dir")));
143             mayOverrideDirectorySystemProperty(context, "user.dir", result);
144             return result;
145         }
146     }
147 
148     protected Path getInstallationDirectory(LocalContext context) throws ParserException {
149         if (context.parserRequest.mavenHome() != null) {
150             Path result = getCanonicalPath(context.parserRequest.mavenHome());
151             context.systemPropertiesOverrides.put(Constants.MAVEN_HOME, result.toString());
152             return result;
153         } else {
154             String mavenHome = System.getProperty(Constants.MAVEN_HOME);
155             if (mavenHome == null) {
156                 throw new ParserException("local mode requires " + Constants.MAVEN_HOME + " Java System Property set");
157             }
158             Path result = getCanonicalPath(Paths.get(mavenHome));
159             mayOverrideDirectorySystemProperty(context, Constants.MAVEN_HOME, result);
160             return result;
161         }
162     }
163 
164     protected Path getUserHomeDirectory(LocalContext context) throws ParserException {
165         if (context.parserRequest.userHome() != null) {
166             Path result = getCanonicalPath(context.parserRequest.userHome());
167             context.systemPropertiesOverrides.put("user.home", result.toString());
168             return result;
169         } else {
170             Path result = getCanonicalPath(Paths.get(System.getProperty("user.home")));
171             mayOverrideDirectorySystemProperty(context, "user.home", result);
172             return result;
173         }
174     }
175 
176     /**
177      * This method is needed to "align" values used later on for interpolations and path calculations.
178      * We enforce "canonical" paths, so IF key and canonical path value disagree, let override it.
179      */
180     protected void mayOverrideDirectorySystemProperty(LocalContext context, String javaSystemPropertyKey, Path value) {
181         String valueString = value.toString();
182         if (!Objects.equals(System.getProperty(javaSystemPropertyKey), valueString)) {
183             context.systemPropertiesOverrides.put(javaSystemPropertyKey, valueString);
184         }
185     }
186 
187     protected Path getTopDirectory(LocalContext context) throws ParserException {
188         // We need to locate the top level project which may be pointed at using
189         // the -f/--file option.
190         Path topDirectory = requireNonNull(context.cwd);
191         boolean isAltFile = false;
192         for (String arg : context.parserRequest.args()) {
193             if (isAltFile) {
194                 // this is the argument following -f/--file
195                 Path path = topDirectory.resolve(stripLeadingAndTrailingQuotes(arg));
196                 if (Files.isDirectory(path)) {
197                     topDirectory = path;
198                 } else if (Files.isRegularFile(path)) {
199                     topDirectory = path.getParent();
200                     if (!Files.isDirectory(topDirectory)) {
201                         throw new ParserException("Directory " + topDirectory
202                                 + " extracted from the -f/--file command-line argument " + arg + " does not exist");
203                     }
204                 } else {
205                     throw new ParserException(
206                             "POM file " + arg + " specified with the -f/--file command line argument does not exist");
207                 }
208                 break;
209             } else {
210                 // Check if this is the -f/--file option
211                 isAltFile = arg.equals("-f") || arg.equals("--file");
212             }
213         }
214         return getCanonicalPath(topDirectory);
215     }
216 
217     @Nullable
218     protected Path getRootDirectory(LocalContext context) throws ParserException {
219         return Utils.findRoot(context.topDirectory);
220     }
221 
222     protected Map<String, String> populateSystemProperties(LocalContext context) throws ParserException {
223         Properties systemProperties = new Properties();
224 
225         // ----------------------------------------------------------------------
226         // Load environment and system properties
227         // ----------------------------------------------------------------------
228 
229         EnvironmentUtils.addEnvVars(systemProperties);
230         SystemProperties.addSystemProperties(systemProperties);
231 
232         // ----------------------------------------------------------------------
233         // Properties containing info about the currently running version of Maven
234         // These override any corresponding properties set on the command line
235         // ----------------------------------------------------------------------
236 
237         Properties buildProperties = CLIReportingUtils.getBuildProperties();
238 
239         String mavenVersion = buildProperties.getProperty(CLIReportingUtils.BUILD_VERSION_PROPERTY);
240         systemProperties.setProperty("maven.version", mavenVersion);
241 
242         String mavenBuildVersion = CLIReportingUtils.createMavenVersionString(buildProperties);
243         systemProperties.setProperty("maven.build.version", mavenBuildVersion);
244 
245         Map<String, String> result = toMap(systemProperties);
246         result.putAll(context.systemPropertiesOverrides);
247         return result;
248     }
249 
250     protected Map<String, String> populateUserProperties(LocalContext context) throws ParserException, IOException {
251         Properties userProperties = new Properties();
252 
253         // ----------------------------------------------------------------------
254         // Options that are set on the command line become system properties
255         // and therefore are set in the session properties. System properties
256         // are most dominant.
257         // ----------------------------------------------------------------------
258 
259         Map<String, String> userSpecifiedProperties =
260                 context.options.userProperties().orElse(new HashMap<>());
261 
262         // ----------------------------------------------------------------------
263         // Load config files
264         // ----------------------------------------------------------------------
265         Map<String, String> paths = context.extraInterpolationSource();
266         Function<String, String> callback =
267                 or(paths::get, prefix("cli.", userSpecifiedProperties::get), context.systemProperties::get);
268 
269         Path mavenConf;
270         if (context.systemProperties.get(Constants.MAVEN_INSTALLATION_CONF) != null) {
271             mavenConf = context.installationDirectory.resolve(
272                     context.systemProperties.get(Constants.MAVEN_INSTALLATION_CONF));
273         } else if (context.systemProperties.get("maven.conf") != null) {
274             mavenConf = context.installationDirectory.resolve(context.systemProperties.get("maven.conf"));
275         } else if (context.systemProperties.get(Constants.MAVEN_HOME) != null) {
276             mavenConf = context.installationDirectory
277                     .resolve(context.systemProperties.get(Constants.MAVEN_HOME))
278                     .resolve("conf");
279         } else {
280             mavenConf = context.installationDirectory.resolve("");
281         }
282         Path propertiesFile = mavenConf.resolve("maven.properties");
283         MavenPropertiesLoader.loadProperties(userProperties, propertiesFile, callback, false);
284 
285         // CLI specified properties are most dominant
286         userProperties.putAll(userSpecifiedProperties);
287 
288         return toMap(userProperties);
289     }
290 
291     protected abstract List<Options> parseCliOptions(LocalContext context) throws ParserException, IOException;
292 
293     protected abstract Options assembleOptions(List<Options> parsedOptions);
294 
295     protected List<CoreExtension> readCoreExtensionsDescriptor(LocalContext context)
296             throws ParserException, IOException {
297         ArrayList<CoreExtension> extensions = new ArrayList<>();
298         String installationExtensionsFile = context.userProperties.get(Constants.MAVEN_INSTALLATION_EXTENSIONS);
299         extensions.addAll(readCoreExtensionsDescriptorFromFile(
300                 context.installationDirectory.resolve(installationExtensionsFile)));
301 
302         String projectExtensionsFile = context.userProperties.get(Constants.MAVEN_PROJECT_EXTENSIONS);
303         extensions.addAll(readCoreExtensionsDescriptorFromFile(context.cwd.resolve(projectExtensionsFile)));
304 
305         String userExtensionsFile = context.userProperties.get(Constants.MAVEN_USER_EXTENSIONS);
306         extensions.addAll(readCoreExtensionsDescriptorFromFile(context.userHomeDirectory.resolve(userExtensionsFile)));
307 
308         return extensions;
309     }
310 
311     protected List<CoreExtension> readCoreExtensionsDescriptorFromFile(Path extensionsFile)
312             throws ParserException, IOException {
313         try {
314             if (extensionsFile != null && Files.exists(extensionsFile)) {
315                 try (InputStream is = Files.newInputStream(extensionsFile)) {
316                     return new CoreExtensionsStaxReader().read(is, true).getExtensions();
317                 }
318             }
319             return List.of();
320         } catch (XMLStreamException e) {
321             throw new ParserException("Failed to parse extensions file: " + extensionsFile, e);
322         }
323     }
324 
325     protected List<String> getJvmArguments(Path rootDirectory) throws ParserException {
326         if (rootDirectory != null) {
327             Path jvmConfig = rootDirectory.resolve(".mvn/jvm.config");
328             if (Files.exists(jvmConfig)) {
329                 try {
330                     return Files.readAllLines(jvmConfig).stream()
331                             .filter(l -> !l.isBlank() && !l.startsWith("#"))
332                             .flatMap(l -> Arrays.stream(l.split(" ")))
333                             .collect(Collectors.toList());
334                 } catch (IOException e) {
335                     throw new ParserException("Failed to read JVM configuration file: " + jvmConfig, e);
336                 }
337             }
338         }
339         return null;
340     }
341 }