Skip to content

Commit

Permalink
[MNG-7774] Maven config and command line interpolation (#1098)
Browse files Browse the repository at this point in the history
Reuse as much as possible from master, but keep existing stuff like multiModuleProjectDirectory alone.

Changes:
* interpolate user properties and arguments
* introduce session.topDirectory and session.rootDirectory expressions (for interpolation only)
* Maven fails to start if any of the new properties are undefined but their use is attempted
* leave everything else untouched

---

https://issues.apache.org/jira/browse/MNG-7774
  • Loading branch information
cstamas authored May 5, 2023
1 parent 7cb87a6 commit 79556dd
Show file tree
Hide file tree
Showing 3 changed files with 216 additions and 32 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
package org.apache.maven.cli;

import java.io.File;
import java.nio.file.Path;
import java.util.Properties;

import org.apache.commons.cli.CommandLine;
Expand All @@ -40,6 +41,10 @@ public class CliRequest {

File multiModuleProjectDirectory;

Path rootDirectory;

Path topDirectory;

boolean debug;

boolean quiet;
Expand Down
207 changes: 175 additions & 32 deletions maven-embedder/src/main/java/org/apache/maven/cli/MavenCli.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,14 @@
import java.io.PrintStream;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
Expand Down Expand Up @@ -102,6 +105,9 @@
import org.codehaus.plexus.classworlds.realm.ClassRealm;
import org.codehaus.plexus.classworlds.realm.NoSuchRealmException;
import org.codehaus.plexus.component.repository.exception.ComponentLookupException;
import org.codehaus.plexus.interpolation.AbstractValueSource;
import org.codehaus.plexus.interpolation.InterpolationException;
import org.codehaus.plexus.interpolation.StringSearchInterpolator;
import org.codehaus.plexus.logging.LoggerManager;
import org.codehaus.plexus.util.StringUtils;
import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
Expand Down Expand Up @@ -140,9 +146,14 @@ public class MavenCli {

private static final String EXT_CLASS_PATH = "maven.ext.class.path";

private static final String EXTENSIONS_FILENAME = ".mvn/extensions.xml";
private static final String DOT_MVN = ".mvn";

private static final String MVN_MAVEN_CONFIG = ".mvn/maven.config";
private static final String UNABLE_TO_FIND_ROOT_PROJECT_MESSAGE = "Unable to find the root directory. Create a "
+ DOT_MVN + " directory in the project root directory to identify it.";

private static final String EXTENSIONS_FILENAME = DOT_MVN + "/extensions.xml";

private static final String MVN_MAVEN_CONFIG = DOT_MVN + "/maven.config";

public static final String STYLE_COLOR_PROPERTY = "style.color";

Expand Down Expand Up @@ -309,6 +320,47 @@ void initialize(CliRequest cliRequest) throws ExitException {
}
}

// We need to locate the top level project which may be pointed at using
// the -f/--file option. However, the command line isn't parsed yet, so
// we need to iterate through the args to find it and act upon it.
Path topDirectory = Paths.get(cliRequest.workingDirectory);
boolean isAltFile = false;
for (String arg : cliRequest.args) {
if (isAltFile) {
// this is the argument following -f/--file
Path path = topDirectory.resolve(arg);
if (Files.isDirectory(path)) {
topDirectory = path;
} else if (Files.isRegularFile(path)) {
topDirectory = path.getParent();
if (!Files.isDirectory(topDirectory)) {
System.err.println("Directory " + topDirectory
+ " extracted from the -f/--file command-line argument " + arg + " does not exist");
throw new ExitException(1);
}
} else {
System.err.println(
"POM file " + arg + " specified with the -f/--file command line argument does not exist");
throw new ExitException(1);
}
break;
} else {
// Check if this is the -f/--file option
isAltFile = arg.equals(String.valueOf(CLIManager.ALTERNATE_POM_FILE)) || arg.equals("file");
}
}
try {
topDirectory = topDirectory.toAbsolutePath().toRealPath();
} catch (IOException e) {
System.err.println("Error computing real path from " + topDirectory + ": " + e.getMessage());
throw new ExitException(1);
}
cliRequest.topDirectory = topDirectory;
// We're very early in the process and we don't have the container set up yet,
// so we on searchAcceptableRootDirectory method to find us acceptable directory.
// The method may return null if nothing acceptable found.
cliRequest.rootDirectory = searchAcceptableRootDirectory(topDirectory);

//
// Make sure the Maven home directory is an absolute path to save us from confusion with say drive-relative
// Windows paths.
Expand Down Expand Up @@ -526,8 +578,39 @@ private void commands(CliRequest cliRequest) {

// Needed to make this method package visible to make writing a unit test possible
// Maybe it's better to move some of those methods to separate class (SoC).
void properties(CliRequest cliRequest) {
populateProperties(cliRequest.commandLine, cliRequest.systemProperties, cliRequest.userProperties);
void properties(CliRequest cliRequest) throws ExitException {
try {
populateProperties(cliRequest, cliRequest.systemProperties, cliRequest.userProperties);

StringSearchInterpolator interpolator =
createInterpolator(cliRequest, cliRequest.systemProperties, cliRequest.userProperties);
CommandLine.Builder commandLineBuilder = new CommandLine.Builder();
for (Option option : cliRequest.commandLine.getOptions()) {
if (!String.valueOf(CLIManager.SET_USER_PROPERTY).equals(option.getOpt())) {
List<String> values = option.getValuesList();
for (ListIterator<String> it = values.listIterator(); it.hasNext(); ) {
it.set(interpolator.interpolate(it.next()));
}
}
commandLineBuilder.addOption(option);
}
for (String arg : cliRequest.commandLine.getArgList()) {
commandLineBuilder.addArg(interpolator.interpolate(arg));
}
cliRequest.commandLine = commandLineBuilder.build();
} catch (InterpolationException e) {
String message = "ERROR: Could not interpolate properties and/or arguments: " + e.getMessage();
System.err.println(message);
throw new ExitException(1); // user error
} catch (IllegalUseOfUndefinedProperty e) {
String message = "ERROR: Illegal use of undefined property: " + e.property;
System.err.println(message);
if (cliRequest.rootDirectory == null) {
System.err.println();
System.err.println(UNABLE_TO_FIND_ROOT_PROJECT_MESSAGE);
}
throw new ExitException(1); // user error
}
}

PlexusContainer container(CliRequest cliRequest) throws Exception {
Expand Down Expand Up @@ -1405,27 +1488,54 @@ int calculateDegreeOfConcurrency(String threadConfiguration) {
// Properties handling
// ----------------------------------------------------------------------

static void populateProperties(CommandLine commandLine, Properties systemProperties, Properties userProperties) {
EnvironmentUtils.addEnvVars(systemProperties);
static void populateProperties(CliRequest cliRequest, Properties systemProperties, Properties userProperties)
throws InterpolationException {

// ----------------------------------------------------------------------
// Options that are set on the command line become system properties
// and therefore are set in the session properties. System properties
// are most dominant.
// ----------------------------------------------------------------------

if (commandLine.hasOption(CLIManager.SET_USER_PROPERTY)) {
String[] defStrs = commandLine.getOptionValues(CLIManager.SET_USER_PROPERTY);
Properties cliProperties = new Properties();
if (cliRequest.commandLine.hasOption(CLIManager.SET_USER_PROPERTY)) {
String[] defStrs = cliRequest.commandLine.getOptionValues(CLIManager.SET_USER_PROPERTY);

if (defStrs != null) {
for (String defStr : defStrs) {
setCliProperty(defStr, userProperties);
String name;
String value;
for (String property : defStrs) {
int i = property.indexOf('=');
if (i <= 0) {
name = property.trim();
value = "true";
} else {
name = property.substring(0, i).trim();
value = property.substring(i + 1);
}
cliProperties.setProperty(name, value);
}
}
}

EnvironmentUtils.addEnvVars(systemProperties);
SystemProperties.addSystemProperties(systemProperties);

StringSearchInterpolator interpolator = createInterpolator(cliRequest, cliProperties, systemProperties);
for (Map.Entry<Object, Object> e : cliProperties.entrySet()) {
String name = (String) e.getKey();
String value = interpolator.interpolate((String) e.getValue());
userProperties.setProperty(name, value);
}

systemProperties.putAll(userProperties);

// ----------------------------------------------------------------------
// I'm leaving the setting of system properties here as not to break
// the SystemPropertyProfileActivator. This won't harm embedding. jvz.
// ----------------------------------------------------------------------
userProperties.forEach((k, v) -> System.setProperty((String) k, (String) v));

// ----------------------------------------------------------------------
// Properties containing info about the currently running version of Maven
// These override any corresponding properties set on the command line
Expand All @@ -1440,31 +1550,56 @@ static void populateProperties(CommandLine commandLine, Properties systemPropert
systemProperties.setProperty("maven.build.version", mavenBuildVersion);
}

private static void setCliProperty(String property, Properties properties) {
String name;

String value;

int i = property.indexOf('=');

if (i <= 0) {
name = property.trim();

value = "true";
} else {
name = property.substring(0, i).trim();
protected boolean isAcceptableRootDirectory(Path path) {
return path != null && Files.isDirectory(path.resolve(DOT_MVN));
}

value = property.substring(i + 1);
protected Path searchAcceptableRootDirectory(Path path) {
if (path == null) {
return null;
}
if (isAcceptableRootDirectory(path)) {
return path;
}
return searchAcceptableRootDirectory(path.getParent());
}

properties.setProperty(name, value);

// ----------------------------------------------------------------------
// I'm leaving the setting of system properties here as not to break
// the SystemPropertyProfileActivator. This won't harm embedding. jvz.
// ----------------------------------------------------------------------

System.setProperty(name, value);
protected static StringSearchInterpolator createInterpolator(CliRequest cliRequest, Properties... properties) {
StringSearchInterpolator interpolator = new StringSearchInterpolator();
interpolator.addValueSource(new AbstractValueSource(false) {
@Override
public Object getValue(String expression) {
if ("session.topDirectory".equals(expression)) {
Path topDirectory = cliRequest.topDirectory;
if (topDirectory != null) {
return topDirectory.toString();
} else {
throw new IllegalUseOfUndefinedProperty(expression);
}
} else if ("session.rootDirectory".equals(expression)) {
Path rootDirectory = cliRequest.rootDirectory;
if (rootDirectory != null) {
return rootDirectory.toString();
} else {
throw new IllegalUseOfUndefinedProperty(expression);
}
}
return null;
}
});
interpolator.addValueSource(new AbstractValueSource(false) {
@Override
public Object getValue(String expression) {
for (Properties props : properties) {
Object val = props.getProperty(expression);
if (val != null) {
return val;
}
}
return null;
}
});
return interpolator;
}

static class ExitException extends Exception {
Expand All @@ -1475,6 +1610,14 @@ static class ExitException extends Exception {
}
}

static class IllegalUseOfUndefinedProperty extends IllegalArgumentException {
final String property;

IllegalUseOfUndefinedProperty(String property) {
this.property = property;
}
}

//
// Customizations available via the CLI
//
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import java.io.File;
import java.io.PrintStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Paths;

import org.apache.commons.cli.ParseException;
import org.apache.maven.Maven;
Expand All @@ -36,6 +37,9 @@
import org.junit.function.ThrowingRunnable;
import org.mockito.InOrder;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;
Expand Down Expand Up @@ -364,6 +368,38 @@ public void testVersionStringWithoutAnsi() throws Exception {
assertEquals(MessageUtils.stripAnsiCodes(versionOut), versionOut);
}

@Test
public void testPropertiesInterpolation() throws Exception {
// Arrange
CliRequest request = new CliRequest(
new String[] {
"-Dfoo=bar",
"-DvalFound=s${foo}i",
"-DvalNotFound=s${foz}i",
"-DvalRootDirectory=${session.rootDirectory}/.mvn/foo",
"-DvalTopDirectory=${session.topDirectory}/pom.xml",
"-f",
"${session.rootDirectory}/my-child",
"prefix:3.0.0:${foo}",
"validate"
},
null);
request.rootDirectory = Paths.get("myRootDirectory");
request.topDirectory = Paths.get("myTopDirectory");

// Act
cli.cli(request);
cli.properties(request);

// Assert
assertThat(request.getUserProperties().getProperty("valFound"), is("sbari"));
assertThat(request.getUserProperties().getProperty("valNotFound"), is("s${foz}i"));
assertThat(request.getUserProperties().getProperty("valRootDirectory"), is("myRootDirectory/.mvn/foo"));
assertThat(request.getUserProperties().getProperty("valTopDirectory"), is("myTopDirectory/pom.xml"));
assertThat(request.getCommandLine().getOptionValue('f'), is("myRootDirectory/my-child"));
assertThat(request.getCommandLine().getArgs(), equalTo(new String[] {"prefix:3.0.0:bar", "validate"}));
}

class ConcurrencyCalculator implements ThrowingRunnable {

private final String value;
Expand Down

0 comments on commit 79556dd

Please sign in to comment.