diff --git a/maven-embedder/src/main/java/org/apache/maven/cli/CliRequest.java b/maven-embedder/src/main/java/org/apache/maven/cli/CliRequest.java index 15439def8dfc..4c48de3925a3 100644 --- a/maven-embedder/src/main/java/org/apache/maven/cli/CliRequest.java +++ b/maven-embedder/src/main/java/org/apache/maven/cli/CliRequest.java @@ -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; @@ -40,6 +41,10 @@ public class CliRequest { File multiModuleProjectDirectory; + Path rootDirectory; + + Path topDirectory; + boolean debug; boolean quiet; diff --git a/maven-embedder/src/main/java/org/apache/maven/cli/MavenCli.java b/maven-embedder/src/main/java/org/apache/maven/cli/MavenCli.java index fa667e9a071f..08187d152285 100644 --- a/maven-embedder/src/main/java/org/apache/maven/cli/MavenCli.java +++ b/maven-embedder/src/main/java/org/apache/maven/cli/MavenCli.java @@ -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; @@ -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; @@ -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"; @@ -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. @@ -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 values = option.getValuesList(); + for (ListIterator 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 { @@ -1405,8 +1488,8 @@ 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 @@ -1414,18 +1497,45 @@ static void populateProperties(CommandLine commandLine, Properties systemPropert // 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 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 @@ -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 { @@ -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 // diff --git a/maven-embedder/src/test/java/org/apache/maven/cli/MavenCliTest.java b/maven-embedder/src/test/java/org/apache/maven/cli/MavenCliTest.java index 384bf02f36f7..ddeceba5d6db 100644 --- a/maven-embedder/src/test/java/org/apache/maven/cli/MavenCliTest.java +++ b/maven-embedder/src/test/java/org/apache/maven/cli/MavenCliTest.java @@ -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; @@ -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; @@ -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;