michael-o commented on code in PR #1597:
URL: https://github.com/apache/maven/pull/1597#discussion_r1670646196


##########
apache-maven/src/assembly/maven/conf/settings.xml:
##########
@@ -29,14 +29,14 @@ under the License.
  |
  |                 -s /path/to/user/settings.xml
  |
- |  2. Global Level. This settings.xml file provides configuration for all 
Maven
+ |  2. System Level. This settings.xml file provides configuration for all 
Maven

Review Comment:
   I consider this a wrong as global. See 
https://cwiki.apache.org/confluence/display/MAVEN/Commandline+inheritance. It 
should be installation level.



##########
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 <code>Properties</code>
+ * managing the maintain of comments, etc.
+ */
+public class Properties extends AbstractMap<String, String> {
+
+    /** 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.*/

Review Comment:
   Unicode



##########
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 <code>Properties</code>
+ * managing the maintain of comments, etc.
+ */
+public class Properties extends AbstractMap<String, String> {
+
+    /** 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<String, String> storage = new LinkedHashMap<String, 
String>();
+    private final Map<String, Layout> layout = new LinkedHashMap<String, 
Layout>();
+    private List<String> header;
+    private List<String> 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<Entry<String, String>> entrySet() {
+        return new AbstractSet<>() {
+            @Override
+            public Iterator<Entry<String, String>> iterator() {
+                return new Iterator<>() {
+                    final Iterator<Entry<String, String>> keyIterator =
+                            storage.entrySet().iterator();
+
+                    public boolean hasNext() {
+                        return keyIterator.hasNext();
+                    }
+
+                    public Entry<String, String> next() {
+                        final Entry<String, String> entry = keyIterator.next();
+                        return new Entry<String, String>() {
+                            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<? extends String, ? extends String> m) {
+        storage.putAll(m);
+    }
+
+    public String put(String key, List<String> commentLines, List<String> 
valueLines) {
+        commentLines = new ArrayList<String>(commentLines);
+        valueLines = new ArrayList<String>(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<String> commentLines, String value) {
+        commentLines = new ArrayList<String>(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<String, String> 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<String>(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<String> comments = properties.getComments(key);
+            List<String> 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<String> getRaw(String key) {
+        if (layout.containsKey(key)) {
+            if (layout.get(key).getValueLines() != null) {
+                return new ArrayList<String>(layout.get(key).getValueLines());
+            }
+        }
+        List<String> result = new ArrayList<String>();
+        if (storage.containsKey(key)) {
+            result.add(storage.get(key));
+        }
+        return result;
+    }
+
+    public List<String> getComments(String key) {
+        if (layout.containsKey(key)) {
+            if (layout.get(key).getCommentLines() != null) {
+                return new 
ArrayList<String>(layout.get(key).getCommentLines());
+            }
+        }
+        return new ArrayList<String>();
+    }
+
+    @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<String> getHeader() {
+        return header;
+    }
+
+    /**
+     * Set the comment header.
+     *
+     * @param header the header to use
+     */
+    public void setHeader(List<String> header) {
+        this.header = header;
+    }
+
+    /**
+     * Return the comment footer.
+     *
+     * @return the comment footer
+     */
+    public List<String> getFooter() {
+        return footer;
+    }
+
+    /**
+     * Set the comment footer.
+     *
+     * @param footer the footer to use
+     */
+    public void setFooter(List<String> 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<String>(reader.getCommentLines()
+                                            .subList(
+                                                    idx,
+                                                    
reader.getCommentLines().size()))
+                                    : null,
+                            new ArrayList<String>(reader.getValueLines())));
+        }
+        typed = maybeTyped && reader.typed != null && reader.typed;
+        if (!typed) {
+            for (Entry<String, String> e : storage.entrySet()) {
+                e.setValue(unescapeJava(e.getValue()));
+            }
+        }
+        if (hasProperty) {
+            footer = new ArrayList<String>(reader.getCommentLines());
+        } else {
+            header = new ArrayList<String>(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<String> 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<String>(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;
+    }
+
+    /**
+     * <p>Unescapes any Java literals found in the <code>String</code> to a
+     * <code>Writer</code>.</p> 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 <code>String</code> to unescape, may be null
+     * @return the processed string
+     * @throws IllegalArgumentException if the Writer is <code>null</code>
+     */
+    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);

Review Comment:
   Does this require documentation?



##########
api/maven-api-core/src/main/java/org/apache/maven/api/Constants.java:
##########
@@ -0,0 +1,239 @@
+/*
+ * 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.api;
+
+import org.apache.maven.api.annotations.Config;
+
+/**
+ * Configuration constants.
+ */
+public final class Constants {
+
+    /**
+     * Maven home.
+     *
+     * @since 3.0.0
+     */
+    @Config(readOnly = true)
+    public static final String MAVEN_HOME = "maven.home";
+
+    /**
+     * Maven configuration.
+     *
+     * @since 3.0.0
+     */
+    @Config(defaultValue = "${maven.home}/conf")
+    public static final String MAVEN_CONF = "maven.conf";
+
+    /**
+     * Maven user home.
+     *
+     * @since 4.0.0
+     */
+    @Config(defaultValue = "${user.home}/.m2")
+    public static final String MAVEN_USER_HOME = "maven.user.home";
+
+    /**
+     * Maven local repository.
+     *
+     * @since 3.0.0
+     */
+    @Config(defaultValue = "${maven.user.home}/repository")
+    public static final String MAVEN_REPO_LOCAL = "maven.repo.local";
+
+    /**
+     * Maven system-wide extensions.
+     *
+     * @since 4.0.0
+     */
+    @Config(defaultValue = "${maven.conf}/extensions.xml")
+    public static final String MAVEN_SYSTEM_EXTENSIONS = 
"maven.system.extensions";
+
+    /**
+     * Maven user extensions.
+     *
+     * @since 4.0.0
+     */
+    @Config(defaultValue = "${maven.user.home}/extensions.xml")
+    public static final String MAVEN_USER_EXTENSIONS = "maven.user.extensions";
+
+    /**
+     * Maven project-wide extensions.
+     *
+     * @since 4.0.0
+     */
+    @Config(defaultValue = "${session.rootDirectory}/.mvn/extensions.xml")
+    public static final String MAVEN_PROJECT_EXTENSIONS = 
"maven.project.extensions";
+
+    /**
+     * Maven system toolchains.
+     *
+     * @since 4.0.0
+     */
+    @Config(defaultValue = "${maven.conf}/toolchains.xml")
+    public static final String MAVEN_SYSTEM_TOOLCHAINS = 
"maven.system.toolchains";
+
+    /**
+     * Maven user toolchains.
+     *
+     * @since 4.0.0
+     */
+    @Config(defaultValue = "${maven.user.home}/toolchains.xml")
+    public static final String MAVEN_USER_TOOLCHAINS = "maven.user.toolchains";
+
+    /**
+     * Extensions class path.
+     */
+    @Config
+    public static final String MAVEN_EXT_CLASS_PATH = "maven.ext.class.path";
+
+    /**
+     * Maven output color mode.
+     * Allowed values are <code>auto</code>, <code>always</code>, 
<code>never</code>.
+     *
+     * @since 4.0.0
+     */
+    @Config(defaultValue = "auto")
+    public static final String MAVEN_STYLE_COLOR_PROPERTY = 
"maven.style.color";
+
+    /**
+     * Build timestamp format.
+     *
+     * @since 3.0.0
+     */
+    @Config(source = Config.Source.MODEL, defaultValue = 
"yyyy-MM-dd'T'HH:mm:ss'Z'")
+    public static final String MAVEN_BUILD_TIMESTAMP_FORMAT = 
"maven.build.timestamp.format";
+
+    /**
+     * User controlled relocations.
+     * This property is a comma separated list of entries with the syntax 
<code>GAV&gt;GAV</code>.
+     * The first <code>GAV</code> can contain <code>*</code> for any elem (so 
<code>*:*:*</code> would mean ALL, something
+     * you don't want). The second <code>GAV</code> is either fully specified, 
or also can contain <code>*</code>,
+     * then it behaves as "ordinary relocation": the coordinate is preserved 
from relocated artifact.
+     * Finally, if right hand <code>GAV</code> is absent (line looks like 
<code>GAV&gt;</code>), the left hand matching
+     * <code>GAV</code> is banned fully (from resolving).
+     * <p>
+     * Note: the <code>&gt;</code> means project level, while 
<code>&gt;&gt;</code> means global (whole session level,
+     * so even plugins will get relocated artifacts) relocation.
+     * </p>
+     * <p>
+     * For example,
+     * <pre>maven.relocations.entries = org.foo:*:*>, \\<br/>    
org.here:*:*>org.there:*:*, \\<br/>    
javax.inject:javax.inject:1>>jakarta.inject:jakarta.inject:1.0.5</pre>
+     * means: 3 entries, ban <code>org.foo group</code> (exactly, so 
<code>org.foo.bar</code> is allowed),
+     * relocate <code>org.here</code> to <code>org.there</code> and finally 
globally relocate (see <code>&gt;&gt;</code> above)
+     * <code>javax.inject:javax.inject:1</code> to 
<code>jakarta.inject:jakarta.inject:1.0.5</code>.
+     * </p>
+     *
+     * @since 4.0.0
+     */
+    @Config
+    public static final String MAVEN_RELOCATIONS_ENTRIES = 
"maven.relocations.entries";
+
+    /**
+     * User property for version filters expression, a semicolon separated 
list of filters to apply. By default, no version
+     * filter is applied (like in Maven 3).
+     * <p>
+     * Supported filters:
+     * <ul>
+     *     <li>"h" or "h(num)" - highest version or top list of highest ones 
filter</li>
+     *     <li>"l" or "l(num)" - lowest version or bottom list of lowest ones 
filter</li>
+     *     <li>"s" - contextual snapshot filter</li>
+     *     <li>"e(G:A:V)" - predicate filter (leaves out G:A:V from range, if 
hit, V can be range)</li>
+     * </ul>
+     * Example filter expression: <code>"h(5);s;e(org.foo:bar:1)</code> will 
cause: ranges are filtered for "top 5" (instead
+     * full range), snapshots are banned if root project is not a snapshot, 
and if range for <code>org.foo:bar</code> is
+     * being processed, version 1 is omitted.
+     * </p>
+     *
+     * @since 4.0.0
+     */
+    @Config
+    public static final String MAVEN_VERSION_FILTERS = "maven.versionFilters";
+
+    /**
+     * User property for chained LRM: list of "tail" local repository paths 
(separated by comma), to be used with
+     * {@code 
org.eclipse.aether.util.repository.ChainedLocalRepositoryManager}.
+     * Default value: <code>null</code>, no chained LRM is used.
+     *
+     * @since 3.9.0
+     */
+    @Config
+    public static final String MAVEN_REPO_LOCAL_TAIL = "maven.repo.local.tail";
+
+    /**
+     * User property for reverse dependency tree. If enabled, Maven will 
record ".tracking" directory into local
+     * repository with "reverse dependency tree", essentially explaining WHY 
given artifact is present in local
+     * repository.
+     * Default: <code>false</code>, will not record anything.
+     *
+     * @since 3.9.0
+     */
+    @Config(defaultValue = "false")
+    public static final String MAVEN_REPO_LOCAL_RECORD_REVERSE_TREE = 
"maven.repo.local.recordReverseTree";
+
+    /**
+     * User property for selecting dependency manager behaviour regarding 
transitive dependencies and dependency
+     * management entries in their POMs. Maven 3 targeted full backward 
compatibility with Maven2, hence it ignored
+     * dependency management entries in transitive dependency POMs. Maven 4 
enables "transitivity" by default, hence
+     * unlike Maven2, obeys dependency management entries deep in dependency 
graph as well.
+     * <p>
+     * Default: <code>"true"</code>.
+     * </p>
+     *
+     * @since 4.0.0
+     */
+    @Config(defaultValue = "true")
+    public static final String MAVEN_RESOLVER_DEPENDENCY_MANAGER_TRANSITIVITY =
+            "maven.resolver.dependencyManagerTransitivity";
+
+    /**
+     * Resolver transport to use.
+     * Can be <code>default</code>, <code>wagon</code>, <code>apache</code>, 
<code>jdk</code> or <code>auto</code>.
+     *
+     * @since 4.0.0
+     */
+    @Config(defaultValue = "default")
+    public static final String MAVEN_RESOLVER_TRANSPORT = 
"maven.resolver.transport";
+
+    /**
+     * Plugin validation level.
+     *
+     * @since 3.9.2
+     */
+    @Config(defaultValue = "inline")
+    public static final String MAVEN_PLUGIN_VALIDATION = 
"maven.plugin.validation";
+
+    /**
+     * Plugin validation exclusions.
+     *
+     * @since 3.9.6
+     */
+    @Config
+    public static final String MAVEN_PLUGIN_VALIDATION_EXCLUDES = 
"maven.plugin.validation.excludes";
+
+    /**
+     * ProjectBuilder parallelism.
+     *
+     * @since 4.0.0
+     */
+    @Config(type = "java.lang.Integer", defaultValue = "cores/2 + 1")

Review Comment:
   How does this `expr` work?



##########
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 <code>Properties</code>
+ * managing the maintain of comments, etc.
+ */
+public class Properties extends AbstractMap<String, String> {
+
+    /** Constant for the supported comment characters.*/
+    private static final String COMMENT_CHARS = "#!";

Review Comment:
   Interesting, why is this not an array like the others below?



##########
api/maven-api-core/src/main/java/org/apache/maven/api/Constants.java:
##########
@@ -0,0 +1,239 @@
+/*
+ * 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.api;
+
+import org.apache.maven.api.annotations.Config;
+
+/**
+ * Configuration constants.
+ */
+public final class Constants {
+
+    /**
+     * Maven home.
+     *
+     * @since 3.0.0
+     */
+    @Config(readOnly = true)
+    public static final String MAVEN_HOME = "maven.home";
+
+    /**
+     * Maven configuration.
+     *
+     * @since 3.0.0
+     */
+    @Config(defaultValue = "${maven.home}/conf")
+    public static final String MAVEN_CONF = "maven.conf";
+
+    /**
+     * Maven user home.
+     *
+     * @since 4.0.0
+     */
+    @Config(defaultValue = "${user.home}/.m2")
+    public static final String MAVEN_USER_HOME = "maven.user.home";
+
+    /**
+     * Maven local repository.
+     *
+     * @since 3.0.0
+     */
+    @Config(defaultValue = "${maven.user.home}/repository")
+    public static final String MAVEN_REPO_LOCAL = "maven.repo.local";
+
+    /**
+     * Maven system-wide extensions.
+     *
+     * @since 4.0.0
+     */
+    @Config(defaultValue = "${maven.conf}/extensions.xml")
+    public static final String MAVEN_SYSTEM_EXTENSIONS = 
"maven.system.extensions";
+
+    /**
+     * Maven user extensions.
+     *
+     * @since 4.0.0
+     */
+    @Config(defaultValue = "${maven.user.home}/extensions.xml")
+    public static final String MAVEN_USER_EXTENSIONS = "maven.user.extensions";
+
+    /**
+     * Maven project-wide extensions.
+     *
+     * @since 4.0.0
+     */
+    @Config(defaultValue = "${session.rootDirectory}/.mvn/extensions.xml")
+    public static final String MAVEN_PROJECT_EXTENSIONS = 
"maven.project.extensions";
+
+    /**
+     * Maven system toolchains.
+     *
+     * @since 4.0.0
+     */
+    @Config(defaultValue = "${maven.conf}/toolchains.xml")
+    public static final String MAVEN_SYSTEM_TOOLCHAINS = 
"maven.system.toolchains";
+
+    /**
+     * Maven user toolchains.
+     *
+     * @since 4.0.0
+     */
+    @Config(defaultValue = "${maven.user.home}/toolchains.xml")
+    public static final String MAVEN_USER_TOOLCHAINS = "maven.user.toolchains";
+
+    /**
+     * Extensions class path.
+     */
+    @Config
+    public static final String MAVEN_EXT_CLASS_PATH = "maven.ext.class.path";
+
+    /**
+     * Maven output color mode.
+     * Allowed values are <code>auto</code>, <code>always</code>, 
<code>never</code>.
+     *
+     * @since 4.0.0
+     */
+    @Config(defaultValue = "auto")
+    public static final String MAVEN_STYLE_COLOR_PROPERTY = 
"maven.style.color";
+
+    /**
+     * Build timestamp format.
+     *
+     * @since 3.0.0
+     */
+    @Config(source = Config.Source.MODEL, defaultValue = 
"yyyy-MM-dd'T'HH:mm:ss'Z'")
+    public static final String MAVEN_BUILD_TIMESTAMP_FORMAT = 
"maven.build.timestamp.format";

Review Comment:
   One of the problems with this is that the timezone (offset) is missing here. 
It should be either mentioned or we should switch to `yyyy-MM-dd'T'HH:mm:ssXXX` 
which will produce the same output as with `Etc/UTC`.



##########
maven-docgen/src/main/java/org/apache/maven/tools/CollectConfiguration.java:
##########
@@ -0,0 +1,369 @@
+/*
+ * 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.tools;
+
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.io.UncheckedIOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.TreeMap;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.spi.ToolProvider;
+
+import org.apache.maven.api.annotations.Config;
+import org.apache.velocity.VelocityContext;
+import org.apache.velocity.app.VelocityEngine;
+import org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader;
+import org.jboss.forge.roaster.Roaster;
+import org.jboss.forge.roaster._shade.org.eclipse.jdt.core.dom.AST;
+import org.jboss.forge.roaster._shade.org.eclipse.jdt.core.dom.ASTNode;
+import org.jboss.forge.roaster._shade.org.eclipse.jdt.core.dom.Javadoc;
+import org.jboss.forge.roaster.model.JavaDocCapable;
+import org.jboss.forge.roaster.model.JavaDocTag;
+import org.jboss.forge.roaster.model.JavaType;
+import org.jboss.forge.roaster.model.impl.JavaDocImpl;
+import org.jboss.forge.roaster.model.source.FieldSource;
+import org.jboss.forge.roaster.model.source.JavaClassSource;
+import org.jboss.forge.roaster.model.source.JavaDocSource;
+import org.objectweb.asm.AnnotationVisitor;
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.FieldVisitor;
+import org.objectweb.asm.Opcodes;
+
+public class CollectConfiguration {
+
+    public static void main(String[] args) throws Exception {
+        try {
+            Path start = Paths.get(args.length > 0 ? args[0] : ".");
+            Path output = Paths.get(args.length > 1 ? args[1] : "output");
+
+            TreeMap<String, ConfigurationKey> discoveredKeys = new TreeMap<>();
+
+            Files.walk(start)
+                    .map(Path::toAbsolutePath)
+                    .filter(p -> p.getFileName().toString().endsWith(".class"))
+                    .filter(p -> p.toString().contains("/target/classes/"))
+                    .forEach(p -> {
+                        processClass(p, discoveredKeys);
+                    });
+
+            VelocityEngine velocityEngine = new VelocityEngine();
+            Properties properties = new Properties();
+            properties.setProperty("resource.loaders", "classpath");
+            properties.setProperty("resource.loader.classpath.class", 
ClasspathResourceLoader.class.getName());
+            velocityEngine.init(properties);
+
+            VelocityContext context = new VelocityContext();
+            context.put("keys", discoveredKeys.values());
+
+            try (BufferedWriter fileWriter = Files.newBufferedWriter(output)) {
+                velocityEngine.getTemplate("page.vm").merge(context, 
fileWriter);
+            }
+        } catch (Throwable t) {
+            t.printStackTrace();
+            throw t;
+        }
+    }
+
+    private static void processClass(Path path, Map<String, ConfigurationKey> 
discoveredKeys) {
+        try {
+            ClassReader classReader = new 
ClassReader(Files.newInputStream(path));
+            classReader.accept(
+                    new ClassVisitor(Opcodes.ASM9) {
+                        @Override
+                        public FieldVisitor visitField(
+                                int fieldAccess,
+                                String fieldName,
+                                String fieldDescriptor,
+                                String fieldSignature,
+                                Object fieldValue) {
+                            return new FieldVisitor(Opcodes.ASM9) {
+                                @Override
+                                public AnnotationVisitor visitAnnotation(
+                                        String annotationDescriptor, boolean 
annotationVisible) {
+                                    if 
(annotationDescriptor.equals("Lorg/apache/maven/api/annotations/Config;")) {
+                                        return new 
AnnotationVisitor(Opcodes.ASM9) {
+                                            Map<String, Object> values = new 
HashMap<>();
+
+                                            @Override
+                                            public void visit(String name, 
Object value) {
+                                                values.put(name, value);
+                                            }
+
+                                            @Override
+                                            public void visitEnum(String name, 
String descriptor, String value) {
+                                                values.put(name, value);
+                                            }
+
+                                            @Override
+                                            public void visitEnd() {
+                                                JavaType<?> jtype = 
parse(Paths.get(path.toString()
+                                                        
.replace("/target/classes/", "/src/main/java/")
+                                                        .replace(".class", 
".java")));
+                                                FieldSource<JavaClassSource> f 
=
+                                                        ((JavaClassSource) 
jtype).getField(fieldName);
+
+                                                int fa = fieldAccess;
+                                                String fn = fieldName;
+                                                String fd = fieldDescriptor;
+                                                String fs = fieldSignature;
+                                                Object fv = fieldValue;
+                                                String ad = 
annotationDescriptor;
+                                                boolean av = annotationVisible;
+
+                                                String fqName = null;
+                                                String desc = 
cloneJavadoc(f.getJavaDoc())
+                                                        .removeAllTags()
+                                                        .getFullText()
+                                                        .replace("*", "\\*");
+                                                String since = getSince(f);
+                                                String source =
+                                                        switch 
((values.get("source") != null
+                                                                        ? 
(String) values.get("source")
+                                                                        : 
Config.Source.USER_PROPERTIES.toString())
+                                                                
.toLowerCase()) {
+                                                            case "model" -> 
"Model properties";
+                                                            case 
"user_properties" -> "User properties";
+                                                            default -> throw 
new IllegalStateException();
+                                                        };
+                                                String type =
+                                                        switch 
((values.get("type") != null
+                                                                ? (String) 
values.get("type")
+                                                                : 
"java.lang.String")) {
+                                                            case 
"java.lang.String" -> "String";
+                                                            case 
"java.lang.Integer" -> "Integer";
+                                                            default -> throw 
new IllegalStateException();
+                                                        };
+                                                discoveredKeys.put(
+                                                        fieldValue.toString(),
+                                                        new ConfigurationKey(
+                                                                
fieldValue.toString(),
+                                                                
values.get("defaultValue") != null
+                                                                        ? 
values.get("defaultValue")
+                                                                               
 .toString()
+                                                                        : null,
+                                                                fqName,
+                                                                desc,
+                                                                since,
+                                                                source,
+                                                                type));
+                                            }
+                                        };
+                                    }
+                                    return null;
+                                }
+                            };
+                        }
+                    },
+                    0);
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    static JavaDocSource<Object> cloneJavadoc(JavaDocSource<?> javaDoc) {
+        Javadoc jd = (Javadoc) javaDoc.getInternal();
+        return new JavaDocImpl(javaDoc.getOrigin(), (Javadoc)
+                ASTNode.copySubtree(AST.newAST(jd.getAST().apiLevel()), jd));
+    }
+
+    private static String unquote(String s) {
+        return (s.startsWith("\"") && s.endsWith("\"")) ? s.substring(1, 
s.length() - 1) : s;
+    }
+
+    private static JavaType<?> parse(Path path) {
+        try {
+            return Roaster.parse(path.toFile());
+        } catch (IOException e) {
+            throw new UncheckedIOException(e);
+        }
+    }
+
+    private static boolean toBoolean(String value) {
+        return ("yes".equalsIgnoreCase(value) || 
"true".equalsIgnoreCase(value));
+    }
+
+    /**
+     * Would be record, but... Velocity have no idea what it is nor how to 
handle it.
+     */
+    public static class ConfigurationKey {
+        private final String key;
+        private final String defaultValue;
+        private final String fqName;
+        private final String description;
+        private final String since;
+        private final String configurationSource;
+        private final String configurationType;
+
+        @SuppressWarnings("checkstyle:parameternumber")
+        public ConfigurationKey(
+                String key,
+                String defaultValue,
+                String fqName,
+                String description,
+                String since,
+                String configurationSource,
+                String configurationType) {
+            this.key = key;
+            this.defaultValue = defaultValue;
+            this.fqName = fqName;
+            this.description = description;
+            this.since = since;
+            this.configurationSource = configurationSource;
+            this.configurationType = configurationType;
+        }
+
+        public String getKey() {
+            return key;
+        }
+
+        public String getDefaultValue() {
+            return defaultValue;
+        }
+
+        public String getFqName() {
+            return fqName;
+        }
+
+        public String getDescription() {
+            return description;
+        }
+
+        public String getSince() {
+            return since;
+        }
+
+        public String getConfigurationSource() {
+            return configurationSource;
+        }
+
+        public String getConfigurationType() {
+            return configurationType;
+        }
+    }
+
+    private static String nvl(String string, String def) {
+        return string == null ? def : string;
+    }
+
+    private static boolean hasConfigurationSource(JavaDocCapable<?> 
javaDocCapable) {
+        return getTag(javaDocCapable, "@configurationSource") != null;
+    }
+
+    private static String getConfigurationType(JavaDocCapable<?> 
javaDocCapable) {
+        String type = getTag(javaDocCapable, "@configurationType");
+        if (type != null) {
+            String linkPrefix = "{@link ";
+            String linkSuffix = "}";
+            if (type.startsWith(linkPrefix) && type.endsWith(linkSuffix)) {
+                type = type.substring(linkPrefix.length(), type.length() - 
linkSuffix.length());
+            }
+            String javaLangPackage = "java.lang.";
+            if (type.startsWith(javaLangPackage)) {
+                type = type.substring(javaLangPackage.length());
+            }
+        }
+        return nvl(type, "n/a");

Review Comment:
   "N/A"



##########
api/maven-api-core/src/main/java/org/apache/maven/api/Constants.java:
##########
@@ -0,0 +1,239 @@
+/*
+ * 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.api;
+
+import org.apache.maven.api.annotations.Config;
+
+/**
+ * Configuration constants.
+ */
+public final class Constants {
+
+    /**
+     * Maven home.
+     *
+     * @since 3.0.0
+     */
+    @Config(readOnly = true)
+    public static final String MAVEN_HOME = "maven.home";
+
+    /**
+     * Maven configuration.
+     *
+     * @since 3.0.0
+     */
+    @Config(defaultValue = "${maven.home}/conf")
+    public static final String MAVEN_CONF = "maven.conf";
+
+    /**
+     * Maven user home.
+     *
+     * @since 4.0.0
+     */
+    @Config(defaultValue = "${user.home}/.m2")
+    public static final String MAVEN_USER_HOME = "maven.user.home";
+
+    /**
+     * Maven local repository.
+     *
+     * @since 3.0.0
+     */
+    @Config(defaultValue = "${maven.user.home}/repository")
+    public static final String MAVEN_REPO_LOCAL = "maven.repo.local";
+
+    /**
+     * Maven system-wide extensions.
+     *
+     * @since 4.0.0
+     */
+    @Config(defaultValue = "${maven.conf}/extensions.xml")
+    public static final String MAVEN_SYSTEM_EXTENSIONS = 
"maven.system.extensions";
+
+    /**
+     * Maven user extensions.
+     *
+     * @since 4.0.0
+     */
+    @Config(defaultValue = "${maven.user.home}/extensions.xml")
+    public static final String MAVEN_USER_EXTENSIONS = "maven.user.extensions";
+
+    /**
+     * Maven project-wide extensions.
+     *
+     * @since 4.0.0
+     */
+    @Config(defaultValue = "${session.rootDirectory}/.mvn/extensions.xml")
+    public static final String MAVEN_PROJECT_EXTENSIONS = 
"maven.project.extensions";
+
+    /**
+     * Maven system toolchains.
+     *
+     * @since 4.0.0
+     */
+    @Config(defaultValue = "${maven.conf}/toolchains.xml")
+    public static final String MAVEN_SYSTEM_TOOLCHAINS = 
"maven.system.toolchains";
+
+    /**
+     * Maven user toolchains.
+     *
+     * @since 4.0.0
+     */
+    @Config(defaultValue = "${maven.user.home}/toolchains.xml")
+    public static final String MAVEN_USER_TOOLCHAINS = "maven.user.toolchains";
+
+    /**
+     * Extensions class path.
+     */
+    @Config
+    public static final String MAVEN_EXT_CLASS_PATH = "maven.ext.class.path";
+
+    /**
+     * Maven output color mode.
+     * Allowed values are <code>auto</code>, <code>always</code>, 
<code>never</code>.
+     *
+     * @since 4.0.0
+     */
+    @Config(defaultValue = "auto")
+    public static final String MAVEN_STYLE_COLOR_PROPERTY = 
"maven.style.color";
+
+    /**
+     * Build timestamp format.
+     *
+     * @since 3.0.0
+     */
+    @Config(source = Config.Source.MODEL, defaultValue = 
"yyyy-MM-dd'T'HH:mm:ss'Z'")
+    public static final String MAVEN_BUILD_TIMESTAMP_FORMAT = 
"maven.build.timestamp.format";
+
+    /**
+     * User controlled relocations.
+     * This property is a comma separated list of entries with the syntax 
<code>GAV&gt;GAV</code>.
+     * The first <code>GAV</code> can contain <code>*</code> for any elem (so 
<code>*:*:*</code> would mean ALL, something
+     * you don't want). The second <code>GAV</code> is either fully specified, 
or also can contain <code>*</code>,
+     * then it behaves as "ordinary relocation": the coordinate is preserved 
from relocated artifact.
+     * Finally, if right hand <code>GAV</code> is absent (line looks like 
<code>GAV&gt;</code>), the left hand matching
+     * <code>GAV</code> is banned fully (from resolving).
+     * <p>
+     * Note: the <code>&gt;</code> means project level, while 
<code>&gt;&gt;</code> means global (whole session level,
+     * so even plugins will get relocated artifacts) relocation.
+     * </p>
+     * <p>
+     * For example,
+     * <pre>maven.relocations.entries = org.foo:*:*>, \\<br/>    
org.here:*:*>org.there:*:*, \\<br/>    
javax.inject:javax.inject:1>>jakarta.inject:jakarta.inject:1.0.5</pre>
+     * means: 3 entries, ban <code>org.foo group</code> (exactly, so 
<code>org.foo.bar</code> is allowed),
+     * relocate <code>org.here</code> to <code>org.there</code> and finally 
globally relocate (see <code>&gt;&gt;</code> above)
+     * <code>javax.inject:javax.inject:1</code> to 
<code>jakarta.inject:jakarta.inject:1.0.5</code>.
+     * </p>
+     *
+     * @since 4.0.0
+     */
+    @Config
+    public static final String MAVEN_RELOCATIONS_ENTRIES = 
"maven.relocations.entries";
+
+    /**
+     * User property for version filters expression, a semicolon separated 
list of filters to apply. By default, no version
+     * filter is applied (like in Maven 3).
+     * <p>
+     * Supported filters:
+     * <ul>
+     *     <li>"h" or "h(num)" - highest version or top list of highest ones 
filter</li>
+     *     <li>"l" or "l(num)" - lowest version or bottom list of lowest ones 
filter</li>
+     *     <li>"s" - contextual snapshot filter</li>
+     *     <li>"e(G:A:V)" - predicate filter (leaves out G:A:V from range, if 
hit, V can be range)</li>
+     * </ul>
+     * Example filter expression: <code>"h(5);s;e(org.foo:bar:1)</code> will 
cause: ranges are filtered for "top 5" (instead
+     * full range), snapshots are banned if root project is not a snapshot, 
and if range for <code>org.foo:bar</code> is
+     * being processed, version 1 is omitted.
+     * </p>
+     *
+     * @since 4.0.0
+     */
+    @Config
+    public static final String MAVEN_VERSION_FILTERS = "maven.versionFilters";

Review Comment:
   This is weird. Here we have `versionFilters`, but above 
`relocations.entries`. Inconsistent, no?



##########
maven-docgen/src/main/java/org/apache/maven/tools/CollectConfiguration.java:
##########
@@ -0,0 +1,369 @@
+/*
+ * 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.tools;
+
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.io.UncheckedIOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.TreeMap;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.spi.ToolProvider;
+
+import org.apache.maven.api.annotations.Config;
+import org.apache.velocity.VelocityContext;
+import org.apache.velocity.app.VelocityEngine;
+import org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader;
+import org.jboss.forge.roaster.Roaster;
+import org.jboss.forge.roaster._shade.org.eclipse.jdt.core.dom.AST;
+import org.jboss.forge.roaster._shade.org.eclipse.jdt.core.dom.ASTNode;
+import org.jboss.forge.roaster._shade.org.eclipse.jdt.core.dom.Javadoc;
+import org.jboss.forge.roaster.model.JavaDocCapable;
+import org.jboss.forge.roaster.model.JavaDocTag;
+import org.jboss.forge.roaster.model.JavaType;
+import org.jboss.forge.roaster.model.impl.JavaDocImpl;
+import org.jboss.forge.roaster.model.source.FieldSource;
+import org.jboss.forge.roaster.model.source.JavaClassSource;
+import org.jboss.forge.roaster.model.source.JavaDocSource;
+import org.objectweb.asm.AnnotationVisitor;
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.FieldVisitor;
+import org.objectweb.asm.Opcodes;
+
+public class CollectConfiguration {
+
+    public static void main(String[] args) throws Exception {
+        try {
+            Path start = Paths.get(args.length > 0 ? args[0] : ".");
+            Path output = Paths.get(args.length > 1 ? args[1] : "output");
+
+            TreeMap<String, ConfigurationKey> discoveredKeys = new TreeMap<>();
+
+            Files.walk(start)
+                    .map(Path::toAbsolutePath)
+                    .filter(p -> p.getFileName().toString().endsWith(".class"))
+                    .filter(p -> p.toString().contains("/target/classes/"))
+                    .forEach(p -> {
+                        processClass(p, discoveredKeys);
+                    });
+
+            VelocityEngine velocityEngine = new VelocityEngine();
+            Properties properties = new Properties();
+            properties.setProperty("resource.loaders", "classpath");
+            properties.setProperty("resource.loader.classpath.class", 
ClasspathResourceLoader.class.getName());
+            velocityEngine.init(properties);
+
+            VelocityContext context = new VelocityContext();
+            context.put("keys", discoveredKeys.values());
+
+            try (BufferedWriter fileWriter = Files.newBufferedWriter(output)) {
+                velocityEngine.getTemplate("page.vm").merge(context, 
fileWriter);
+            }
+        } catch (Throwable t) {
+            t.printStackTrace();
+            throw t;
+        }
+    }
+
+    private static void processClass(Path path, Map<String, ConfigurationKey> 
discoveredKeys) {
+        try {
+            ClassReader classReader = new 
ClassReader(Files.newInputStream(path));
+            classReader.accept(
+                    new ClassVisitor(Opcodes.ASM9) {
+                        @Override
+                        public FieldVisitor visitField(
+                                int fieldAccess,
+                                String fieldName,
+                                String fieldDescriptor,
+                                String fieldSignature,
+                                Object fieldValue) {
+                            return new FieldVisitor(Opcodes.ASM9) {
+                                @Override
+                                public AnnotationVisitor visitAnnotation(
+                                        String annotationDescriptor, boolean 
annotationVisible) {
+                                    if 
(annotationDescriptor.equals("Lorg/apache/maven/api/annotations/Config;")) {
+                                        return new 
AnnotationVisitor(Opcodes.ASM9) {
+                                            Map<String, Object> values = new 
HashMap<>();
+
+                                            @Override
+                                            public void visit(String name, 
Object value) {
+                                                values.put(name, value);
+                                            }
+
+                                            @Override
+                                            public void visitEnum(String name, 
String descriptor, String value) {
+                                                values.put(name, value);
+                                            }
+
+                                            @Override
+                                            public void visitEnd() {
+                                                JavaType<?> jtype = 
parse(Paths.get(path.toString()
+                                                        
.replace("/target/classes/", "/src/main/java/")
+                                                        .replace(".class", 
".java")));
+                                                FieldSource<JavaClassSource> f 
=
+                                                        ((JavaClassSource) 
jtype).getField(fieldName);
+
+                                                int fa = fieldAccess;
+                                                String fn = fieldName;
+                                                String fd = fieldDescriptor;
+                                                String fs = fieldSignature;
+                                                Object fv = fieldValue;
+                                                String ad = 
annotationDescriptor;
+                                                boolean av = annotationVisible;
+
+                                                String fqName = null;
+                                                String desc = 
cloneJavadoc(f.getJavaDoc())
+                                                        .removeAllTags()
+                                                        .getFullText()
+                                                        .replace("*", "\\*");
+                                                String since = getSince(f);
+                                                String source =
+                                                        switch 
((values.get("source") != null
+                                                                        ? 
(String) values.get("source")
+                                                                        : 
Config.Source.USER_PROPERTIES.toString())
+                                                                
.toLowerCase()) {
+                                                            case "model" -> 
"Model properties";
+                                                            case 
"user_properties" -> "User properties";
+                                                            default -> throw 
new IllegalStateException();
+                                                        };
+                                                String type =
+                                                        switch 
((values.get("type") != null
+                                                                ? (String) 
values.get("type")
+                                                                : 
"java.lang.String")) {
+                                                            case 
"java.lang.String" -> "String";
+                                                            case 
"java.lang.Integer" -> "Integer";
+                                                            default -> throw 
new IllegalStateException();
+                                                        };
+                                                discoveredKeys.put(
+                                                        fieldValue.toString(),
+                                                        new ConfigurationKey(
+                                                                
fieldValue.toString(),
+                                                                
values.get("defaultValue") != null
+                                                                        ? 
values.get("defaultValue")
+                                                                               
 .toString()
+                                                                        : null,
+                                                                fqName,
+                                                                desc,
+                                                                since,
+                                                                source,
+                                                                type));
+                                            }
+                                        };
+                                    }
+                                    return null;
+                                }
+                            };
+                        }
+                    },
+                    0);
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    static JavaDocSource<Object> cloneJavadoc(JavaDocSource<?> javaDoc) {
+        Javadoc jd = (Javadoc) javaDoc.getInternal();
+        return new JavaDocImpl(javaDoc.getOrigin(), (Javadoc)
+                ASTNode.copySubtree(AST.newAST(jd.getAST().apiLevel()), jd));
+    }
+
+    private static String unquote(String s) {
+        return (s.startsWith("\"") && s.endsWith("\"")) ? s.substring(1, 
s.length() - 1) : s;
+    }
+
+    private static JavaType<?> parse(Path path) {
+        try {
+            return Roaster.parse(path.toFile());
+        } catch (IOException e) {
+            throw new UncheckedIOException(e);
+        }
+    }
+
+    private static boolean toBoolean(String value) {
+        return ("yes".equalsIgnoreCase(value) || 
"true".equalsIgnoreCase(value));
+    }
+
+    /**
+     * Would be record, but... Velocity have no idea what it is nor how to 
handle it.
+     */
+    public static class ConfigurationKey {
+        private final String key;
+        private final String defaultValue;
+        private final String fqName;
+        private final String description;
+        private final String since;
+        private final String configurationSource;
+        private final String configurationType;
+
+        @SuppressWarnings("checkstyle:parameternumber")
+        public ConfigurationKey(
+                String key,
+                String defaultValue,
+                String fqName,
+                String description,
+                String since,
+                String configurationSource,
+                String configurationType) {
+            this.key = key;
+            this.defaultValue = defaultValue;
+            this.fqName = fqName;
+            this.description = description;
+            this.since = since;
+            this.configurationSource = configurationSource;
+            this.configurationType = configurationType;
+        }
+
+        public String getKey() {
+            return key;
+        }
+
+        public String getDefaultValue() {
+            return defaultValue;
+        }
+
+        public String getFqName() {
+            return fqName;
+        }
+
+        public String getDescription() {
+            return description;
+        }
+
+        public String getSince() {
+            return since;
+        }
+
+        public String getConfigurationSource() {
+            return configurationSource;
+        }
+
+        public String getConfigurationType() {
+            return configurationType;
+        }
+    }
+
+    private static String nvl(String string, String def) {
+        return string == null ? def : string;
+    }
+
+    private static boolean hasConfigurationSource(JavaDocCapable<?> 
javaDocCapable) {
+        return getTag(javaDocCapable, "@configurationSource") != null;
+    }
+
+    private static String getConfigurationType(JavaDocCapable<?> 
javaDocCapable) {
+        String type = getTag(javaDocCapable, "@configurationType");
+        if (type != null) {
+            String linkPrefix = "{@link ";
+            String linkSuffix = "}";
+            if (type.startsWith(linkPrefix) && type.endsWith(linkSuffix)) {
+                type = type.substring(linkPrefix.length(), type.length() - 
linkSuffix.length());
+            }
+            String javaLangPackage = "java.lang.";
+            if (type.startsWith(javaLangPackage)) {
+                type = type.substring(javaLangPackage.length());
+            }
+        }
+        return nvl(type, "n/a");
+    }
+
+    private static String getConfigurationSource(JavaDocCapable<?> 
javaDocCapable) {
+        String source = getTag(javaDocCapable, "@configurationSource");
+        if ("{@link 
RepositorySystemSession#getConfigProperties()}".equals(source)) {
+            return "Session Configuration";
+        } else if ("{@link System#getProperty(String,String)}".equals(source)) 
{
+            return "Java System Properties";
+        } else if ("{@link 
org.apache.maven.api.model.Model#getProperties()}".equals(source)) {
+            return "Model Properties";
+        } else if ("{@link Session#getUserProperties()}".equals(source)) {
+            return "Session Properties";
+        } else {
+            return source;
+        }
+    }
+
+    private static String getSince(JavaDocCapable<?> javaDocCapable) {
+        List<JavaDocTag> tags;
+        if (javaDocCapable != null) {
+            if (javaDocCapable instanceof FieldSource<?> fieldSource) {
+                tags = fieldSource.getJavaDoc().getTags("@since");
+                if (tags.isEmpty()) {
+                    return getSince(fieldSource.getOrigin());
+                } else {
+                    return tags.get(0).getValue();
+                }
+            } else if (javaDocCapable instanceof JavaClassSource classSource) {
+                tags = classSource.getJavaDoc().getTags("@since");
+                if (!tags.isEmpty()) {
+                    return tags.get(0).getValue();
+                }
+            }
+        }
+        return "";
+    }
+
+    private static String getTag(JavaDocCapable<?> javaDocCapable, String 
tagName) {
+        List<JavaDocTag> tags;
+        if (javaDocCapable != null) {
+            if (javaDocCapable instanceof FieldSource<?> fieldSource) {
+                tags = fieldSource.getJavaDoc().getTags(tagName);
+                if (tags.isEmpty()) {
+                    return getTag(fieldSource.getOrigin(), tagName);
+                } else {
+                    return tags.get(0).getValue();
+                }
+            }
+        }
+        return null;
+    }
+
+    private static final Pattern CONSTANT_PATTERN = Pattern.compile(".*static 
final.* ([A-Z_]+) = (.*);");
+
+    private static final ToolProvider JAVAP = 
ToolProvider.findFirst("javap").orElseThrow();

Review Comment:
   Is `javap` always expected to exist?



##########
src/site/markdown/configuration.md:
##########
@@ -0,0 +1,49 @@
+
+# Configuration Options
+<!--
+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.
+-->
+
+
+
+
+
+| No | Key | Type | Description | Default Value | Since | Source |
+| --- | --- | --- | --- | --- | --- | --- |
+| 1. | `maven.build.timestamp.format` | `String` | Build timestamp format. |  
`yyyy-MM-dd'T'HH:mm:ss'Z'`  | 3.0.0 | Model properties |

Review Comment:
   I think that the desc should contain the time zone information.



##########
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 <code>Properties</code>
+ * managing the maintain of comments, etc.
+ */
+public class Properties extends AbstractMap<String, String> {

Review Comment:
   Maven this class should be called `MavenProperties` to avoid confusion?



##########
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 <code>Properties</code>
+ * managing the maintain of comments, etc.
+ */
+public class Properties extends AbstractMap<String, String> {
+
+    /** 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<String, String> storage = new LinkedHashMap<String, 
String>();
+    private final Map<String, Layout> layout = new LinkedHashMap<String, 
Layout>();
+    private List<String> header;
+    private List<String> 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<Entry<String, String>> entrySet() {
+        return new AbstractSet<>() {
+            @Override
+            public Iterator<Entry<String, String>> iterator() {
+                return new Iterator<>() {
+                    final Iterator<Entry<String, String>> keyIterator =
+                            storage.entrySet().iterator();
+
+                    public boolean hasNext() {
+                        return keyIterator.hasNext();
+                    }
+
+                    public Entry<String, String> next() {
+                        final Entry<String, String> entry = keyIterator.next();
+                        return new Entry<String, String>() {
+                            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<? extends String, ? extends String> m) {
+        storage.putAll(m);
+    }
+
+    public String put(String key, List<String> commentLines, List<String> 
valueLines) {
+        commentLines = new ArrayList<String>(commentLines);
+        valueLines = new ArrayList<String>(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<String> commentLines, String value) {
+        commentLines = new ArrayList<String>(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<String, String> 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<String>(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<String> comments = properties.getComments(key);
+            List<String> 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<String> getRaw(String key) {
+        if (layout.containsKey(key)) {
+            if (layout.get(key).getValueLines() != null) {
+                return new ArrayList<String>(layout.get(key).getValueLines());
+            }
+        }
+        List<String> result = new ArrayList<String>();
+        if (storage.containsKey(key)) {
+            result.add(storage.get(key));
+        }
+        return result;
+    }
+
+    public List<String> getComments(String key) {
+        if (layout.containsKey(key)) {
+            if (layout.get(key).getCommentLines() != null) {
+                return new 
ArrayList<String>(layout.get(key).getCommentLines());
+            }
+        }
+        return new ArrayList<String>();
+    }
+
+    @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<String> getHeader() {
+        return header;
+    }
+
+    /**
+     * Set the comment header.
+     *
+     * @param header the header to use
+     */
+    public void setHeader(List<String> header) {
+        this.header = header;
+    }
+
+    /**
+     * Return the comment footer.
+     *
+     * @return the comment footer
+     */
+    public List<String> getFooter() {
+        return footer;
+    }
+
+    /**
+     * Set the comment footer.
+     *
+     * @param footer the footer to use
+     */
+    public void setFooter(List<String> 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<String>(reader.getCommentLines()
+                                            .subList(
+                                                    idx,
+                                                    
reader.getCommentLines().size()))
+                                    : null,
+                            new ArrayList<String>(reader.getValueLines())));
+        }
+        typed = maybeTyped && reader.typed != null && reader.typed;
+        if (!typed) {
+            for (Entry<String, String> e : storage.entrySet()) {
+                e.setValue(unescapeJava(e.getValue()));
+            }
+        }
+        if (hasProperty) {
+            footer = new ArrayList<String>(reader.getCommentLines());
+        } else {
+            header = new ArrayList<String>(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<String> 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<String>(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;
+    }
+
+    /**
+     * <p>Unescapes any Java literals found in the <code>String</code> to a
+     * <code>Writer</code>.</p> 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 <code>String</code> to unescape, may be null
+     * @return the processed string
+     * @throws IllegalArgumentException if the Writer is <code>null</code>

Review Comment:
   Which writer?



##########
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

Review Comment:
   Maybe we can make optionals more succint with `${includes?}` like we have in 
here: 
https://maven.apache.org/plugins/maven-assembly-plugin/faq.html#dashClassifier



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscr...@maven.apache.org

For queries about this service, please contact Infrastructure at:
us...@infra.apache.org

Reply via email to