diff --git a/apache-maven/src/assembly/maven/conf/maven.properties b/apache-maven/src/assembly/maven/conf/maven.properties new file mode 100644 index 000000000000..6b2403f73063 --- /dev/null +++ b/apache-maven/src/assembly/maven/conf/maven.properties @@ -0,0 +1,10 @@ +# +# Maven user properties +# + +# Required files to load +${includes} = + +# Optionally load global user properties and project user properties +${optionals} = ${user.home}/.m2/maven.properties \ + ${session.rootDirectory}/.mvn/maven.properties diff --git a/maven-embedder/pom.xml b/maven-embedder/pom.xml index e6a1d13d1559..0c823eb6c677 100644 --- a/maven-embedder/pom.xml +++ b/maven-embedder/pom.xml @@ -167,6 +167,12 @@ under the License. mockito-core test + + com.google.jimfs + jimfs + 1.3.0 + test + 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 b4219e66f0df..7057cae818ba 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 @@ -28,9 +28,10 @@ import java.io.InputStream; import java.io.PrintStream; import java.nio.charset.Charset; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; import java.util.*; import java.util.Map.Entry; import java.util.function.Consumer; @@ -62,6 +63,8 @@ import org.apache.maven.cli.logging.Slf4jConfigurationFactory; import org.apache.maven.cli.logging.Slf4jLoggerManager; import org.apache.maven.cli.logging.Slf4jStdoutLogger; +import org.apache.maven.cli.props.InterpolationHelper; +import org.apache.maven.cli.props.PropertiesLoader; import org.apache.maven.cli.transfer.*; import org.apache.maven.eventspy.internal.EventSpyDispatcher; import org.apache.maven.exception.DefaultExceptionHandler; @@ -179,6 +182,8 @@ public class MavenCli { private MessageBuilderFactory messageBuilderFactory; + private FileSystem fileSystem = FileSystems.getDefault(); + private static final Pattern NEXT_LINE = Pattern.compile("\r?\n"); public MavenCli() { @@ -328,7 +333,7 @@ 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); + Path topDirectory = fileSystem.getPath(cliRequest.workingDirectory); boolean isAltFile = false; for (String arg : cliRequest.args) { if (isAltFile) { @@ -371,7 +376,9 @@ void initialize(CliRequest cliRequest) throws ExitException { String mavenHome = System.getProperty("maven.home"); if (mavenHome != null) { - System.setProperty("maven.home", new File(mavenHome).getAbsolutePath()); + System.setProperty( + "maven.home", + getCanonicalPath(fileSystem.getPath(mavenHome)).toString()); } } @@ -1581,20 +1588,15 @@ int calculateDegreeOfConcurrency(String threadConfiguration) { // Properties handling // ---------------------------------------------------------------------- - static void populateProperties( + void populateProperties( CommandLine commandLine, Properties paths, Properties systemProperties, Properties userProperties) throws Exception { - EnvironmentUtils.addEnvVars(systemProperties); // ---------------------------------------------------------------------- - // Options that are set on the command line become system properties - // and therefore are set in the session properties. System properties - // are most dominant. + // Load environment and system properties // ---------------------------------------------------------------------- - final Properties userSpecifiedProperties = - commandLine.getOptionProperties(String.valueOf(CLIManager.SET_USER_PROPERTY)); - + EnvironmentUtils.addEnvVars(systemProperties); SystemProperties.addSystemProperties(systemProperties); // ---------------------------------------------------------------------- @@ -1610,20 +1612,48 @@ static void populateProperties( String mavenBuildVersion = CLIReportingUtils.createMavenVersionString(buildProperties); systemProperties.setProperty("maven.build.version", mavenBuildVersion); - BasicInterpolator interpolator = - createInterpolator(paths, systemProperties, userProperties, userSpecifiedProperties); - for (Map.Entry e : userSpecifiedProperties.entrySet()) { - String name = (String) e.getKey(); - String value = interpolator.interpolate((String) e.getValue()); - userProperties.setProperty(name, value); - // ---------------------------------------------------------------------- - // I'm leaving the setting of system properties here as not to break - // the SystemPropertyProfileActivator. This won't harm embedding. jvz. - // ---------------------------------------------------------------------- - if (System.getProperty(name) == null) { - System.setProperty(name, value); - } + // ---------------------------------------------------------------------- + // Options that are set on the command line become system properties + // and therefore are set in the session properties. System properties + // are most dominant. + // ---------------------------------------------------------------------- + + Properties userSpecifiedProperties = + commandLine.getOptionProperties(String.valueOf(CLIManager.SET_USER_PROPERTY)); + userProperties.putAll(userSpecifiedProperties); + + // ---------------------------------------------------------------------- + // Load config files + // ---------------------------------------------------------------------- + InterpolationHelper.SubstitutionCallback callback = or(paths::getProperty, systemProperties::getProperty); + + if (systemProperties.getProperty("maven.conf") != null) { + Path mavenConf = fileSystem.getPath(systemProperties.getProperty("maven.conf")); + + Path propertiesFile = mavenConf.resolve("maven.properties"); + PropertiesLoader.loadProperties(userProperties, propertiesFile, callback, false); } + + // ---------------------------------------------------------------------- + // I'm leaving the setting of system properties here as not to break + // the SystemPropertyProfileActivator. This won't harm embedding. jvz. + // ---------------------------------------------------------------------- + Set sys = SystemProperties.getSystemProperties().stringPropertyNames(); + userProperties.stringPropertyNames().stream() + .filter(k -> !sys.contains(k)) + .forEach(k -> System.setProperty(k, userProperties.getProperty(k))); + } + + private static InterpolationHelper.SubstitutionCallback or(InterpolationHelper.SubstitutionCallback... callbacks) { + return s -> { + for (InterpolationHelper.SubstitutionCallback cb : callbacks) { + String r = cb.getValue(s); + if (r != null) { + return r; + } + } + return null; + }; } private static BasicInterpolator createInterpolator(Properties... properties) { @@ -1689,4 +1719,8 @@ protected void customizeContainer(PlexusContainer container) {} protected ModelProcessor createModelProcessor(PlexusContainer container) throws ComponentLookupException { return container.lookup(ModelProcessor.class); } + + public void setFileSystem(FileSystem fileSystem) { + this.fileSystem = fileSystem; + } } diff --git a/maven-embedder/src/main/java/org/apache/maven/cli/props/InterpolationHelper.java b/maven-embedder/src/main/java/org/apache/maven/cli/props/InterpolationHelper.java new file mode 100644 index 000000000000..9a7258df3b41 --- /dev/null +++ b/maven-embedder/src/main/java/org/apache/maven/cli/props/InterpolationHelper.java @@ -0,0 +1,363 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.cli.props; + +import java.util.HashMap; +import java.util.Map; + +public class InterpolationHelper { + + private InterpolationHelper() {} + + private static final char ESCAPE_CHAR = '\\'; + private static final String DELIM_START = "${"; + private static final String DELIM_STOP = "}"; + private static final String MARKER = "$__"; + private static final String ENV_PREFIX = "env:"; + + /** + * Callback for substitution + */ + public interface SubstitutionCallback { + + String getValue(String key); + } + + /** + * Perform substitution on a property set + * + * @param properties the property set to perform substitution on + * @param callback Callback for substitution + */ + public static void performSubstitution(Map properties, SubstitutionCallback callback) { + performSubstitution(properties, callback, true, true, true); + } + + /** + * Perform substitution on a property set + * + * @param properties the property set to perform substitution on + * @param callback the callback to obtain substitution values + * @param substituteFromConfig If substitute from configuration + * @param substituteFromSystemProperties If substitute from system properties + * @param defaultsToEmptyString sets an empty string if a replacement value is not found, leaves intact otherwise + */ + public static void performSubstitution( + Map properties, + SubstitutionCallback callback, + boolean substituteFromConfig, + boolean substituteFromSystemProperties, + boolean defaultsToEmptyString) { + Map org = new HashMap<>(properties); + for (String name : properties.keySet()) { + properties.compute( + name, + (k, value) -> substVars( + value, + name, + null, + org, + callback, + substituteFromConfig, + substituteFromSystemProperties, + defaultsToEmptyString)); + } + } + + /** + *

+ * This method performs property variable substitution on the + * specified value. If the specified value contains the syntax + * {@code ${<prop-name>}}, where {@code <prop-name>} + * refers to either a configuration property or a system property, + * then the corresponding property value is substituted for the variable + * placeholder. Multiple variable placeholders may exist in the + * specified value as well as nested variable placeholders, which + * are substituted from inner most to outer most. Configuration + * properties override system properties. + *

+ * + * @param val The string on which to perform property substitution. + * @param currentKey The key of the property being evaluated used to + * detect cycles. + * @param cycleMap Map of variable references used to detect nested cycles. + * @param configProps Set of configuration properties. + * @return The value of the specified string after system property substitution. + * @throws IllegalArgumentException If there was a syntax error in the + * property placeholder syntax or a recursive variable reference. + **/ + public static String substVars( + String val, String currentKey, Map cycleMap, Map configProps) + throws IllegalArgumentException { + return substVars(val, currentKey, cycleMap, configProps, (SubstitutionCallback) null); + } + + /** + *

+ * This method performs property variable substitution on the + * specified value. If the specified value contains the syntax + * {@code ${<prop-name>}}, where {@code <prop-name>} + * refers to either a configuration property or a system property, + * then the corresponding property value is substituted for the variable + * placeholder. Multiple variable placeholders may exist in the + * specified value as well as nested variable placeholders, which + * are substituted from inner most to outer most. Configuration + * properties override system properties. + *

+ * + * @param val The string on which to perform property substitution. + * @param currentKey The key of the property being evaluated used to + * detect cycles. + * @param cycleMap Map of variable references used to detect nested cycles. + * @param configProps Set of configuration properties. + * @param callback the callback to obtain substitution values + * @return The value of the specified string after system property substitution. + * @throws IllegalArgumentException If there was a syntax error in the + * property placeholder syntax or a recursive variable reference. + **/ + public static String substVars( + String val, + String currentKey, + Map cycleMap, + Map configProps, + SubstitutionCallback callback) + throws IllegalArgumentException { + return substVars(val, currentKey, cycleMap, configProps, callback, true, true, false); + } + + /** + *

+ * This method performs property variable substitution on the + * specified value. If the specified value contains the syntax + * {@code ${<prop-name>}}, where {@code <prop-name>} + * refers to either a configuration property or a system property, + * then the corresponding property value is substituted for the variable + * placeholder. Multiple variable placeholders may exist in the + * specified value as well as nested variable placeholders, which + * are substituted from inner most to outer most. Configuration + * properties override system properties. + *

+ * + * @param val The string on which to perform property substitution. + * @param currentKey The key of the property being evaluated used to + * detect cycles. + * @param cycleMap Map of variable references used to detect nested cycles. + * @param configProps Set of configuration properties. + * @param callback the callback to obtain substitution values + * @param substituteFromConfig If substitute from configuration + * @param substituteFromSystemProperties If substitute from system properties + * @param defaultsToEmptyString sets an empty string if a replacement value is not found, leaves intact otherwise + * @return The value of the specified string after system property substitution. + * @throws IllegalArgumentException If there was a syntax error in the + * property placeholder syntax or a recursive variable reference. + **/ + public static String substVars( + String val, + String currentKey, + Map cycleMap, + Map configProps, + SubstitutionCallback callback, + boolean substituteFromConfig, + boolean substituteFromSystemProperties, + boolean defaultsToEmptyString) { + return unescape(doSubstVars( + val, + currentKey, + cycleMap, + configProps, + callback, + substituteFromConfig, + substituteFromSystemProperties, + defaultsToEmptyString)); + } + + private static String doSubstVars( + String val, + String currentKey, + Map cycleMap, + Map configProps, + SubstitutionCallback callback, + boolean substituteFromConfig, + boolean substituteFromSystemProperties, + boolean defaultsToEmptyString) + throws IllegalArgumentException { + if (cycleMap == null) { + cycleMap = new HashMap<>(); + } + + // Put the current key in the cycle map. + cycleMap.put(currentKey, currentKey); + + // Assume we have a value that is something like: + // "leading ${foo.${bar}} middle ${baz} trailing" + + // Find the first ending '}' variable delimiter, which + // will correspond to the first deepest nested variable + // placeholder. + int startDelim; + int stopDelim = -1; + do { + stopDelim = val.indexOf(DELIM_STOP, stopDelim + 1); + while (stopDelim > 0 && val.charAt(stopDelim - 1) == ESCAPE_CHAR) { + stopDelim = val.indexOf(DELIM_STOP, stopDelim + 1); + } + + // Find the matching starting "${" variable delimiter + // by looping until we find a start delimiter that is + // greater than the stop delimiter we have found. + startDelim = val.indexOf(DELIM_START); + while (stopDelim >= 0) { + int idx = val.indexOf(DELIM_START, startDelim + DELIM_START.length()); + if ((idx < 0) || (idx > stopDelim)) { + break; + } else if (idx < stopDelim) { + startDelim = idx; + } + } + } while (startDelim >= 0 && stopDelim >= 0 && stopDelim < startDelim + DELIM_START.length()); + + // If we do not have a start or stop delimiter, then just + // return the existing value. + if ((startDelim < 0) || (stopDelim < 0)) { + cycleMap.remove(currentKey); + return val; + } + + // At this point, we have found a variable placeholder so + // we must perform a variable substitution on it. + // Using the start and stop delimiter indices, extract + // the first, deepest nested variable placeholder. + String variable = val.substring(startDelim + DELIM_START.length(), stopDelim); + String org = variable; + + // Strip expansion modifiers + int idx1 = variable.lastIndexOf(":-"); + int idx2 = variable.lastIndexOf(":+"); + int idx = idx1 >= 0 && idx2 >= 0 ? Math.min(idx1, idx2) : idx1 >= 0 ? idx1 : idx2; + String op = null; + if (idx >= 0) { + op = variable.substring(idx); + variable = variable.substring(0, idx); + } + + // Verify that this is not a recursive variable reference. + if (cycleMap.get(variable) != null) { + throw new IllegalArgumentException("recursive variable reference: " + variable); + } + + String substValue = null; + // Get the value of the deepest nested variable placeholder. + // Try to configuration properties first. + if (substituteFromConfig && configProps != null) { + substValue = configProps.get(variable); + } + if (substValue == null) { + if (!variable.isEmpty()) { + if (callback != null) { + substValue = callback.getValue(variable); + } + if (substValue == null && substituteFromSystemProperties) { + substValue = System.getProperty(variable); + } + } + } + + if (op != null) { + if (op.startsWith(":-")) { + if (substValue == null || substValue.isEmpty()) { + substValue = op.substring(":-".length()); + } + } else if (op.startsWith(":+")) { + if (substValue != null && !substValue.isEmpty()) { + substValue = op.substring(":+".length()); + } + } else { + throw new IllegalArgumentException("Bad substitution: ${" + org + "}"); + } + } + + if (substValue == null) { + if (defaultsToEmptyString) { + substValue = ""; + } else { + // alters the original token to avoid infinite recursion + // altered tokens are reverted in substVarsPreserveUnresolved() + substValue = MARKER + "{" + variable + "}"; + } + } + + // Remove the found variable from the cycle map, since + // it may appear more than once in the value and we don't + // want such situations to appear as a recursive reference. + cycleMap.remove(variable); + + // Append the leading characters, the substituted value of + // the variable, and the trailing characters to get the new + // value. + val = val.substring(0, startDelim) + substValue + val.substring(stopDelim + DELIM_STOP.length()); + + // Now perform substitution again, since there could still + // be substitutions to make. + val = doSubstVars( + val, + currentKey, + cycleMap, + configProps, + callback, + substituteFromConfig, + substituteFromSystemProperties, + defaultsToEmptyString); + + cycleMap.remove(currentKey); + + // Return the value. + return val; + } + + public static String escape(String val) { + return val.replace("$", MARKER); + } + + private static String unescape(String val) { + val = val.replaceAll("\\" + MARKER, "\\$"); + int escape = val.indexOf(ESCAPE_CHAR); + while (escape >= 0 && escape < val.length() - 1) { + char c = val.charAt(escape + 1); + if (c == '{' || c == '}' || c == ESCAPE_CHAR) { + val = val.substring(0, escape) + val.substring(escape + 1); + } + escape = val.indexOf(ESCAPE_CHAR, escape + 1); + } + return val; + } + + public static class DefaultSubstitutionCallback implements SubstitutionCallback { + public DefaultSubstitutionCallback() {} + + public String getValue(String key) { + String value = null; + if (key.startsWith(ENV_PREFIX)) { + value = System.getenv(key.substring(ENV_PREFIX.length())); + } else { + value = System.getProperty(key); + } + return value; + } + } +} diff --git a/maven-embedder/src/main/java/org/apache/maven/cli/props/Properties.java b/maven-embedder/src/main/java/org/apache/maven/cli/props/Properties.java new file mode 100644 index 000000000000..bd3e0099d43b --- /dev/null +++ b/maven-embedder/src/main/java/org/apache/maven/cli/props/Properties.java @@ -0,0 +1,1153 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.cli.props; + +import java.io.FilterWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.LineNumberReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Reader; +import java.io.Writer; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.AbstractMap; +import java.util.AbstractSet; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +/** + * Enhancement of the standard Properties + * managing the maintain of comments, etc. + */ +public class Properties extends AbstractMap { + + /** Constant for the supported comment characters.*/ + private static final String COMMENT_CHARS = "#!"; + + /** The list of possible key/value separators */ + private static final char[] SEPARATORS = new char[] {'=', ':'}; + + /** The white space characters used as key/value separators. */ + private static final char[] WHITE_SPACE = new char[] {' ', '\t', '\f'}; + + /** + * Unless standard java props, use UTF-8 + */ + static final String DEFAULT_ENCODING = StandardCharsets.UTF_8.name(); + + /** Constant for the platform specific line separator.*/ + private static final String LINE_SEPARATOR = System.lineSeparator(); + + /** Constant for the radix of hex numbers.*/ + private static final int HEX_RADIX = 16; + + /** Constant for the length of a unicode literal.*/ + private static final int UNICODE_LEN = 4; + + private final Map storage = new LinkedHashMap(); + private final Map layout = new LinkedHashMap(); + private List header; + private List footer; + private Path location; + private InterpolationHelper.SubstitutionCallback callback; + boolean substitute = true; + boolean typed; + + public Properties() {} + + public Properties(Path location) throws IOException { + this(location, null); + } + + public Properties(Path location, InterpolationHelper.SubstitutionCallback callback) throws IOException { + this.location = location; + this.callback = callback; + if (Files.exists(location)) { + load(location); + } + } + + public Properties(boolean substitute) { + this.substitute = substitute; + } + + public Properties(Path location, boolean substitute) { + this.location = location; + this.substitute = substitute; + } + + public void load(Path location) throws IOException { + try (InputStream is = Files.newInputStream(location)) { + load(is); + } + } + + public void load(URL location) throws IOException { + try (InputStream is = location.openStream()) { + load(is); + } + } + + public void load(InputStream is) throws IOException { + load(new InputStreamReader(is, DEFAULT_ENCODING)); + } + + public void load(Reader reader) throws IOException { + loadLayout(reader, false); + } + + public void save() throws IOException { + save(this.location); + } + + public void save(Path location) throws IOException { + try (OutputStream os = Files.newOutputStream(location)) { + save(os); + } + } + + public void save(OutputStream os) throws IOException { + save(new OutputStreamWriter(os, DEFAULT_ENCODING)); + } + + public void save(Writer writer) throws IOException { + saveLayout(writer, typed); + } + + /** + * Store a properties into a output stream, preserving comments, special character, etc. + * This method is mainly to be compatible with the java.util.Properties class. + * + * @param os an output stream. + * @param comment this parameter is ignored as this Properties + * @throws IOException If storing fails + */ + public void store(OutputStream os, String comment) throws IOException { + this.save(os); + } + + /** + * Searches for the property with the specified key in this property list. + * + * @param key the property key. + * @return the value in this property list with the specified key value. + */ + public String getProperty(String key) { + return this.get(key); + } + + /** + * Searches for the property with the specified key in this property list. If the key is not found in this property + * list, the default property list, and its defaults, recursively, are then checked. The method returns the default + * value argument if the property is not found. + * + * @param key the property key. + * @param defaultValue a default value. + * @return The property value of the default value + */ + public String getProperty(String key, String defaultValue) { + if (this.get(key) != null) { + return this.get(key); + } + return defaultValue; + } + + @Override + public Set> entrySet() { + return new AbstractSet<>() { + @Override + public Iterator> iterator() { + return new Iterator<>() { + final Iterator> keyIterator = + storage.entrySet().iterator(); + + public boolean hasNext() { + return keyIterator.hasNext(); + } + + public Entry next() { + final Entry entry = keyIterator.next(); + return new Entry() { + public String getKey() { + return entry.getKey(); + } + + public String getValue() { + return entry.getValue(); + } + + public String setValue(String value) { + String old = entry.setValue(value); + if (old == null || !old.equals(value)) { + Layout l = layout.get(entry.getKey()); + if (l != null) { + l.clearValue(); + } + } + return old; + } + }; + } + + public void remove() { + keyIterator.remove(); + } + }; + } + + @Override + public int size() { + return storage.size(); + } + }; + } + + /** + * Returns an enumeration of all the keys in this property list, including distinct keys in the default property + * list if a key of the same name has not already been found from the main properties list. + * + * @return an enumeration of all the keys in this property list, including the keys in the default property list. + */ + public Enumeration propertyNames() { + return Collections.enumeration(storage.keySet()); + } + + /** + * Calls the map method put. Provided for parallelism with the getProperty method. + * Enforces use of strings for property keys and values. The value returned is the result of the map call to put. + * + * @param key the key to be placed into this property list. + * @param value the value corresponding to the key. + * @return the previous value of the specified key in this property list, or null if it did not have one. + */ + public Object setProperty(String key, String value) { + return this.put(key, value); + } + + @Override + public String put(String key, String value) { + String old = storage.put(key, value); + if (old == null || !old.equals(value)) { + Layout l = layout.get(key); + if (l != null) { + l.clearValue(); + } + } + return old; + } + + void putAllSubstituted(Map m) { + storage.putAll(m); + } + + public String put(String key, List commentLines, List valueLines) { + commentLines = new ArrayList(commentLines); + valueLines = new ArrayList(valueLines); + String escapedKey = escapeKey(key); + StringBuilder sb = new StringBuilder(); + int lastLine = valueLines.size() - 1; + if (valueLines.isEmpty()) { + valueLines.add(escapedKey + "="); + sb.append(escapedKey).append("="); + } else { + String val0 = valueLines.get(0); + String rv0 = typed ? val0 : escapeJava(val0); + if (!val0.trim().startsWith(escapedKey)) { + valueLines.set(0, escapedKey + " = " + rv0 /*+ (0 < lastLine? "\\": "")*/); + sb.append(escapedKey).append(" = ").append(rv0); + } else { + valueLines.set(0, rv0 /*+ (0 < lastLine? "\\": "")*/); + sb.append(rv0); + } + } + for (int i = 1; i < valueLines.size(); i++) { + String val = valueLines.get(i); + valueLines.set(i, typed ? val : escapeJava(val) /*+ (i < lastLine? "\\": "")*/); + while (!val.isEmpty() && Character.isWhitespace(val.charAt(0))) { + val = val.substring(1); + } + sb.append(val); + } + String[] property = PropertiesReader.parseProperty(sb.toString()); + this.layout.put(key, new Layout(commentLines, valueLines)); + return storage.put(key, property[1]); + } + + public String put(String key, List commentLines, String value) { + commentLines = new ArrayList(commentLines); + this.layout.put(key, new Layout(commentLines, null)); + return storage.put(key, value); + } + + public String put(String key, String comment, String value) { + return put(key, Collections.singletonList(comment), value); + } + + public boolean update(Map props) { + Properties properties; + if (props instanceof Properties) { + properties = (Properties) props; + } else { + properties = new Properties(); + properties.putAll(props); + } + return update(properties); + } + + public boolean update(Properties properties) { + boolean modified = false; + // Remove "removed" properties from the cfg file + for (String key : new ArrayList(this.keySet())) { + if (!properties.containsKey(key)) { + this.remove(key); + modified = true; + } + } + // Update existing keys + for (String key : properties.keySet()) { + String v = this.get(key); + List comments = properties.getComments(key); + List value = properties.getRaw(key); + if (v == null) { + this.put(key, comments, value); + modified = true; + } else if (!v.equals(properties.get(key))) { + if (comments.isEmpty()) { + comments = this.getComments(key); + } + this.put(key, comments, value); + modified = true; + } + } + return modified; + } + + public List getRaw(String key) { + if (layout.containsKey(key)) { + if (layout.get(key).getValueLines() != null) { + return new ArrayList(layout.get(key).getValueLines()); + } + } + List result = new ArrayList(); + if (storage.containsKey(key)) { + result.add(storage.get(key)); + } + return result; + } + + public List getComments(String key) { + if (layout.containsKey(key)) { + if (layout.get(key).getCommentLines() != null) { + return new ArrayList(layout.get(key).getCommentLines()); + } + } + return new ArrayList(); + } + + @Override + public String remove(Object key) { + Layout l = layout.get(key); + if (l != null) { + l.clearValue(); + } + return storage.remove(key); + } + + @Override + public void clear() { + for (Layout l : layout.values()) { + l.clearValue(); + } + storage.clear(); + } + + /** + * Return the comment header. + * + * @return the comment header + */ + public List getHeader() { + return header; + } + + /** + * Set the comment header. + * + * @param header the header to use + */ + public void setHeader(List header) { + this.header = header; + } + + /** + * Return the comment footer. + * + * @return the comment footer + */ + public List getFooter() { + return footer; + } + + /** + * Set the comment footer. + * + * @param footer the footer to use + */ + public void setFooter(List footer) { + this.footer = footer; + } + + /** + * Reads a properties file and stores its internal structure. The found + * properties will be added to the associated configuration object. + * + * @param in the reader to the properties file + * @throws IOException if an error occurs + */ + protected void loadLayout(Reader in, boolean maybeTyped) throws IOException { + PropertiesReader reader = new PropertiesReader(in, maybeTyped); + boolean hasProperty = false; + while (reader.nextProperty()) { + hasProperty = true; + storage.put(reader.getPropertyName(), reader.getPropertyValue()); + int idx = checkHeaderComment(reader.getCommentLines()); + layout.put( + reader.getPropertyName(), + new Layout( + idx < reader.getCommentLines().size() + ? new ArrayList(reader.getCommentLines() + .subList( + idx, + reader.getCommentLines().size())) + : null, + new ArrayList(reader.getValueLines()))); + } + typed = maybeTyped && reader.typed != null && reader.typed; + if (!typed) { + for (Entry e : storage.entrySet()) { + e.setValue(unescapeJava(e.getValue())); + } + } + if (hasProperty) { + footer = new ArrayList(reader.getCommentLines()); + } else { + header = new ArrayList(reader.getCommentLines()); + } + if (substitute) { + substitute(); + } + } + + public void substitute() { + substitute(callback); + } + + public void substitute(InterpolationHelper.SubstitutionCallback callback) { + if (callback == null) { + callback = new InterpolationHelper.DefaultSubstitutionCallback(); + } + InterpolationHelper.performSubstitution(storage, callback); + } + + /** + * Writes the properties file to the given writer, preserving as much of its + * structure as possible. + * + * @param out the writer + * @throws IOException if an error occurs + */ + protected void saveLayout(Writer out, boolean typed) throws IOException { + PropertiesWriter writer = new PropertiesWriter(out, typed); + if (header != null) { + for (String s : header) { + writer.writeln(s); + } + } + + for (String key : storage.keySet()) { + Layout l = layout.get(key); + if (l != null && l.getCommentLines() != null) { + for (String s : l.getCommentLines()) { + writer.writeln(s); + } + } + if (l != null && l.getValueLines() != null) { + for (int i = 0; i < l.getValueLines().size(); i++) { + String s = l.getValueLines().get(i); + if (i < l.getValueLines().size() - 1) { + writer.writeln(s + "\\"); + } else { + writer.writeln(s); + } + } + } else { + writer.writeProperty(key, storage.get(key)); + } + } + if (footer != null) { + for (String s : footer) { + writer.writeln(s); + } + } + writer.flush(); + } + + /** + * Checks if parts of the passed in comment can be used as header comment. + * This method checks whether a header comment can be defined (i.e. whether + * this is the first comment in the loaded file). If this is the case, it is + * searched for the lates blank line. This line will mark the end of the + * header comment. The return value is the index of the first line in the + * passed in list, which does not belong to the header comment. + * + * @param commentLines the comment lines + * @return the index of the next line after the header comment + */ + private int checkHeaderComment(List commentLines) { + if (getHeader() == null && layout.isEmpty()) { + // This is the first comment. Search for blank lines. + int index = commentLines.size() - 1; + while (index >= 0 && !commentLines.get(index).isEmpty()) { + index--; + } + setHeader(new ArrayList(commentLines.subList(0, index + 1))); + return index + 1; + } else { + return 0; + } + } + + /** + * Tests whether a line is a comment, i.e. whether it starts with a comment + * character. + * + * @param line the line + * @return a flag if this is a comment line + */ + static boolean isCommentLine(String line) { + String s = line.trim(); + // blank lines are also treated as comment lines + return s.isEmpty() || COMMENT_CHARS.indexOf(s.charAt(0)) >= 0; + } + + /** + *

Unescapes any Java literals found in the String to a + * Writer.

This is a slightly modified version of the + * StringEscapeUtils.unescapeJava() function in commons-lang that doesn't + * drop escaped separators (i.e '\,'). + * + * @param str the String to unescape, may be null + * @return the processed string + * @throws IllegalArgumentException if the Writer is null + */ + protected static String unescapeJava(String str) { + if (str == null) { + return null; + } + int sz = str.length(); + StringBuilder out = new StringBuilder(sz); + StringBuilder unicode = new StringBuilder(UNICODE_LEN); + boolean hadSlash = false; + boolean inUnicode = false; + for (int i = 0; i < sz; i++) { + char ch = str.charAt(i); + if (inUnicode) { + // if in unicode, then we're reading unicode + // values in somehow + unicode.append(ch); + if (unicode.length() == UNICODE_LEN) { + // unicode now contains the four hex digits + // which represents our unicode character + try { + int value = Integer.parseInt(unicode.toString(), HEX_RADIX); + out.append((char) value); + unicode.setLength(0); + inUnicode = false; + hadSlash = false; + } catch (NumberFormatException nfe) { + throw new IllegalArgumentException("Unable to parse unicode value: " + unicode, nfe); + } + } + continue; + } + + if (hadSlash) { + // handle an escaped value + hadSlash = false; + switch (ch) { + case '\\': + out.append('\\'); + break; + case '\'': + out.append('\''); + break; + case '\"': + out.append('"'); + break; + case 'r': + out.append('\r'); + break; + case 'f': + out.append('\f'); + break; + case 't': + out.append('\t'); + break; + case 'n': + out.append('\n'); + break; + case 'b': + out.append('\b'); + break; + case 'u': + // uh-oh, we're in unicode country.... + inUnicode = true; + break; + default: + out.append(ch); + break; + } + continue; + } else if (ch == '\\') { + hadSlash = true; + continue; + } + out.append(ch); + } + + if (hadSlash) { + // then we're in the weird case of a \ at the end of the + // string, let's output it anyway. + out.append('\\'); + } + + return out.toString(); + } + + /** + *

Escapes the characters in a String using Java String rules.

+ * + *

Deals correctly with quotes and control-chars (tab, backslash, cr, ff, etc.)

+ * + *

So a tab becomes the characters '\\' and + * 't'.

+ * + *

The only difference between Java strings and JavaScript strings + * is that in JavaScript, a single quote must be escaped.

+ * + *

Example:

+ *
+     * input string: He didn't say, "Stop!"
+     * output string: He didn't say, \"Stop!\"
+     * 
+ * + * + * @param str String to escape values in, may be null + * @return String with escaped values, null if null string input + */ + @SuppressWarnings("checkstyle:MagicNumber") + protected static String escapeJava(String str) { + if (str == null) { + return null; + } + int sz = str.length(); + StringBuilder out = new StringBuilder(sz * 2); + for (int i = 0; i < sz; i++) { + char ch = str.charAt(i); + // handle unicode + if (ch > 0xfff) { + out.append("\\u").append(hex(ch)); + } else if (ch > 0xff) { + out.append("\\u0").append(hex(ch)); + } else if (ch > 0x7f) { + out.append("\\u00").append(hex(ch)); + } else if (ch < 32) { + switch (ch) { + case '\b': + out.append('\\'); + out.append('b'); + break; + case '\n': + out.append('\\'); + out.append('n'); + break; + case '\t': + out.append('\\'); + out.append('t'); + break; + case '\f': + out.append('\\'); + out.append('f'); + break; + case '\r': + out.append('\\'); + out.append('r'); + break; + default: + if (ch > 0xf) { + out.append("\\u00").append(hex(ch)); + } else { + out.append("\\u000").append(hex(ch)); + } + break; + } + } else { + switch (ch) { + case '"': + out.append('\\'); + out.append('"'); + break; + case '\\': + out.append('\\'); + out.append('\\'); + break; + default: + out.append(ch); + break; + } + } + } + return out.toString(); + } + + /** + *

Returns an upper case hexadecimal String for the given + * character.

+ * + * @param ch The character to convert. + * @return An upper case hexadecimal String + */ + protected static String hex(char ch) { + return Integer.toHexString(ch).toUpperCase(Locale.ENGLISH); + } + + /** + *

Checks if the value is in the given array.

+ * + *

The method returns false if a null array is passed in.

+ * + * @param array the array to search through + * @param valueToFind the value to find + * @return true if the array contains the object + */ + public static boolean contains(char[] array, char valueToFind) { + if (array == null) { + return false; + } + for (char c : array) { + if (valueToFind == c) { + return true; + } + } + return false; + } + + /** + * Escape the separators in the key. + * + * @param key the key + * @return the escaped key + */ + private static String escapeKey(String key) { + StringBuilder newkey = new StringBuilder(); + + for (int i = 0; i < key.length(); i++) { + char c = key.charAt(i); + + if (contains(SEPARATORS, c) || contains(WHITE_SPACE, c)) { + // escape the separator + newkey.append('\\'); + newkey.append(c); + } else { + newkey.append(c); + } + } + + return newkey.toString(); + } + + /** + * This class is used to read properties lines. These lines do + * not terminate with new-line chars but rather when there is no + * backslash sign a the end of the line. This is used to + * concatenate multiple lines for readability. + */ + public static class PropertiesReader extends LineNumberReader { + /** Stores the comment lines for the currently processed property.*/ + private final List commentLines; + + /** Stores the value lines for the currently processed property.*/ + private final List valueLines; + + /** Stores the name of the last read property.*/ + private String propertyName; + + /** Stores the value of the last read property.*/ + private String propertyValue; + + private boolean maybeTyped; + + /** Stores if the properties are typed or not */ + Boolean typed; + + /** + * Creates a new instance of PropertiesReader and sets + * the underlaying reader and the list delimiter. + * + * @param reader the reader + */ + public PropertiesReader(Reader reader, boolean maybeTyped) { + super(reader); + commentLines = new ArrayList(); + valueLines = new ArrayList(); + this.maybeTyped = maybeTyped; + } + + /** + * Reads a property line. Returns null if Stream is + * at EOF. Concatenates lines ending with "\". + * Skips lines beginning with "#" or "!" and empty lines. + * The return value is a property definition (<name> + * = <value>) + * + * @return A string containing a property value or null + * + * @throws IOException in case of an I/O error + */ + public String readProperty() throws IOException { + commentLines.clear(); + valueLines.clear(); + StringBuilder buffer = new StringBuilder(); + + while (true) { + String line = readLine(); + if (line == null) { + // EOF + return null; + } + + if (isCommentLine(line)) { + commentLines.add(line); + continue; + } + + boolean combine = checkCombineLines(line); + if (combine) { + line = line.substring(0, line.length() - 1); + } + valueLines.add(line); + while (line.length() > 0 && contains(WHITE_SPACE, line.charAt(0))) { + line = line.substring(1, line.length()); + } + buffer.append(line); + if (!combine) { + break; + } + } + return buffer.toString(); + } + + /** + * Parses the next property from the input stream and stores the found + * name and value in internal fields. These fields can be obtained using + * the provided getter methods. The return value indicates whether EOF + * was reached (false) or whether further properties are + * available (true). + * + * @return a flag if further properties are available + * @throws IOException if an error occurs + */ + public boolean nextProperty() throws IOException { + String line = readProperty(); + + if (line == null) { + return false; // EOF + } + + // parse the line + String[] property = parseProperty(line); + boolean typed = false; + if (maybeTyped && property[1].length() >= 2) { + typed = property[1].matches( + "\\s*[TILFDXSCBilfdxscb]?(\\[[\\S\\s]*\\]|\\([\\S\\s]*\\)|\\{[\\S\\s]*\\}|\"[\\S\\s]*\")\\s*"); + } + if (this.typed == null) { + this.typed = typed; + } else { + this.typed = this.typed & typed; + } + propertyName = unescapeJava(property[0]); + propertyValue = property[1]; + return true; + } + + /** + * Returns the comment lines that have been read for the last property. + * + * @return the comment lines for the last property returned by + * readProperty() + */ + public List getCommentLines() { + return commentLines; + } + + /** + * Returns the value lines that have been read for the last property. + * + * @return the raw value lines for the last property returned by + * readProperty() + */ + public List getValueLines() { + return valueLines; + } + + /** + * Returns the name of the last read property. This method can be called + * after {@link #nextProperty()} was invoked and its + * return value was true. + * + * @return the name of the last read property + */ + public String getPropertyName() { + return propertyName; + } + + /** + * Returns the value of the last read property. This method can be + * called after {@link #nextProperty()} was invoked and + * its return value was true. + * + * @return the value of the last read property + */ + public String getPropertyValue() { + return propertyValue; + } + + /** + * Checks if the passed in line should be combined with the following. + * This is true, if the line ends with an odd number of backslashes. + * + * @param line the line + * @return a flag if the lines should be combined + */ + private static boolean checkCombineLines(String line) { + int bsCount = 0; + for (int idx = line.length() - 1; idx >= 0 && line.charAt(idx) == '\\'; idx--) { + bsCount++; + } + + return bsCount % 2 != 0; + } + + /** + * Parse a property line and return the key and the value in an array. + * + * @param line the line to parse + * @return an array with the property's key and value + */ + private static String[] parseProperty(String line) { + // sorry for this spaghetti code, please replace it as soon as + // possible with a regexp when the Java 1.3 requirement is dropped + + String[] result = new String[2]; + StringBuilder key = new StringBuilder(); + StringBuilder value = new StringBuilder(); + + // state of the automaton: + // 0: key parsing + // 1: antislash found while parsing the key + // 2: separator crossing + // 3: white spaces + // 4: value parsing + int state = 0; + + for (int pos = 0; pos < line.length(); pos++) { + char c = line.charAt(pos); + + switch (state) { + case 0: + if (c == '\\') { + state = 1; + } else if (contains(WHITE_SPACE, c)) { + // switch to the separator crossing state + state = 2; + } else if (contains(SEPARATORS, c)) { + // switch to the value parsing state + state = 3; + } else { + key.append(c); + } + + break; + + case 1: + if (contains(SEPARATORS, c) || contains(WHITE_SPACE, c)) { + // this is an escaped separator or white space + key.append(c); + } else { + // another escaped character, the '\' is preserved + key.append('\\'); + key.append(c); + } + + // return to the key parsing state + state = 0; + + break; + + case 2: + if (contains(WHITE_SPACE, c)) { + // do nothing, eat all white spaces + state = 2; + } else if (contains(SEPARATORS, c)) { + // switch to the value parsing state + state = 3; + } else { + // any other character indicates we encoutered the beginning of the value + value.append(c); + + // switch to the value parsing state + state = 4; + } + + break; + + case 3: + if (contains(WHITE_SPACE, c)) { + // do nothing, eat all white spaces + state = 3; + } else { + // any other character indicates we encoutered the beginning of the value + value.append(c); + + // switch to the value parsing state + state = 4; + } + + break; + + case 4: + value.append(c); + break; + + default: + throw new IllegalStateException(); + } + } + + result[0] = key.toString(); + result[1] = value.toString(); + + return result; + } + } // class PropertiesReader + + /** + * This class is used to write properties lines. + */ + public static class PropertiesWriter extends FilterWriter { + private boolean typed; + + /** + * Constructor. + * + * @param writer a Writer object providing the underlying stream + */ + public PropertiesWriter(Writer writer, boolean typed) { + super(writer); + this.typed = typed; + } + + /** + * Writes the given property and its value. + * + * @param key the property key + * @param value the property value + * @throws IOException if an error occurs + */ + public void writeProperty(String key, String value) throws IOException { + write(escapeKey(key)); + write(" = "); + write(typed ? value : escapeJava(value)); + writeln(null); + } + + /** + * Helper method for writing a line with the platform specific line + * ending. + * + * @param s the content of the line (may be null) + * @throws IOException if an error occurs + */ + public void writeln(String s) throws IOException { + if (s != null) { + write(s); + } + write(LINE_SEPARATOR); + } + } // class PropertiesWriter + + /** + * TODO + */ + protected static class Layout { + + private List commentLines; + private List valueLines; + + public Layout() {} + + public Layout(List commentLines, List valueLines) { + this.commentLines = commentLines; + this.valueLines = valueLines; + } + + public List getCommentLines() { + return commentLines; + } + + public void setCommentLines(List commentLines) { + this.commentLines = commentLines; + } + + public List getValueLines() { + return valueLines; + } + + public void setValueLines(List valueLines) { + this.valueLines = valueLines; + } + + public void clearValue() { + this.valueLines = null; + } + } // class Layout +} diff --git a/maven-embedder/src/main/java/org/apache/maven/cli/props/PropertiesLoader.java b/maven-embedder/src/main/java/org/apache/maven/cli/props/PropertiesLoader.java new file mode 100644 index 000000000000..996d2ea4b300 --- /dev/null +++ b/maven-embedder/src/main/java/org/apache/maven/cli/props/PropertiesLoader.java @@ -0,0 +1,174 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.cli.props; + +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; +import java.util.Enumeration; +import java.util.StringTokenizer; + +import static org.apache.maven.cli.props.InterpolationHelper.substVars; + +public class PropertiesLoader { + + public static final String INCLUDES_PROPERTY = "${includes}"; // mandatory includes + + public static final String OPTIONALS_PROPERTY = "${optionals}"; // optionals include + + public static final String OVERRIDE_PREFIX = + "maven.override."; // prefix that marks that system property should override defaults. + + public static void loadProperties( + java.util.Properties properties, + Path path, + InterpolationHelper.SubstitutionCallback callback, + boolean escape) + throws Exception { + Properties sp = new Properties(false); + try { + sp.load(path); + } catch (NoSuchFileException ex) { + // ignore + } + properties.forEach( + (k, v) -> sp.put(k.toString(), escape ? InterpolationHelper.escape(v.toString()) : v.toString())); + substitute(sp, callback, INCLUDES_PROPERTY); + substitute(sp, callback, OPTIONALS_PROPERTY); + loadIncludes(INCLUDES_PROPERTY, true, path, sp); + loadIncludes(OPTIONALS_PROPERTY, false, path, sp); + substitute(sp, callback); + sp.forEach(properties::setProperty); + } + + public static void substitute(Properties props, InterpolationHelper.SubstitutionCallback callback) { + for (Enumeration e = props.propertyNames(); e.hasMoreElements(); ) { + String name = (String) e.nextElement(); + String value = props.getProperty(name); + if (value == null) { + value = callback.getValue(name); + } + if (name.startsWith(OVERRIDE_PREFIX)) { + String overrideName = name.substring(OVERRIDE_PREFIX.length()); + props.put(overrideName, substVars(value, name, null, props, callback)); + } else { + props.put(name, substVars(value, name, null, props, callback)); + } + } + props.keySet().removeIf(k -> k.startsWith(OVERRIDE_PREFIX)); + } + + private static void substitute(Properties props, InterpolationHelper.SubstitutionCallback callback, String name) { + String value = props.getProperty(name); + if (value == null) { + value = callback.getValue(name); + } + if (value != null) { + props.put(name, substVars(value, name, null, props, callback)); + } + } + + private static Properties loadPropertiesFile(Path path, boolean failIfNotFound) throws Exception { + Properties configProps = new Properties(null, false); + try { + configProps.load(path); + } catch (NoSuchFileException ex) { + if (failIfNotFound) { + throw ex; + } + } catch (Exception ex) { + System.err.println("Error loading config properties from " + path); + System.err.println("Main: " + ex); + return configProps; + } + loadIncludes(INCLUDES_PROPERTY, true, path, configProps); + loadIncludes(OPTIONALS_PROPERTY, false, path, configProps); + trimValues(configProps); + return configProps; + } + + private static void loadIncludes(String propertyName, boolean mandatory, Path configProp, Properties configProps) + throws Exception { + String includes = configProps.get(propertyName); + if (includes != null) { + StringTokenizer st = new StringTokenizer(includes, "\" ", true); + if (st.countTokens() > 0) { + String location; + do { + location = nextLocation(st); + if (location != null) { + Path path = configProp.resolve(location); + Properties props = loadPropertiesFile(path, mandatory); + configProps.putAll(props); + } + } while (location != null); + } + } + configProps.remove(propertyName); + } + + private static void trimValues(Properties configProps) { + configProps.replaceAll((k, v) -> v.trim()); + } + + private static String nextLocation(StringTokenizer st) { + String retVal = null; + + if (st.countTokens() > 0) { + String tokenList = "\" "; + StringBuilder tokBuf = new StringBuilder(10); + String tok; + boolean inQuote = false; + boolean tokStarted = false; + boolean exit = false; + while ((st.hasMoreTokens()) && (!exit)) { + tok = st.nextToken(tokenList); + switch (tok) { + case "\"": + inQuote = !inQuote; + if (inQuote) { + tokenList = "\""; + } else { + tokenList = "\" "; + } + break; + case " ": + if (tokStarted) { + retVal = tokBuf.toString(); + tokStarted = false; + tokBuf = new StringBuilder(10); + exit = true; + } + break; + default: + tokStarted = true; + tokBuf.append(tok.trim()); + break; + } + } + + // Handle case where end of token stream and + // still got data + if ((!exit) && (tokStarted)) { + retVal = tokBuf.toString(); + } + } + + return retVal; + } +} 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 40e15ac38210..701da7654134 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,12 +22,16 @@ import java.io.File; import java.io.PrintStream; import java.nio.charset.StandardCharsets; +import java.nio.file.FileSystem; +import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Collections; import java.util.List; import java.util.stream.Stream; +import com.google.common.jimfs.Configuration; +import com.google.common.jimfs.Jimfs; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.CommandLineParser; import org.apache.commons.cli.DefaultParser; @@ -51,6 +55,7 @@ import org.codehaus.plexus.DefaultPlexusContainer; import org.codehaus.plexus.PlexusContainer; import org.eclipse.aether.transfer.TransferListener; +import org.hamcrest.CoreMatchers; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -578,6 +583,20 @@ public void findRootProjectWithAttribute() { @Test public void testPropertiesInterpolation() throws Exception { + FileSystem fs = Jimfs.newFileSystem(Configuration.windows()); + + Path mavenHome = fs.getPath("C:\\maven"); + Files.createDirectories(mavenHome); + Path mavenConf = mavenHome.resolve("conf"); + Files.createDirectories(mavenConf); + Path mavenUserProps = mavenConf.resolve("maven.properties"); + Files.writeString(mavenUserProps, "${optionals} = ${session.rootDirectory}/.mvn/maven.properties\n"); + Path rootDirectory = fs.getPath("C:\\myRootDirectory"); + Path topDirectory = rootDirectory.resolve("myTopDirectory"); + Path mvn = rootDirectory.resolve(".mvn"); + Files.createDirectories(mvn); + Files.writeString(mvn.resolve("maven.properties"), "fro = ${bar}z\n" + "bar = chti${java.version}\n"); + // Arrange CliRequest request = new CliRequest( new String[] { @@ -592,20 +611,28 @@ public void testPropertiesInterpolation() throws Exception { "validate" }, null); - request.rootDirectory = Paths.get("myRootDirectory"); - request.topDirectory = Paths.get("myTopDirectory"); + request.rootDirectory = rootDirectory; + request.topDirectory = topDirectory; + System.setProperty("maven.conf", mavenConf.toString()); // Act + cli.setFileSystem(fs); cli.cli(request); cli.properties(request); // Assert + assertThat(request.getUserProperties().getProperty("fro"), CoreMatchers.startsWith("chti")); 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.getUserProperties().getProperty("valRootDirectory"), is("C:\\myRootDirectory/.mvn/foo")); + assertThat( + request.getUserProperties().getProperty("valTopDirectory"), + is("C:\\myRootDirectory\\myTopDirectory/pom.xml")); + assertThat(request.getCommandLine().getOptionValue('f'), is("C:\\myRootDirectory/my-child")); assertThat(request.getCommandLine().getArgs(), equalTo(new String[] {"prefix:3.0.0:bar", "validate"})); + + Path p = fs.getPath(request.getUserProperties().getProperty("valTopDirectory")); + assertThat(p.toString(), is("C:\\myRootDirectory\\myTopDirectory\\pom.xml")); } @ParameterizedTest