This is an automated email from the ASF dual-hosted git repository.

gnodet pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/maven.git


The following commit(s) were added to refs/heads/master by this push:
     new 2db7c85b6 [MNG-7038] Introduce public properties to point to the root 
and top directories of (multi-module) project (#1061)
2db7c85b6 is described below

commit 2db7c85b6494682363c22180f07010813f25b4ed
Author: Guillaume Nodet <[email protected]>
AuthorDate: Thu Apr 20 12:58:12 2023 +0200

    [MNG-7038] Introduce public properties to point to the root and top 
directories of (multi-module) project (#1061)
    
    This commit introduces three properties:
    
     * project.rootDirectory: the project's directory or parent directory 
containing a .mvn subdirectory or a pom.xml flagged with the root="true" 
attribute. If no such directory can be found, accessing the rootDirectory 
property will throw an IllegalStateException.
    
     * session.topDirectory : the directory of the topmost project being built, 
usually the current directory or the directory pointed at by the -f/--file 
command line argument. The topDirectory is similar to the 
executionRootDirectory property available on the session, but renamed to make 
it coherent with the new rootDirectory and to avoid using root in its name. The 
topDirectory property is computed by the CLI as the directory pointed at by the 
-f/--file command line argument, or the cu [...]
    
     * session.rootDirectory : the rootDirectory for the topDirectory project.
    
    The topDirectory and rootDirectory properties are made available on the 
MavenSession / Session and deprecate the executionRootDirectory and 
multiModuleProjectDirectory properties. The rootDirectory should never change 
for a given project and is thus made available for profile activation and model 
interpolation (without the project. prefix, similar to basedir). The goal is 
also to make the rootDirectory property also available during command line 
arguments interpolation.
    
    A root boolean attribute is also added to the model to indicate that the 
project is the root project. This attribute is only supported if the 
buildconsumer feature is active and removed before the pom is installed or 
deployed. It can be used as an alternative mechanism to the .mvn directory.
---
 .../main/java/org/apache/maven/api/Project.java    |  37 +++++++
 .../main/java/org/apache/maven/api/Session.java    |  29 ++++-
 api/maven-api-model/src/main/mdo/maven.mdo         |  13 +++
 .../apache/maven/project/TestProjectBuilder.java   |   7 +-
 .../execution/DefaultMavenExecutionRequest.java    |  33 ++++++
 .../maven/execution/MavenExecutionRequest.java     |  47 ++++++++
 .../org/apache/maven/execution/MavenSession.java   |  21 ++++
 .../apache/maven/internal/impl/DefaultProject.java |  16 +++
 .../apache/maven/internal/impl/DefaultSession.java |  13 ++-
 .../ConsumerPomArtifactTransformer.java            |   3 +
 .../maven/project/DefaultProjectBuilder.java       |  14 ++-
 .../org/apache/maven/project/MavenProject.java     |  20 ++++
 .../maven/internal/impl/DefaultSessionTest.java    |  62 +++++++++++
 .../PluginParameterExpressionEvaluatorTest.java    |  32 ++++++
 ... PluginParameterExpressionEvaluatorV4Test.java} | 119 +++++++++++++++------
 .../org/apache/maven/cli/CLIReportingUtils.java    |   7 ++
 .../main/java/org/apache/maven/cli/CliRequest.java |   5 +
 .../main/java/org/apache/maven/cli/MavenCli.java   |  54 ++++++++++
 .../java/org/apache/maven/cli/MavenCliTest.java    |   9 ++
 .../src/test/projects/root-attribute/child/pom.xml |   3 +
 .../src/test/projects/root-attribute/pom.xml       |   3 +
 .../model/building/DefaultModelBuilderFactory.java |  18 +++-
 .../AbstractStringBasedModelInterpolator.java      |  22 +++-
 .../StringVisitorModelInterpolator.java            |   6 +-
 .../ProfileActivationFilePathInterpolator.java     |  19 +++-
 .../maven/model/root/DefaultRootLocator.java       |  59 ++++++++++
 .../org/apache/maven/model/root/RootLocator.java   |  69 ++++++++++++
 .../org.apache.maven.model.root.RootLocator        |   1 +
 maven-model-builder/src/site/apt/index.apt         |  16 +--
 .../AbstractModelInterpolatorTest.java             |  47 +++++++-
 .../StringVisitorModelInterpolatorTest.java        |   2 +-
 .../activation/FileProfileActivatorTest.java       |  27 ++++-
 .../RawToConsumerPomXMLFilterFactory.java          |   2 +
 ...PomXMLFilterFactory.java => RootXMLFilter.java} |  41 +++----
 .../model/transform/pull/BufferingParser.java      |  10 ++
 src/mdo/reader-ex.vm                               |   2 +-
 src/mdo/reader-modified.vm                         |   2 +-
 src/mdo/reader.vm                                  |  14 +--
 src/mdo/writer-ex.vm                               |   9 +-
 src/mdo/writer.vm                                  |   9 +-
 40 files changed, 833 insertions(+), 89 deletions(-)

diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/Project.java 
b/api/maven-api-core/src/main/java/org/apache/maven/api/Project.java
index 7d80f3ae4..256fe9479 100644
--- a/api/maven-api-core/src/main/java/org/apache/maven/api/Project.java
+++ b/api/maven-api-core/src/main/java/org/apache/maven/api/Project.java
@@ -86,8 +86,45 @@ public interface Project {
         return getModel().getId();
     }
 
+    /**
+     * @deprecated use {@link #isTopProject()} instead
+     */
+    @Deprecated
     boolean isExecutionRoot();
 
+    /**
+     * Returns a boolean indicating if the project is the top level project for
+     * this reactor build.  The top level project may be different from the
+     * {@code rootDirectory}, especially if a subtree of the project is being
+     * built, either because Maven has been launched in a subdirectory or using
+     * a {@code -f} option.
+     *
+     * @return {@code true} if the project is the top level project for this 
build
+     */
+    boolean isTopProject();
+
+    /**
+     * Returns a boolean indicating if the project is a root project,
+     * meaning that the {@link #getRootDirectory()} and {@link #getBasedir()}
+     * points to the same directory, and that either {@link Model#isRoot()}
+     * is {@code true} or that {@code basedir} contains a {@code .mvn} child
+     * directory.
+     *
+     * @return {@code true} if the project is the root project
+     * @see Model#isRoot()
+     */
+    boolean isRootProject();
+
+    /**
+     * Gets the root directory of the project, which is the parent directory
+     * containing the {@code .mvn} directory or flagged with {@code 
root="true"}.
+     *
+     * @throws IllegalStateException if the root directory could not be found
+     * @see Session#getRootDirectory()
+     */
+    @Nonnull
+    Path getRootDirectory();
+
     @Nonnull
     Optional<Project> getParent();
 
diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/Session.java 
b/api/maven-api-core/src/main/java/org/apache/maven/api/Session.java
index 5616c3dd9..32baea23e 100644
--- a/api/maven-api-core/src/main/java/org/apache/maven/api/Session.java
+++ b/api/maven-api-core/src/main/java/org/apache/maven/api/Session.java
@@ -84,9 +84,34 @@ public interface Session {
     @Nonnull
     Instant getStartTime();
 
+    /**
+     * Gets the directory of the topmost project being built, usually the 
current directory or the
+     * directory pointed at by the {@code -f/--file} command line argument.
+     */
+    @Nonnull
+    Path getTopDirectory();
+
+    /**
+     * Gets the root directory of the session, which is the root directory for 
the top directory project.
+     *
+     * @throws IllegalStateException if the root directory could not be found
+     * @see #getTopDirectory()
+     * @see Project#getRootDirectory()
+     */
     @Nonnull
+    Path getRootDirectory();
+
+    /**
+     * @deprecated use {@link #getRootDirectory()} instead
+     */
+    @Nonnull
+    @Deprecated
     Path getMultiModuleProjectDirectory();
 
+    /**
+     * @deprecated use {@link #getTopDirectory()} instead
+     */
+    @Deprecated
     @Nonnull
     Path getExecutionRootDirectory();
 
@@ -97,8 +122,8 @@ public interface Session {
      * Returns the plugin context for mojo being executed and the specified
      * {@link Project}, never returns {@code null} as if context not present, 
creates it.
      *
-     * <strong>Implementation note:</strong> while this method return type is 
{@link Map}, the returned map instance
-     * implements {@link java.util.concurrent.ConcurrentMap} as well.
+     * <strong>Implementation note:</strong> while this method return type is 
{@link Map}, the
+     * returned map instance implements {@link 
java.util.concurrent.ConcurrentMap} as well.
      *
      * @throws org.apache.maven.api.services.MavenException if not called from 
the within a mojo execution
      */
diff --git a/api/maven-api-model/src/main/mdo/maven.mdo 
b/api/maven-api-model/src/main/mdo/maven.mdo
index a42a65531..fc8418fb6 100644
--- a/api/maven-api-model/src/main/mdo/maven.mdo
+++ b/api/maven-api-model/src/main/mdo/maven.mdo
@@ -212,6 +212,19 @@
           </description>
           <type>String</type>
         </field>
+        <field xml.attribute="true" xml.tagName="root">
+          <name>root</name>
+          <version>4.0.0+</version>
+          <description>
+            <![CDATA[
+            Indicates that this project is the root project, located in the 
upper directory of the source tree.
+            This is the directory which may contain the .mvn directory.
+            <br><b>Since</b>: Maven 4.0.0
+            ]]>
+          </description>
+          <type>boolean</type>
+          <defaultValue>false</defaultValue>
+        </field>
         <field>
           <name>inceptionYear</name>
           <version>3.0.0+</version>
diff --git 
a/maven-compat/src/test/java/org/apache/maven/project/TestProjectBuilder.java 
b/maven-compat/src/test/java/org/apache/maven/project/TestProjectBuilder.java
index 6a3f15509..612d8e7dd 100644
--- 
a/maven-compat/src/test/java/org/apache/maven/project/TestProjectBuilder.java
+++ 
b/maven-compat/src/test/java/org/apache/maven/project/TestProjectBuilder.java
@@ -29,6 +29,7 @@ import 
org.apache.maven.artifact.repository.ArtifactRepository;
 import org.apache.maven.bridge.MavenRepositorySystem;
 import org.apache.maven.model.building.ModelBuilder;
 import org.apache.maven.model.building.ModelProcessor;
+import org.apache.maven.model.root.RootLocator;
 import org.apache.maven.repository.internal.ModelCacheFactory;
 import org.eclipse.aether.RepositorySystem;
 import org.eclipse.aether.impl.RemoteRepositoryManager;
@@ -45,7 +46,8 @@ public class TestProjectBuilder extends DefaultProjectBuilder 
{
             RepositorySystem repoSystem,
             RemoteRepositoryManager repositoryManager,
             ProjectDependenciesResolver dependencyResolver,
-            ModelCacheFactory modelCacheFactory) {
+            ModelCacheFactory modelCacheFactory,
+            RootLocator rootLocator) {
         super(
                 modelBuilder,
                 modelProcessor,
@@ -54,7 +56,8 @@ public class TestProjectBuilder extends DefaultProjectBuilder 
{
                 repoSystem,
                 repositoryManager,
                 dependencyResolver,
-                modelCacheFactory);
+                modelCacheFactory,
+                rootLocator);
     }
 
     @Override
diff --git 
a/maven-core/src/main/java/org/apache/maven/execution/DefaultMavenExecutionRequest.java
 
b/maven-core/src/main/java/org/apache/maven/execution/DefaultMavenExecutionRequest.java
index e0a073b91..310574f37 100644
--- 
a/maven-core/src/main/java/org/apache/maven/execution/DefaultMavenExecutionRequest.java
+++ 
b/maven-core/src/main/java/org/apache/maven/execution/DefaultMavenExecutionRequest.java
@@ -19,6 +19,7 @@
 package org.apache.maven.execution;
 
 import java.io.File;
+import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.Date;
 import java.util.HashMap;
@@ -30,6 +31,7 @@ import java.util.Properties;
 import org.apache.maven.artifact.repository.ArtifactRepository;
 import org.apache.maven.eventspy.internal.EventSpyDispatcher;
 import org.apache.maven.model.Profile;
+import org.apache.maven.model.root.RootLocator;
 import org.apache.maven.project.DefaultProjectBuildingRequest;
 import org.apache.maven.project.ProjectBuildingRequest;
 import org.apache.maven.properties.internal.SystemProperties;
@@ -102,6 +104,10 @@ public class DefaultMavenExecutionRequest implements 
MavenExecutionRequest {
 
     private File basedir;
 
+    private Path rootDirectory;
+
+    private Path topDirectory;
+
     private List<String> goals;
 
     private boolean useReactor = false;
@@ -1051,16 +1057,43 @@ public class DefaultMavenExecutionRequest implements 
MavenExecutionRequest {
         return this;
     }
 
+    @Deprecated
     @Override
     public void setMultiModuleProjectDirectory(File directory) {
         this.multiModuleProjectDirectory = directory;
     }
 
+    @Deprecated
     @Override
     public File getMultiModuleProjectDirectory() {
         return multiModuleProjectDirectory;
     }
 
+    @Override
+    public Path getRootDirectory() {
+        if (rootDirectory == null) {
+            throw new 
IllegalStateException(RootLocator.UNABLE_TO_FIND_ROOT_PROJECT_MESSAGE);
+        }
+        return rootDirectory;
+    }
+
+    @Override
+    public MavenExecutionRequest setRootDirectory(Path rootDirectory) {
+        this.rootDirectory = rootDirectory;
+        return this;
+    }
+
+    @Override
+    public Path getTopDirectory() {
+        return topDirectory;
+    }
+
+    @Override
+    public MavenExecutionRequest setTopDirectory(Path topDirectory) {
+        this.topDirectory = topDirectory;
+        return this;
+    }
+
     @Override
     public MavenExecutionRequest setEventSpyDispatcher(EventSpyDispatcher 
eventSpyDispatcher) {
         this.eventSpyDispatcher = eventSpyDispatcher;
diff --git 
a/maven-core/src/main/java/org/apache/maven/execution/MavenExecutionRequest.java
 
b/maven-core/src/main/java/org/apache/maven/execution/MavenExecutionRequest.java
index 9d86df5e8..66a49a1f2 100644
--- 
a/maven-core/src/main/java/org/apache/maven/execution/MavenExecutionRequest.java
+++ 
b/maven-core/src/main/java/org/apache/maven/execution/MavenExecutionRequest.java
@@ -19,6 +19,7 @@
 package org.apache.maven.execution;
 
 import java.io.File;
+import java.nio.file.Path;
 import java.util.Date;
 import java.util.List;
 import java.util.Map;
@@ -91,8 +92,17 @@ public interface MavenExecutionRequest {
     // ----------------------------------------------------------------------
 
     // Base directory
+
+    /**
+     * @deprecated use {@link #setTopDirectory(Path)} instead
+     */
+    @Deprecated
     MavenExecutionRequest setBaseDirectory(File basedir);
 
+    /**
+     * @deprecated use {@link #getTopDirectory()} instead
+     */
+    @Deprecated
     String getBaseDirectory();
 
     // Timing (remove this)
@@ -494,14 +504,51 @@ public interface MavenExecutionRequest {
 
     /**
      * @since 3.3.0
+     * @deprecated use {@link #setRootDirectory(Path)} instead
      */
+    @Deprecated
     void setMultiModuleProjectDirectory(File file);
 
     /**
      * @since 3.3.0
+     * @deprecated use {@link #getRootDirectory()} instead
      */
+    @Deprecated
     File getMultiModuleProjectDirectory();
 
+    /**
+     * Sets the top directory of the project.
+     *
+     * @since 4.0.0
+     */
+    MavenExecutionRequest setTopDirectory(Path topDirectory);
+
+    /**
+     * Gets the directory of the topmost project being built, usually the 
current directory or the
+     * directory pointed at by the {@code -f/--file} command line argument.
+     *
+     * @since 4.0.0
+     */
+    Path getTopDirectory();
+
+    /**
+     * Sets the root directory of the project.
+     *
+     * @since 4.0.0
+     */
+    MavenExecutionRequest setRootDirectory(Path rootDirectory);
+
+    /**
+     * Gets the root directory of the top project, which is the parent 
directory containing the {@code .mvn}
+     * directory or a {@code pom.xml} file with the {@code root="true"} 
attribute.
+     * If there's no such directory, an {@code IllegalStateException} will be 
thrown.
+     *
+     * @throws IllegalStateException if the root directory could not be found
+     * @see #getTopDirectory()
+     * @since 4.0.0
+     */
+    Path getRootDirectory();
+
     /**
      * @since 3.3.0
      */
diff --git 
a/maven-core/src/main/java/org/apache/maven/execution/MavenSession.java 
b/maven-core/src/main/java/org/apache/maven/execution/MavenSession.java
index bd4e34ad5..fc430696e 100644
--- a/maven-core/src/main/java/org/apache/maven/execution/MavenSession.java
+++ b/maven-core/src/main/java/org/apache/maven/execution/MavenSession.java
@@ -19,6 +19,7 @@
 package org.apache.maven.execution;
 
 import java.io.File;
+import java.nio.file.Path;
 import java.util.Arrays;
 import java.util.Date;
 import java.util.List;
@@ -141,10 +142,30 @@ public class MavenSession implements Cloneable {
         return projects;
     }
 
+    /**
+     * @deprecated use {@link #getTopDirectory()} ()}
+     */
+    @Deprecated
     public String getExecutionRootDirectory() {
         return request.getBaseDirectory();
     }
 
+    /**
+     * @see MavenExecutionRequest#getTopDirectory()
+     * @since 4.0.0
+     */
+    public Path getTopDirectory() {
+        return request.getTopDirectory();
+    }
+
+    /**
+     * @see MavenExecutionRequest#getRootDirectory()
+     * @since 4.0.0
+     */
+    public Path getRootDirectory() {
+        return request.getRootDirectory();
+    }
+
     public MavenExecutionRequest getRequest() {
         return request;
     }
diff --git 
a/maven-core/src/main/java/org/apache/maven/internal/impl/DefaultProject.java 
b/maven-core/src/main/java/org/apache/maven/internal/impl/DefaultProject.java
index 0951f26f8..c234608ff 100644
--- 
a/maven-core/src/main/java/org/apache/maven/internal/impl/DefaultProject.java
+++ 
b/maven-core/src/main/java/org/apache/maven/internal/impl/DefaultProject.java
@@ -129,6 +129,22 @@ public class DefaultProject implements Project {
         return project.isExecutionRoot();
     }
 
+    @Override
+    public boolean isTopProject() {
+        return getBasedir().isPresent()
+                && getBasedir().get().equals(getSession().getTopDirectory());
+    }
+
+    @Override
+    public boolean isRootProject() {
+        return getBasedir().isPresent() && 
getBasedir().get().equals(getRootDirectory());
+    }
+
+    @Override
+    public Path getRootDirectory() {
+        return project.getRootDirectory();
+    }
+
     @Override
     public Optional<Project> getParent() {
         MavenProject parent = project.getParent();
diff --git 
a/maven-core/src/main/java/org/apache/maven/internal/impl/DefaultSession.java 
b/maven-core/src/main/java/org/apache/maven/internal/impl/DefaultSession.java
index 05a72ff97..011710778 100644
--- 
a/maven-core/src/main/java/org/apache/maven/internal/impl/DefaultSession.java
+++ 
b/maven-core/src/main/java/org/apache/maven/internal/impl/DefaultSession.java
@@ -19,7 +19,6 @@
 package org.apache.maven.internal.impl;
 
 import java.nio.file.Path;
-import java.nio.file.Paths;
 import java.time.Instant;
 import java.util.Collections;
 import java.util.HashMap;
@@ -150,7 +149,17 @@ public class DefaultSession extends AbstractSession {
     @Nonnull
     @Override
     public Path getExecutionRootDirectory() {
-        return Paths.get(mavenSession.getRequest().getBaseDirectory());
+        return getTopDirectory();
+    }
+
+    @Override
+    public Path getRootDirectory() {
+        return mavenSession.getRequest().getRootDirectory();
+    }
+
+    @Override
+    public Path getTopDirectory() {
+        return mavenSession.getRequest().getTopDirectory();
     }
 
     @Nonnull
diff --git 
a/maven-core/src/main/java/org/apache/maven/internal/transformation/ConsumerPomArtifactTransformer.java
 
b/maven-core/src/main/java/org/apache/maven/internal/transformation/ConsumerPomArtifactTransformer.java
index 1b6c5a8d0..eabbe03f7 100644
--- 
a/maven-core/src/main/java/org/apache/maven/internal/transformation/ConsumerPomArtifactTransformer.java
+++ 
b/maven-core/src/main/java/org/apache/maven/internal/transformation/ConsumerPomArtifactTransformer.java
@@ -74,6 +74,9 @@ public final class ConsumerPomArtifactTransformer {
                 generatedFile = Files.createTempFile(buildDir, 
CONSUMER_POM_CLASSIFIER, "pom");
             }
             project.addAttachedArtifact(new ConsumerPomArtifact(project, 
generatedFile, session));
+        } else if (project.getModel().isRoot()) {
+            throw new IllegalStateException(
+                    "The use of the root attribute on the model requires the 
buildconsumer feature to be active");
         }
     }
 
diff --git 
a/maven-core/src/main/java/org/apache/maven/project/DefaultProjectBuilder.java 
b/maven-core/src/main/java/org/apache/maven/project/DefaultProjectBuilder.java
index 5bb9dab0f..027ca74ee 100644
--- 
a/maven-core/src/main/java/org/apache/maven/project/DefaultProjectBuilder.java
+++ 
b/maven-core/src/main/java/org/apache/maven/project/DefaultProjectBuilder.java
@@ -69,6 +69,7 @@ import org.apache.maven.model.building.StringModelSource;
 import org.apache.maven.model.building.TransformerContext;
 import org.apache.maven.model.building.TransformerContextBuilder;
 import org.apache.maven.model.resolution.ModelResolver;
+import org.apache.maven.model.root.RootLocator;
 import org.apache.maven.repository.internal.ArtifactDescriptorUtils;
 import org.apache.maven.repository.internal.ModelCacheFactory;
 import org.codehaus.plexus.util.Os;
@@ -101,6 +102,8 @@ public class DefaultProjectBuilder implements 
ProjectBuilder {
     private final ProjectDependenciesResolver dependencyResolver;
     private final ModelCacheFactory modelCacheFactory;
 
+    private final RootLocator rootLocator;
+
     @SuppressWarnings("checkstyle:ParameterNumber")
     @Inject
     public DefaultProjectBuilder(
@@ -111,7 +114,8 @@ public class DefaultProjectBuilder implements 
ProjectBuilder {
             RepositorySystem repoSystem,
             RemoteRepositoryManager repositoryManager,
             ProjectDependenciesResolver dependencyResolver,
-            ModelCacheFactory modelCacheFactory) {
+            ModelCacheFactory modelCacheFactory,
+            RootLocator rootLocator) {
         this.modelBuilder = modelBuilder;
         this.modelProcessor = modelProcessor;
         this.projectBuildingHelper = projectBuildingHelper;
@@ -120,6 +124,7 @@ public class DefaultProjectBuilder implements 
ProjectBuilder {
         this.repositoryManager = repositoryManager;
         this.dependencyResolver = dependencyResolver;
         this.modelCacheFactory = modelCacheFactory;
+        this.rootLocator = rootLocator;
     }
     // ----------------------------------------------------------------------
     // MavenProjectBuilder Implementation
@@ -162,6 +167,11 @@ public class DefaultProjectBuilder implements 
ProjectBuilder {
                 request.setModelSource(modelSource);
                 request.setLocationTracking(true);
 
+                if (pomFile != null) {
+                    project.setRootDirectory(
+                            
rootLocator.findRoot(pomFile.getParentFile().toPath()));
+                }
+
                 ModelBuildingResult result;
                 try {
                     result = modelBuilder.build(request);
@@ -445,6 +455,8 @@ public class DefaultProjectBuilder implements 
ProjectBuilder {
         MavenProject project = new MavenProject();
         project.setFile(pomFile);
 
+        
project.setRootDirectory(rootLocator.findRoot(pomFile.getParentFile().toPath()));
+
         ModelBuildingRequest request = getModelBuildingRequest(config)
                 .setPomFile(pomFile)
                 .setTwoPhaseBuilding(true)
diff --git 
a/maven-core/src/main/java/org/apache/maven/project/MavenProject.java 
b/maven-core/src/main/java/org/apache/maven/project/MavenProject.java
index b024001bf..84a4090b2 100644
--- a/maven-core/src/main/java/org/apache/maven/project/MavenProject.java
+++ b/maven-core/src/main/java/org/apache/maven/project/MavenProject.java
@@ -21,6 +21,7 @@ package org.apache.maven.project;
 import java.io.File;
 import java.io.IOException;
 import java.io.Writer;
+import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
@@ -64,6 +65,7 @@ import org.apache.maven.model.Repository;
 import org.apache.maven.model.Resource;
 import org.apache.maven.model.Scm;
 import org.apache.maven.model.io.xpp3.MavenXpp3Writer;
+import org.apache.maven.model.root.RootLocator;
 import org.apache.maven.project.artifact.InvalidDependencyVersionException;
 import org.apache.maven.project.artifact.MavenMetadataSource;
 import org.codehaus.plexus.classworlds.realm.ClassRealm;
@@ -105,6 +107,8 @@ public class MavenProject implements Cloneable {
 
     private File basedir;
 
+    private Path rootDirectory;
+
     private Set<Artifact> resolvedArtifacts;
 
     private ArtifactFilter artifactFilter;
@@ -1679,4 +1683,20 @@ public class MavenProject implements Cloneable {
     public void setProjectBuildingRequest(ProjectBuildingRequest 
projectBuildingRequest) {
         this.projectBuilderConfiguration = projectBuildingRequest;
     }
+
+    /**
+     * @since 4.0.0
+     * @return the rootDirectory for this project
+     * @throws IllegalStateException if the rootDirectory cannot be found
+     */
+    public Path getRootDirectory() {
+        if (rootDirectory == null) {
+            throw new 
IllegalStateException(RootLocator.UNABLE_TO_FIND_ROOT_PROJECT_MESSAGE);
+        }
+        return rootDirectory;
+    }
+
+    public void setRootDirectory(Path rootDirectory) {
+        this.rootDirectory = rootDirectory;
+    }
 }
diff --git 
a/maven-core/src/test/java/org/apache/maven/internal/impl/DefaultSessionTest.java
 
b/maven-core/src/test/java/org/apache/maven/internal/impl/DefaultSessionTest.java
new file mode 100644
index 000000000..dd516c91a
--- /dev/null
+++ 
b/maven-core/src/test/java/org/apache/maven/internal/impl/DefaultSessionTest.java
@@ -0,0 +1,62 @@
+/*
+ * 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.internal.impl;
+
+import java.nio.file.Paths;
+import java.util.Collections;
+
+import org.apache.maven.execution.DefaultMavenExecutionRequest;
+import org.apache.maven.execution.MavenSession;
+import org.apache.maven.model.root.RootLocator;
+import org.apache.maven.repository.internal.MavenRepositorySystemUtils;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.internal.impl.DefaultRepositorySystem;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+public class DefaultSessionTest {
+
+    @Test
+    void testRootDirectoryWithNull() {
+        RepositorySystemSession rss = MavenRepositorySystemUtils.newSession();
+        DefaultMavenExecutionRequest mer = new DefaultMavenExecutionRequest();
+        MavenSession ms = new MavenSession(null, rss, mer, null);
+        DefaultSession session =
+                new DefaultSession(ms, new DefaultRepositorySystem(), 
Collections.emptyList(), null, null, null);
+
+        assertEquals(
+                RootLocator.UNABLE_TO_FIND_ROOT_PROJECT_MESSAGE,
+                assertThrows(IllegalStateException.class, 
session::getRootDirectory)
+                        .getMessage());
+    }
+
+    @Test
+    void testRootDirectory() {
+        RepositorySystemSession rss = MavenRepositorySystemUtils.newSession();
+        DefaultMavenExecutionRequest mer = new DefaultMavenExecutionRequest();
+        MavenSession ms = new MavenSession(null, rss, mer, null);
+        ms.getRequest().setRootDirectory(Paths.get("myRootDirectory"));
+        DefaultSession session =
+                new DefaultSession(ms, new DefaultRepositorySystem(), 
Collections.emptyList(), null, null, null);
+
+        assertEquals(Paths.get("myRootDirectory"), session.getRootDirectory());
+    }
+}
diff --git 
a/maven-core/src/test/java/org/apache/maven/plugin/PluginParameterExpressionEvaluatorTest.java
 
b/maven-core/src/test/java/org/apache/maven/plugin/PluginParameterExpressionEvaluatorTest.java
index 39db5828a..c01c6f56c 100644
--- 
a/maven-core/src/test/java/org/apache/maven/plugin/PluginParameterExpressionEvaluatorTest.java
+++ 
b/maven-core/src/test/java/org/apache/maven/plugin/PluginParameterExpressionEvaluatorTest.java
@@ -21,6 +21,9 @@ package org.apache.maven.plugin;
 import javax.inject.Inject;
 
 import java.io.File;
+import java.lang.reflect.InvocationTargetException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
@@ -38,6 +41,7 @@ import org.apache.maven.execution.MavenSession;
 import org.apache.maven.model.Build;
 import org.apache.maven.model.Dependency;
 import org.apache.maven.model.Model;
+import org.apache.maven.model.root.RootLocator;
 import org.apache.maven.plugin.descriptor.MojoDescriptor;
 import org.apache.maven.plugin.descriptor.PluginDescriptor;
 import org.apache.maven.project.DuplicateProjectException;
@@ -51,8 +55,11 @@ import org.junit.jupiter.api.Test;
 
 import static org.codehaus.plexus.testing.PlexusExtension.getTestFile;
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 /**
@@ -64,6 +71,8 @@ class PluginParameterExpressionEvaluatorTest extends 
AbstractCoreMavenComponentT
     @Inject
     private RepositorySystem factory;
 
+    private Path rootDirectory;
+
     @Test
     void testPluginDescriptorExpressionReference() throws Exception {
         MojoExecution exec = newMojoExecution();
@@ -357,6 +366,28 @@ class PluginParameterExpressionEvaluatorTest extends 
AbstractCoreMavenComponentT
         assertEquals("testGroup", result.getGroupId());
     }
 
+    @Test
+    void testRootDirectoryNotPrefixed() throws Exception {
+        ExpressionEvaluator ee = 
createExpressionEvaluator(createDefaultProject(), null, new Properties());
+        assertNull(ee.evaluate("${rootDirectory}"));
+    }
+
+    @Test
+    void testRootDirectoryWithNull() throws Exception {
+        ExpressionEvaluator ee = 
createExpressionEvaluator(createDefaultProject(), null, new Properties());
+        Exception e = assertThrows(Exception.class, () -> 
ee.evaluate("${session.rootDirectory}"));
+        e = assertInstanceOf(InvocationTargetException.class, e.getCause());
+        e = assertInstanceOf(IllegalStateException.class, e.getCause());
+        assertEquals(RootLocator.UNABLE_TO_FIND_ROOT_PROJECT_MESSAGE, 
e.getMessage());
+    }
+
+    @Test
+    void testRootDirectory() throws Exception {
+        this.rootDirectory = Paths.get("myRootDirectory");
+        ExpressionEvaluator ee = 
createExpressionEvaluator(createDefaultProject(), null, new Properties());
+        assertInstanceOf(Path.class, ee.evaluate("${session.rootDirectory}"));
+    }
+
     private MavenProject createDefaultProject() {
         return new MavenProject(new Model());
     }
@@ -368,6 +399,7 @@ class PluginParameterExpressionEvaluatorTest extends 
AbstractCoreMavenComponentT
         MutablePlexusContainer container = (MutablePlexusContainer) 
getContainer();
         MavenSession session = createSession(container, repo, 
executionProperties);
         session.setCurrentProject(project);
+        session.getRequest().setRootDirectory(rootDirectory);
 
         MojoDescriptor mojo = new MojoDescriptor();
         mojo.setPluginDescriptor(pluginDescriptor);
diff --git 
a/maven-core/src/test/java/org/apache/maven/plugin/PluginParameterExpressionEvaluatorTest.java
 
b/maven-core/src/test/java/org/apache/maven/plugin/PluginParameterExpressionEvaluatorV4Test.java
similarity index 69%
copy from 
maven-core/src/test/java/org/apache/maven/plugin/PluginParameterExpressionEvaluatorTest.java
copy to 
maven-core/src/test/java/org/apache/maven/plugin/PluginParameterExpressionEvaluatorV4Test.java
index 39db5828a..aee330983 100644
--- 
a/maven-core/src/test/java/org/apache/maven/plugin/PluginParameterExpressionEvaluatorTest.java
+++ 
b/maven-core/src/test/java/org/apache/maven/plugin/PluginParameterExpressionEvaluatorV4Test.java
@@ -21,6 +21,9 @@ package org.apache.maven.plugin;
 import javax.inject.Inject;
 
 import java.io.File;
+import java.lang.reflect.InvocationTargetException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
@@ -28,6 +31,7 @@ import java.util.Map;
 import java.util.Properties;
 
 import org.apache.maven.AbstractCoreMavenComponentTestCase;
+import org.apache.maven.api.Session;
 import org.apache.maven.artifact.Artifact;
 import org.apache.maven.artifact.ArtifactUtils;
 import org.apache.maven.artifact.repository.ArtifactRepository;
@@ -35,9 +39,12 @@ import 
org.apache.maven.execution.DefaultMavenExecutionRequest;
 import org.apache.maven.execution.DefaultMavenExecutionResult;
 import org.apache.maven.execution.MavenExecutionRequest;
 import org.apache.maven.execution.MavenSession;
+import org.apache.maven.internal.impl.DefaultProject;
+import org.apache.maven.internal.impl.DefaultSession;
 import org.apache.maven.model.Build;
 import org.apache.maven.model.Dependency;
 import org.apache.maven.model.Model;
+import org.apache.maven.model.root.RootLocator;
 import org.apache.maven.plugin.descriptor.MojoDescriptor;
 import org.apache.maven.plugin.descriptor.PluginDescriptor;
 import org.apache.maven.project.DuplicateProjectException;
@@ -47,30 +54,40 @@ import org.codehaus.plexus.MutablePlexusContainer;
 import org.codehaus.plexus.PlexusContainer;
 import 
org.codehaus.plexus.component.configurator.expression.ExpressionEvaluator;
 import org.codehaus.plexus.util.dag.CycleDetectedException;
+import org.eclipse.aether.DefaultRepositorySystemSession;
+import org.eclipse.aether.internal.impl.DefaultRepositorySystem;
+import org.eclipse.aether.internal.impl.SimpleLocalRepositoryManagerFactory;
+import org.eclipse.aether.repository.LocalRepository;
+import org.eclipse.aether.repository.NoLocalRepositoryManagerException;
 import org.junit.jupiter.api.Test;
 
 import static org.codehaus.plexus.testing.PlexusExtension.getTestFile;
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 /**
  * @author Jason van Zyl
  */
-class PluginParameterExpressionEvaluatorTest extends 
AbstractCoreMavenComponentTestCase {
+public class PluginParameterExpressionEvaluatorV4Test extends 
AbstractCoreMavenComponentTestCase {
     private static final String FS = File.separator;
 
     @Inject
     private RepositorySystem factory;
 
+    private Path rootDirectory;
+
     @Test
-    void testPluginDescriptorExpressionReference() throws Exception {
+    public void testPluginDescriptorExpressionReference() throws Exception {
         MojoExecution exec = newMojoExecution();
 
-        MavenSession session = newMavenSession();
+        Session session = newSession();
 
-        Object result = new PluginParameterExpressionEvaluator(session, 
exec).evaluate("${plugin}");
+        Object result = new PluginParameterExpressionEvaluatorV4(session, 
null, exec).evaluate("${plugin}");
 
         System.out.println("Result: " + result);
 
@@ -81,7 +98,7 @@ class PluginParameterExpressionEvaluatorTest extends 
AbstractCoreMavenComponentT
     }
 
     @Test
-    void testPluginArtifactsExpressionReference() throws Exception {
+    public void testPluginArtifactsExpressionReference() throws Exception {
         MojoExecution exec = newMojoExecution();
 
         Artifact depArtifact = createArtifact("group", "artifact", "1");
@@ -91,11 +108,11 @@ class PluginParameterExpressionEvaluatorTest extends 
AbstractCoreMavenComponentT
 
         exec.getMojoDescriptor().getPluginDescriptor().setArtifacts(deps);
 
-        MavenSession session = newMavenSession();
+        Session session = newSession();
 
         @SuppressWarnings("unchecked")
-        List<Artifact> depResults =
-                (List<Artifact>) new 
PluginParameterExpressionEvaluator(session, 
exec).evaluate("${plugin.artifacts}");
+        List<Artifact> depResults = (List<Artifact>)
+                new PluginParameterExpressionEvaluatorV4(session, null, 
exec).evaluate("${plugin.artifacts}");
 
         System.out.println("Result: " + depResults);
 
@@ -105,7 +122,7 @@ class PluginParameterExpressionEvaluatorTest extends 
AbstractCoreMavenComponentT
     }
 
     @Test
-    void testPluginArtifactMapExpressionReference() throws Exception {
+    public void testPluginArtifactMapExpressionReference() throws Exception {
         MojoExecution exec = newMojoExecution();
 
         Artifact depArtifact = createArtifact("group", "artifact", "1");
@@ -115,11 +132,11 @@ class PluginParameterExpressionEvaluatorTest extends 
AbstractCoreMavenComponentT
 
         exec.getMojoDescriptor().getPluginDescriptor().setArtifacts(deps);
 
-        MavenSession session = newMavenSession();
+        Session session = newSession();
 
         @SuppressWarnings("unchecked")
         Map<String, Artifact> depResults = (Map<String, Artifact>)
-                new PluginParameterExpressionEvaluator(session, 
exec).evaluate("${plugin.artifactMap}");
+                new PluginParameterExpressionEvaluatorV4(session, null, 
exec).evaluate("${plugin.artifactMap}");
 
         System.out.println("Result: " + depResults);
 
@@ -132,12 +149,12 @@ class PluginParameterExpressionEvaluatorTest extends 
AbstractCoreMavenComponentT
     }
 
     @Test
-    void testPluginArtifactIdExpressionReference() throws Exception {
+    public void testPluginArtifactIdExpressionReference() throws Exception {
         MojoExecution exec = newMojoExecution();
 
-        MavenSession session = newMavenSession();
+        Session session = newSession();
 
-        Object result = new PluginParameterExpressionEvaluator(session, 
exec).evaluate("${plugin.artifactId}");
+        Object result = new PluginParameterExpressionEvaluatorV4(session, 
null, exec).evaluate("${plugin.artifactId}");
 
         System.out.println("Result: " + result);
 
@@ -148,7 +165,7 @@ class PluginParameterExpressionEvaluatorTest extends 
AbstractCoreMavenComponentT
     }
 
     @Test
-    void testValueExtractionWithAPomValueContainingAPath() throws Exception {
+    public void testValueExtractionWithAPomValueContainingAPath() throws 
Exception {
         String expected = 
getTestFile("target/test-classes/target/classes").getCanonicalPath();
 
         Build build = new Build();
@@ -169,7 +186,7 @@ class PluginParameterExpressionEvaluatorTest extends 
AbstractCoreMavenComponentT
     }
 
     @Test
-    void testEscapedVariablePassthrough() throws Exception {
+    public void testEscapedVariablePassthrough() throws Exception {
         String var = "${var}";
 
         Model model = new Model();
@@ -185,7 +202,7 @@ class PluginParameterExpressionEvaluatorTest extends 
AbstractCoreMavenComponentT
     }
 
     @Test
-    void testEscapedVariablePassthroughInLargerExpression() throws Exception {
+    public void testEscapedVariablePassthroughInLargerExpression() throws 
Exception {
         String var = "${var}";
         String key = var + " with version: ${project.version}";
 
@@ -202,7 +219,7 @@ class PluginParameterExpressionEvaluatorTest extends 
AbstractCoreMavenComponentT
     }
 
     @Test
-    void testMultipleSubExpressionsInLargerExpression() throws Exception {
+    public void testMultipleSubExpressionsInLargerExpression() throws 
Exception {
         String key = "${project.artifactId} with version: ${project.version}";
 
         Model model = new Model();
@@ -219,7 +236,7 @@ class PluginParameterExpressionEvaluatorTest extends 
AbstractCoreMavenComponentT
     }
 
     @Test
-    void testMissingPOMPropertyRefInLargerExpression() throws Exception {
+    public void testMissingPOMPropertyRefInLargerExpression() throws Exception 
{
         String expr = "/path/to/someproject-${baseVersion}";
 
         MavenProject project = new MavenProject(new Model());
@@ -232,7 +249,7 @@ class PluginParameterExpressionEvaluatorTest extends 
AbstractCoreMavenComponentT
     }
 
     @Test
-    void testPOMPropertyExtractionWithMissingProject_WithDotNotation() throws 
Exception {
+    public void testPOMPropertyExtractionWithMissingProject_WithDotNotation() 
throws Exception {
         String key = "m2.name";
         String checkValue = "value";
 
@@ -252,7 +269,7 @@ class PluginParameterExpressionEvaluatorTest extends 
AbstractCoreMavenComponentT
     }
 
     @Test
-    void testBasedirExtractionWithMissingProject() throws Exception {
+    public void testBasedirExtractionWithMissingProject() throws Exception {
         ExpressionEvaluator ee = createExpressionEvaluator(null, null, new 
Properties());
 
         Object value = ee.evaluate("${basedir}");
@@ -261,7 +278,7 @@ class PluginParameterExpressionEvaluatorTest extends 
AbstractCoreMavenComponentT
     }
 
     @Test
-    void testValueExtractionFromSystemPropertiesWithMissingProject() throws 
Exception {
+    public void testValueExtractionFromSystemPropertiesWithMissingProject() 
throws Exception {
         String sysprop = "PPEET_sysprop1";
 
         Properties executionProperties = new Properties();
@@ -278,7 +295,7 @@ class PluginParameterExpressionEvaluatorTest extends 
AbstractCoreMavenComponentT
     }
 
     @Test
-    void 
testValueExtractionFromSystemPropertiesWithMissingProject_WithDotNotation() 
throws Exception {
+    public void 
testValueExtractionFromSystemPropertiesWithMissingProject_WithDotNotation() 
throws Exception {
         String sysprop = "PPEET.sysprop2";
 
         Properties executionProperties = new Properties();
@@ -296,28 +313,33 @@ class PluginParameterExpressionEvaluatorTest extends 
AbstractCoreMavenComponentT
 
     @SuppressWarnings("deprecation")
     private static MavenSession createSession(PlexusContainer container, 
ArtifactRepository repo, Properties properties)
-            throws CycleDetectedException, DuplicateProjectException {
+            throws CycleDetectedException, DuplicateProjectException, 
NoLocalRepositoryManagerException {
         MavenExecutionRequest request = new DefaultMavenExecutionRequest()
                 .setSystemProperties(properties)
                 .setGoals(Collections.<String>emptyList())
                 .setBaseDirectory(new File(""))
                 .setLocalRepository(repo);
 
-        return new MavenSession(
-                container, request, new DefaultMavenExecutionResult(), 
Collections.<MavenProject>emptyList());
+        DefaultRepositorySystemSession repositorySession = new 
DefaultRepositorySystemSession();
+        repositorySession.setLocalRepositoryManager(new 
SimpleLocalRepositoryManagerFactory()
+                .newInstance(repositorySession, new 
LocalRepository(repo.getUrl())));
+        MavenSession session =
+                new MavenSession(container, repositorySession, request, new 
DefaultMavenExecutionResult());
+        session.setProjects(Collections.<MavenProject>emptyList());
+        return session;
     }
 
     @Test
-    void testLocalRepositoryExtraction() throws Exception {
+    public void testLocalRepositoryExtraction() throws Exception {
         ExpressionEvaluator expressionEvaluator =
                 createExpressionEvaluator(createDefaultProject(), null, new 
Properties());
         Object value = expressionEvaluator.evaluate("${localRepository}");
 
-        assertEquals("local", ((ArtifactRepository) value).getId());
+        assertEquals("local", ((org.apache.maven.api.LocalRepository) 
value).getId());
     }
 
     @Test
-    void testTwoExpressions() throws Exception {
+    public void testTwoExpressions() throws Exception {
         Build build = new Build();
         build.setDirectory("expected-directory");
         build.setFinalName("expected-finalName");
@@ -334,7 +356,7 @@ class PluginParameterExpressionEvaluatorTest extends 
AbstractCoreMavenComponentT
     }
 
     @Test
-    void testShouldExtractPluginArtifacts() throws Exception {
+    public void testShouldExtractPluginArtifacts() throws Exception {
         PluginDescriptor pd = new PluginDescriptor();
 
         Artifact artifact = createArtifact("testGroup", "testArtifact", "1.0");
@@ -357,6 +379,28 @@ class PluginParameterExpressionEvaluatorTest extends 
AbstractCoreMavenComponentT
         assertEquals("testGroup", result.getGroupId());
     }
 
+    @Test
+    void testRootDirectoryNotPrefixed() throws Exception {
+        ExpressionEvaluator ee = 
createExpressionEvaluator(createDefaultProject(), null, new Properties());
+        assertNull(ee.evaluate("${rootDirectory}"));
+    }
+
+    @Test
+    void testRootDirectoryWithNull() throws Exception {
+        ExpressionEvaluator ee = 
createExpressionEvaluator(createDefaultProject(), null, new Properties());
+        Exception e = assertThrows(Exception.class, () -> 
ee.evaluate("${session.rootDirectory}"));
+        e = assertInstanceOf(InvocationTargetException.class, e.getCause());
+        e = assertInstanceOf(IllegalStateException.class, e.getCause());
+        assertEquals(RootLocator.UNABLE_TO_FIND_ROOT_PROJECT_MESSAGE, 
e.getMessage());
+    }
+
+    @Test
+    void testRootDirectory() throws Exception {
+        this.rootDirectory = Paths.get("myRootDirectory");
+        ExpressionEvaluator ee = 
createExpressionEvaluator(createDefaultProject(), null, new Properties());
+        assertInstanceOf(Path.class, ee.evaluate("${session.rootDirectory}"));
+    }
+
     private MavenProject createDefaultProject() {
         return new MavenProject(new Model());
     }
@@ -366,8 +410,12 @@ class PluginParameterExpressionEvaluatorTest extends 
AbstractCoreMavenComponentT
         ArtifactRepository repo = factory.createDefaultLocalRepository();
 
         MutablePlexusContainer container = (MutablePlexusContainer) 
getContainer();
-        MavenSession session = createSession(container, repo, 
executionProperties);
-        session.setCurrentProject(project);
+        MavenSession mavenSession = createSession(container, repo, 
executionProperties);
+        mavenSession.setCurrentProject(project);
+        mavenSession.getRequest().setRootDirectory(rootDirectory);
+
+        DefaultSession session =
+                new DefaultSession(mavenSession, new 
DefaultRepositorySystem(), null, null, null, null);
 
         MojoDescriptor mojo = new MojoDescriptor();
         mojo.setPluginDescriptor(pluginDescriptor);
@@ -375,7 +423,8 @@ class PluginParameterExpressionEvaluatorTest extends 
AbstractCoreMavenComponentT
 
         MojoExecution mojoExecution = new MojoExecution(mojo);
 
-        return new PluginParameterExpressionEvaluator(session, mojoExecution);
+        return new PluginParameterExpressionEvaluatorV4(
+                session, project != null ? new DefaultProject(session, 
project) : null, mojoExecution);
     }
 
     protected Artifact createArtifact(String groupId, String artifactId, 
String version) throws Exception {
@@ -403,6 +452,10 @@ class PluginParameterExpressionEvaluatorTest extends 
AbstractCoreMavenComponentT
         return new MojoExecution(md);
     }
 
+    private DefaultSession newSession() throws Exception {
+        return new DefaultSession(newMavenSession(), new 
DefaultRepositorySystem(), null, null, null, null);
+    }
+
     private MavenSession newMavenSession() throws Exception {
         return createMavenSession(null);
     }
diff --git 
a/maven-embedder/src/main/java/org/apache/maven/cli/CLIReportingUtils.java 
b/maven-embedder/src/main/java/org/apache/maven/cli/CLIReportingUtils.java
index bc879c640..704212be5 100644
--- a/maven-embedder/src/main/java/org/apache/maven/cli/CLIReportingUtils.java
+++ b/maven-embedder/src/main/java/org/apache/maven/cli/CLIReportingUtils.java
@@ -135,6 +135,13 @@ public final class CLIReportingUtils {
     }
 
     public static void showError(Logger logger, String message, Throwable e, 
boolean showStackTrace) {
+        if (logger == null) {
+            System.err.println(message);
+            if (showStackTrace && e != null) {
+                e.printStackTrace(System.err);
+            }
+            return;
+        }
         if (showStackTrace) {
             logger.error(message, e);
         } else {
diff --git a/maven-embedder/src/main/java/org/apache/maven/cli/CliRequest.java 
b/maven-embedder/src/main/java/org/apache/maven/cli/CliRequest.java
index 9a315f12c..29bff7b38 100644
--- a/maven-embedder/src/main/java/org/apache/maven/cli/CliRequest.java
+++ b/maven-embedder/src/main/java/org/apache/maven/cli/CliRequest.java
@@ -19,6 +19,7 @@
 package org.apache.maven.cli;
 
 import java.io.File;
+import java.nio.file.Path;
 import java.util.Properties;
 
 import org.apache.commons.cli.CommandLine;
@@ -40,6 +41,10 @@ public class CliRequest {
 
     File multiModuleProjectDirectory;
 
+    Path rootDirectory;
+
+    Path topDirectory;
+
     boolean verbose;
 
     boolean quiet;
diff --git a/maven-embedder/src/main/java/org/apache/maven/cli/MavenCli.java 
b/maven-embedder/src/main/java/org/apache/maven/cli/MavenCli.java
index d5fd81215..6e9ae1a0c 100644
--- a/maven-embedder/src/main/java/org/apache/maven/cli/MavenCli.java
+++ b/maven-embedder/src/main/java/org/apache/maven/cli/MavenCli.java
@@ -29,6 +29,8 @@ import java.io.InputStream;
 import java.io.PrintStream;
 import java.nio.charset.Charset;
 import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashSet;
@@ -37,6 +39,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Properties;
+import java.util.ServiceLoader;
 import java.util.Set;
 import java.util.function.Consumer;
 import java.util.regex.Matcher;
@@ -87,6 +90,7 @@ import org.apache.maven.lifecycle.LifecycleExecutionException;
 import org.apache.maven.logwrapper.LogLevelRecorder;
 import org.apache.maven.logwrapper.MavenSlf4jWrapperFactory;
 import org.apache.maven.model.building.ModelProcessor;
+import org.apache.maven.model.root.RootLocator;
 import org.apache.maven.project.MavenProject;
 import org.apache.maven.properties.internal.EnvironmentUtils;
 import org.apache.maven.properties.internal.SystemProperties;
@@ -320,6 +324,54 @@ public class MavenCli {
             }
         }
 
+        // We need to locate the top level project which may be pointed at 
using
+        // the -f/--file option.  However, the command line isn't parsed yet, 
so
+        // we need to iterate through the args to find it and act upon it.
+        Path topDirectory = Paths.get(cliRequest.workingDirectory);
+        boolean isAltFile = false;
+        for (String arg : cliRequest.args) {
+            if (isAltFile) {
+                // this is the argument following -f/--file
+                Path path = Paths.get(arg);
+                if (Files.isDirectory(path)) {
+                    topDirectory = path;
+                } else if (Files.isRegularFile(topDirectory)) {
+                    topDirectory = path.getParent();
+                    if (!Files.isDirectory(topDirectory)) {
+                        System.err.println("Directory " + topDirectory
+                                + " extracted from the -f/--file command-line 
argument " + arg + " does not exist");
+                        throw new ExitException(1);
+                    }
+                } else {
+                    System.err.println(
+                            "POM file " + arg + " specified with the -f/--file 
command line argument does not exist");
+                    throw new ExitException(1);
+                }
+                break;
+            } else {
+                // Check if this is the -f/--file option
+                isAltFile = 
arg.equals(String.valueOf(CLIManager.ALTERNATE_POM_FILE)) || arg.equals("file");
+            }
+        }
+        try {
+            topDirectory = topDirectory.toAbsolutePath().toRealPath();
+        } catch (IOException e) {
+            System.err.println("Error computing real path from " + 
topDirectory);
+            throw new ExitException(1);
+        }
+        cliRequest.topDirectory = topDirectory;
+        // We're very early in the process and we don't have the container set 
up yet,
+        // so we rely on the JDK services to eventually lookup a custom 
RootLocator.
+        // This is used to compute {@code session.rootDirectory} but all 
{@code project.rootDirectory}
+        // properties will be compute through the RootLocator found in the 
container.
+        RootLocator rootLocator =
+                ServiceLoader.load(RootLocator.class).iterator().next();
+        Path rootDirectory = rootLocator.findRoot(topDirectory);
+        if (rootDirectory == null) {
+            
System.err.println(RootLocator.UNABLE_TO_FIND_ROOT_PROJECT_MESSAGE);
+        }
+        cliRequest.rootDirectory = rootDirectory;
+
         //
         // Make sure the Maven home directory is an absolute path to save us 
from confusion with say drive-relative
         // Windows paths.
@@ -1185,6 +1237,8 @@ public class MavenCli {
         request.setSystemProperties(cliRequest.systemProperties);
         request.setUserProperties(cliRequest.userProperties);
         
request.setMultiModuleProjectDirectory(cliRequest.multiModuleProjectDirectory);
+        request.setRootDirectory(cliRequest.rootDirectory);
+        request.setTopDirectory(cliRequest.topDirectory);
         request.setPom(determinePom(commandLine, workingDirectory, 
baseDirectory));
         request.setTransferListener(determineTransferListener(quiet, verbose, 
commandLine, request));
         request.setExecutionListener(determineExecutionListener());
diff --git 
a/maven-embedder/src/test/java/org/apache/maven/cli/MavenCliTest.java 
b/maven-embedder/src/test/java/org/apache/maven/cli/MavenCliTest.java
index 2d58ebeb2..eea6a87f9 100644
--- a/maven-embedder/src/test/java/org/apache/maven/cli/MavenCliTest.java
+++ b/maven-embedder/src/test/java/org/apache/maven/cli/MavenCliTest.java
@@ -22,6 +22,8 @@ import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.PrintStream;
 import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+import java.nio.file.Paths;
 import java.util.Collections;
 import java.util.List;
 
@@ -36,6 +38,7 @@ import org.apache.maven.eventspy.internal.EventSpyDispatcher;
 import org.apache.maven.execution.MavenExecutionRequest;
 import org.apache.maven.execution.ProfileActivation;
 import org.apache.maven.execution.ProjectActivation;
+import org.apache.maven.model.root.DefaultRootLocator;
 import org.apache.maven.project.MavenProject;
 import org.apache.maven.shared.utils.logging.MessageUtils;
 import org.apache.maven.toolchain.building.ToolchainsBuildingRequest;
@@ -543,6 +546,12 @@ class MavenCliTest {
         assertThat(request.getUserProperties().getProperty("x"), is("false"));
     }
 
+    @Test
+    public void findRootProjectWithAttribute() {
+        Path test = Paths.get("src/test/projects/root-attribute");
+        assertEquals(test, new 
DefaultRootLocator().findRoot(test.resolve("child")));
+    }
+
     private MavenProject createMavenProject(String groupId, String artifactId) 
{
         MavenProject project = new MavenProject();
         project.setGroupId(groupId);
diff --git a/maven-embedder/src/test/projects/root-attribute/child/pom.xml 
b/maven-embedder/src/test/projects/root-attribute/child/pom.xml
new file mode 100644
index 000000000..7e5b53c8e
--- /dev/null
+++ b/maven-embedder/src/test/projects/root-attribute/child/pom.xml
@@ -0,0 +1,3 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0";>
+
+</project>
\ No newline at end of file
diff --git a/maven-embedder/src/test/projects/root-attribute/pom.xml 
b/maven-embedder/src/test/projects/root-attribute/pom.xml
new file mode 100644
index 000000000..c44d4f0c7
--- /dev/null
+++ b/maven-embedder/src/test/projects/root-attribute/pom.xml
@@ -0,0 +1,3 @@
+<project root="true" xmlns="http://maven.apache.org/POM/4.0.0";>
+
+</project>
\ No newline at end of file
diff --git 
a/maven-model-builder/src/main/java/org/apache/maven/model/building/DefaultModelBuilderFactory.java
 
b/maven-model-builder/src/main/java/org/apache/maven/model/building/DefaultModelBuilderFactory.java
index 3ddecdc57..75bc40381 100644
--- 
a/maven-model-builder/src/main/java/org/apache/maven/model/building/DefaultModelBuilderFactory.java
+++ 
b/maven-model-builder/src/main/java/org/apache/maven/model/building/DefaultModelBuilderFactory.java
@@ -64,6 +64,8 @@ import 
org.apache.maven.model.profile.activation.JdkVersionProfileActivator;
 import 
org.apache.maven.model.profile.activation.OperatingSystemProfileActivator;
 import org.apache.maven.model.profile.activation.ProfileActivator;
 import org.apache.maven.model.profile.activation.PropertyProfileActivator;
+import org.apache.maven.model.root.DefaultRootLocator;
+import org.apache.maven.model.root.RootLocator;
 import org.apache.maven.model.superpom.DefaultSuperPomProvider;
 import org.apache.maven.model.superpom.SuperPomProvider;
 import org.apache.maven.model.validation.DefaultModelValidator;
@@ -100,6 +102,8 @@ public class DefaultModelBuilderFactory {
     private ProfileActivationFilePathInterpolator 
profileActivationFilePathInterpolator;
     private ModelVersionProcessor versionProcessor;
 
+    private RootLocator rootLocator;
+
     public DefaultModelBuilderFactory setModelProcessor(ModelProcessor 
modelProcessor) {
         this.modelProcessor = modelProcessor;
         return this;
@@ -201,6 +205,11 @@ public class DefaultModelBuilderFactory {
         return this;
     }
 
+    public DefaultModelBuilderFactory setRootLocator(RootLocator rootLocator) {
+        this.rootLocator = rootLocator;
+        return this;
+    }
+
     protected ModelProcessor newModelProcessor() {
         return new DefaultModelProcessor(newModelLocator(), newModelReader());
     }
@@ -227,7 +236,7 @@ public class DefaultModelBuilderFactory {
     }
 
     protected ProfileActivationFilePathInterpolator 
newProfileActivationFilePathInterpolator() {
-        return new ProfileActivationFilePathInterpolator(newPathTranslator());
+        return new ProfileActivationFilePathInterpolator(newPathTranslator(), 
newRootLocator());
     }
 
     protected UrlNormalizer newUrlNormalizer() {
@@ -238,10 +247,15 @@ public class DefaultModelBuilderFactory {
         return new DefaultPathTranslator();
     }
 
+    protected RootLocator newRootLocator() {
+        return new DefaultRootLocator();
+    }
+
     protected ModelInterpolator newModelInterpolator() {
         UrlNormalizer normalizer = newUrlNormalizer();
         PathTranslator pathTranslator = newPathTranslator();
-        return new StringVisitorModelInterpolator(pathTranslator, normalizer);
+        RootLocator rootLocator = newRootLocator();
+        return new StringVisitorModelInterpolator(pathTranslator, normalizer, 
rootLocator);
     }
 
     protected ModelVersionProcessor newModelVersionPropertiesProcessor() {
diff --git 
a/maven-model-builder/src/main/java/org/apache/maven/model/interpolation/AbstractStringBasedModelInterpolator.java
 
b/maven-model-builder/src/main/java/org/apache/maven/model/interpolation/AbstractStringBasedModelInterpolator.java
index 8e8483699..37e7b0236 100644
--- 
a/maven-model-builder/src/main/java/org/apache/maven/model/interpolation/AbstractStringBasedModelInterpolator.java
+++ 
b/maven-model-builder/src/main/java/org/apache/maven/model/interpolation/AbstractStringBasedModelInterpolator.java
@@ -21,6 +21,7 @@ package org.apache.maven.model.interpolation;
 import javax.inject.Inject;
 
 import java.io.File;
+import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -33,6 +34,7 @@ import org.apache.maven.model.building.ModelBuildingRequest;
 import org.apache.maven.model.building.ModelProblemCollector;
 import org.apache.maven.model.path.PathTranslator;
 import org.apache.maven.model.path.UrlNormalizer;
+import org.apache.maven.model.root.RootLocator;
 import org.codehaus.plexus.interpolation.AbstractValueSource;
 import org.codehaus.plexus.interpolation.InterpolationPostProcessor;
 import org.codehaus.plexus.interpolation.MapBasedValueSource;
@@ -74,10 +76,14 @@ public abstract class AbstractStringBasedModelInterpolator 
implements ModelInter
     private final PathTranslator pathTranslator;
     private final UrlNormalizer urlNormalizer;
 
+    private final RootLocator rootLocator;
+
     @Inject
-    public AbstractStringBasedModelInterpolator(PathTranslator pathTranslator, 
UrlNormalizer urlNormalizer) {
+    public AbstractStringBasedModelInterpolator(
+            PathTranslator pathTranslator, UrlNormalizer urlNormalizer, 
RootLocator rootLocator) {
         this.pathTranslator = pathTranslator;
         this.urlNormalizer = urlNormalizer;
+        this.rootLocator = rootLocator;
     }
 
     @Override
@@ -133,6 +139,20 @@ public abstract class AbstractStringBasedModelInterpolator 
implements ModelInter
             valueSources.add(new 
BuildTimestampValueSource(config.getBuildStartTime(), modelProperties));
         }
 
+        valueSources.add(new PrefixedValueSourceWrapper(
+                new AbstractValueSource(false) {
+                    @Override
+                    public Object getValue(String expression) {
+                        if ("rootDirectory".equals(expression)) {
+                            Path base = projectDir != null ? 
projectDir.toPath() : null;
+                            Path root = rootLocator.findMandatoryRoot(base);
+                            return root.toFile().getPath();
+                        }
+                        return null;
+                    }
+                },
+                PROJECT_PREFIXES));
+
         valueSources.add(projectPrefixValueSource);
 
         valueSources.add(new MapBasedValueSource(config.getUserProperties()));
diff --git 
a/maven-model-builder/src/main/java/org/apache/maven/model/interpolation/StringVisitorModelInterpolator.java
 
b/maven-model-builder/src/main/java/org/apache/maven/model/interpolation/StringVisitorModelInterpolator.java
index 88318f7b6..f2a511c03 100644
--- 
a/maven-model-builder/src/main/java/org/apache/maven/model/interpolation/StringVisitorModelInterpolator.java
+++ 
b/maven-model-builder/src/main/java/org/apache/maven/model/interpolation/StringVisitorModelInterpolator.java
@@ -35,6 +35,7 @@ import org.apache.maven.model.building.ModelProblemCollector;
 import org.apache.maven.model.building.ModelProblemCollectorRequest;
 import org.apache.maven.model.path.PathTranslator;
 import org.apache.maven.model.path.UrlNormalizer;
+import org.apache.maven.model.root.RootLocator;
 import org.apache.maven.model.v4.MavenTransformer;
 import org.codehaus.plexus.interpolation.InterpolationException;
 import org.codehaus.plexus.interpolation.InterpolationPostProcessor;
@@ -51,8 +52,9 @@ import org.codehaus.plexus.interpolation.ValueSource;
 @Singleton
 public class StringVisitorModelInterpolator extends 
AbstractStringBasedModelInterpolator {
     @Inject
-    public StringVisitorModelInterpolator(PathTranslator pathTranslator, 
UrlNormalizer urlNormalizer) {
-        super(pathTranslator, urlNormalizer);
+    public StringVisitorModelInterpolator(
+            PathTranslator pathTranslator, UrlNormalizer urlNormalizer, 
RootLocator rootLocator) {
+        super(pathTranslator, urlNormalizer, rootLocator);
     }
 
     interface InnerInterpolator {
diff --git 
a/maven-model-builder/src/main/java/org/apache/maven/model/path/ProfileActivationFilePathInterpolator.java
 
b/maven-model-builder/src/main/java/org/apache/maven/model/path/ProfileActivationFilePathInterpolator.java
index 3ec95ab45..1455b7fcc 100644
--- 
a/maven-model-builder/src/main/java/org/apache/maven/model/path/ProfileActivationFilePathInterpolator.java
+++ 
b/maven-model-builder/src/main/java/org/apache/maven/model/path/ProfileActivationFilePathInterpolator.java
@@ -23,9 +23,11 @@ import javax.inject.Named;
 import javax.inject.Singleton;
 
 import java.io.File;
+import java.nio.file.Path;
 
 import org.apache.maven.api.model.ActivationFile;
 import org.apache.maven.model.profile.ProfileActivationContext;
+import org.apache.maven.model.root.RootLocator;
 import org.codehaus.plexus.interpolation.AbstractValueSource;
 import org.codehaus.plexus.interpolation.InterpolationException;
 import org.codehaus.plexus.interpolation.MapBasedValueSource;
@@ -42,9 +44,12 @@ public class ProfileActivationFilePathInterpolator {
 
     private final PathTranslator pathTranslator;
 
+    private final RootLocator rootLocator;
+
     @Inject
-    public ProfileActivationFilePathInterpolator(PathTranslator 
pathTranslator) {
+    public ProfileActivationFilePathInterpolator(PathTranslator 
pathTranslator, RootLocator rootLocator) {
         this.pathTranslator = pathTranslator;
+        this.rootLocator = rootLocator;
     }
 
     /**
@@ -79,6 +84,18 @@ public class ProfileActivationFilePathInterpolator {
             return null;
         }
 
+        interpolator.addValueSource(new AbstractValueSource(false) {
+            @Override
+            public Object getValue(String expression) {
+                if ("rootDirectory".equals(expression)) {
+                    Path base = basedir != null ? basedir.toPath() : null;
+                    Path root = rootLocator.findMandatoryRoot(base);
+                    return root.toFile().getAbsolutePath();
+                }
+                return null;
+            }
+        });
+
         interpolator.addValueSource(new 
MapBasedValueSource(context.getProjectProperties()));
 
         interpolator.addValueSource(new 
MapBasedValueSource(context.getUserProperties()));
diff --git 
a/maven-model-builder/src/main/java/org/apache/maven/model/root/DefaultRootLocator.java
 
b/maven-model-builder/src/main/java/org/apache/maven/model/root/DefaultRootLocator.java
new file mode 100644
index 000000000..4b4a36a22
--- /dev/null
+++ 
b/maven-model-builder/src/main/java/org/apache/maven/model/root/DefaultRootLocator.java
@@ -0,0 +1,59 @@
+/*
+ * 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.model.root;
+
+import javax.inject.Named;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import org.codehaus.plexus.util.xml.pull.MXParser;
+import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
+
+@Named
+public class DefaultRootLocator implements RootLocator {
+
+    public boolean isRootDirectory(Path dir) {
+        if (Files.isDirectory(dir.resolve(".mvn"))) {
+            return true;
+        }
+        // we're too early to use the modelProcessor ...
+        Path pom = dir.resolve("pom.xml");
+        try (InputStream is = Files.newInputStream(pom)) {
+            MXParser parser = new MXParser();
+            parser.setInput(is, null);
+            if (parser.nextTag() == MXParser.START_TAG && 
parser.getName().equals("project")) {
+                for (int i = 0; i < parser.getAttributeCount(); i++) {
+                    if ("root".equals(parser.getAttributeName(i))) {
+                        return 
Boolean.parseBoolean(parser.getAttributeValue(i));
+                    }
+                }
+            }
+        } catch (IOException | XmlPullParserException e) {
+            // The root locator can be used very early during the setup of 
Maven,
+            // even before the arguments from the command line are parsed.  
Any exception
+            // that would happen here should cause the build to fail at a 
later stage
+            // (when actually parsing the POM) and will lead to a better 
exception being
+            // displayed to the user, so just bail out and return false.
+        }
+        return false;
+    }
+}
diff --git 
a/maven-model-builder/src/main/java/org/apache/maven/model/root/RootLocator.java
 
b/maven-model-builder/src/main/java/org/apache/maven/model/root/RootLocator.java
new file mode 100644
index 000000000..c1b458db7
--- /dev/null
+++ 
b/maven-model-builder/src/main/java/org/apache/maven/model/root/RootLocator.java
@@ -0,0 +1,69 @@
+/*
+ * 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.model.root;
+
+import java.nio.file.Path;
+
+import org.apache.maven.api.annotations.Nonnull;
+import org.apache.maven.api.annotations.Nullable;
+
+/**
+ * Interface used to locate the root directory for a given project.
+ *
+ * The root locator is usually looked up from the plexus container.
+ * One notable exception is the computation of the early {@code 
session.rootDirectory}
+ * property which happens very early.  The implementation used in this case
+ * will be discovered using the JDK service mechanism.
+ *
+ * The default implementation will look for a {@code .mvn} child directory
+ * or a {@code pom.xml} containing the {@code root="true"} attribute.
+ *
+ * @see DefaultRootLocator
+ */
+public interface RootLocator {
+
+    String UNABLE_TO_FIND_ROOT_PROJECT_MESSAGE = "Unable to find the root 
directory. "
+            + "Create a .mvn directory in the root directory or add the 
root=\"true\""
+            + " attribute on the root project's model to identify it.";
+
+    @Nonnull
+    default Path findMandatoryRoot(Path basedir) {
+        Path rootDirectory = findRoot(basedir);
+        if (rootDirectory == null) {
+            throw new IllegalStateException(getNoRootMessage());
+        }
+        return rootDirectory;
+    }
+
+    @Nullable
+    default Path findRoot(Path basedir) {
+        Path rootDirectory = basedir;
+        while (rootDirectory != null && !isRootDirectory(rootDirectory)) {
+            rootDirectory = rootDirectory.getParent();
+        }
+        return rootDirectory;
+    }
+
+    @Nonnull
+    default String getNoRootMessage() {
+        return UNABLE_TO_FIND_ROOT_PROJECT_MESSAGE;
+    }
+
+    boolean isRootDirectory(Path dir);
+}
diff --git 
a/maven-model-builder/src/main/resources/META-INF/services/org.apache.maven.model.root.RootLocator
 
b/maven-model-builder/src/main/resources/META-INF/services/org.apache.maven.model.root.RootLocator
new file mode 100644
index 000000000..4b81cf5c8
--- /dev/null
+++ 
b/maven-model-builder/src/main/resources/META-INF/services/org.apache.maven.model.root.RootLocator
@@ -0,0 +1 @@
+org.apache.maven.model.root.DefaultRootLocator
diff --git a/maven-model-builder/src/site/apt/index.apt 
b/maven-model-builder/src/site/apt/index.apt
index 270d2a8eb..38faeff9b 100644
--- a/maven-model-builder/src/site/apt/index.apt
+++ b/maven-model-builder/src/site/apt/index.apt
@@ -41,7 +41,7 @@ Maven Model Builder
 
    ** profile activation: see 
{{{./apidocs/org/apache/maven/model/profile/activation/package-summary.html}available
 activators}}.
    Notice that model interpolation hasn't happened yet, then interpolation for 
file-based activation is limited to
-   <<<$\{basedir}>>> (since Maven 3), system properties and user properties
+   <<<$\{basedir}>>> (since Maven 3), <<<$\{rootDirectory}>>> (since Maven 4), 
system properties and user properties
 
    ** file model validation: <<<ModelValidator>>> 
({{{./apidocs/org/apache/maven/model/validation/ModelValidator.html}javadoc}}),
    with its <<<DefaultModelValidator>>> implementation
@@ -51,7 +51,7 @@ Maven Model Builder
 
  * phase 2, with optional plugin processing
 
-   ** Build up a raw model by re-reading the file and enrich it based on 
information available in the reactor. Some features:
+   ** Build up a raw model by re-reading the file and enriching it based on 
information available in the reactor. Some features:
 
       *** Resolve version of versionless parents based on relativePath 
(including ci-friendly versions)
 
@@ -156,13 +156,13 @@ Maven Model Builder
 
 * Model Interpolation
 
-  Model Interpolation consists in replacing <<<$\{...\}>>> with calculated 
value. It is done in <<<StringSearchModelInterpolator>>>
-  
({{{./apidocs/org/apache/maven/model/interpolation/StringSearchModelInterpolator.html}javadoc}},
-  
{{{./xref/org/apache/maven/model/interpolation/StringSearchModelInterpolator.html}source}}).
+  Model Interpolation consists in replacing <<<$\{...\}>>> with calculated 
value. It is done in <<<StringVisitorModelInterpolator>>>
+  
({{{./apidocs/org/apache/maven/model/interpolation/StringVisitorModelInterpolator.html}javadoc}},
+  
{{{./xref/org/apache/maven/model/interpolation/StringVisitorModelInterpolator.html}source}}).
 
-  Notice that model interpolation happens <after> profile activation, then 
profile activation doesn't benefit from every values:
+  Notice that model interpolation happens <after> profile activation, and that 
profile activation doesn't benefit from every values:
   interpolation for file-based activation is limited to <<<$\{basedir}>>> 
(which was introduced in Maven 3 and is not deprecated
-  in this context), system properties and user properties.
+  in this context) and <<<$\{rootDirectory}>>> (introduced in Maven 4), system 
properties and user properties.
 
   Values are evaluated in sequence from different syntaxes:
 
@@ -183,6 +183,8 @@ Maven Model Builder
 | <<<project.baseUri>>>\
 <<<pom.baseUri>>> (<deprecated>) | the directory containing the <<<pom.xml>>> 
file as URI | <<<$\{project.baseUri\}>>> |
 *----+------+------+
+| <<<project.rootDirectory>>> | the project's root directory (containing a 
<<<.mvn>>> directory or with the <<<root="true">>> xml attribute) | 
<<<$\{project.rootDirectory\}>>> |
+*----+------+------+
 | <<<build.timestamp>>>\
 <<<maven.build.timestamp>>> | the UTC timestamp of build start, in 
<<<yyyy-MM-dd'T'HH:mm:ss'Z'>>> default format, which can be overridden with 
<<<maven.build.timestamp.format>>> POM property | 
<<<$\{maven.build.timestamp\}>>> |
 *----+------+------+
diff --git 
a/maven-model-builder/src/test/java/org/apache/maven/model/interpolation/AbstractModelInterpolatorTest.java
 
b/maven-model-builder/src/test/java/org/apache/maven/model/interpolation/AbstractModelInterpolatorTest.java
index b50789efa..44a2a59ef 100644
--- 
a/maven-model-builder/src/test/java/org/apache/maven/model/interpolation/AbstractModelInterpolatorTest.java
+++ 
b/maven-model-builder/src/test/java/org/apache/maven/model/interpolation/AbstractModelInterpolatorTest.java
@@ -19,6 +19,8 @@
 package org.apache.maven.model.interpolation;
 
 import java.io.File;
+import java.nio.file.Path;
+import java.nio.file.Paths;
 import java.text.SimpleDateFormat;
 import java.util.Arrays;
 import java.util.Calendar;
@@ -41,11 +43,13 @@ import org.apache.maven.api.model.Scm;
 import org.apache.maven.model.building.DefaultModelBuildingRequest;
 import org.apache.maven.model.building.ModelBuildingRequest;
 import org.apache.maven.model.building.SimpleProblemCollector;
+import org.apache.maven.model.root.RootLocator;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 /**
@@ -306,9 +310,50 @@ public abstract class AbstractModelInterpolatorTest {
         assertEquals("myBaseUri/temp-repo", 
(out.getRepositories().get(0)).getUrl());
     }
 
+    @Test
+    void testRootDirectory() throws Exception {
+        Path rootDirectory = Paths.get("myRootDirectory");
+
+        Model model = Model.newBuilder()
+                .version("3.8.1")
+                .artifactId("foo")
+                .repositories(Collections.singletonList(Repository.newBuilder()
+                        .url("file:${project.rootDirectory}/temp-repo")
+                        .build()))
+                .build();
+
+        ModelInterpolator interpolator = createInterpolator();
+
+        final SimpleProblemCollector collector = new SimpleProblemCollector();
+        Model out = interpolator.interpolateModel(
+                model, rootDirectory.toFile(), 
createModelBuildingRequest(context), collector);
+        assertProblemFree(collector);
+
+        assertEquals("file:myRootDirectory/temp-repo", 
(out.getRepositories().get(0)).getUrl());
+    }
+
+    @Test
+    void testRootDirectoryWithNull() throws Exception {
+        Model model = Model.newBuilder()
+                .version("3.8.1")
+                .artifactId("foo")
+                .repositories(Collections.singletonList(Repository.newBuilder()
+                        .url("file:///${project.rootDirectory}/temp-repo")
+                        .build()))
+                .build();
+
+        ModelInterpolator interpolator = createInterpolator();
+
+        final SimpleProblemCollector collector = new SimpleProblemCollector();
+        IllegalStateException e = assertThrows(
+                IllegalStateException.class,
+                () -> interpolator.interpolateModel(model, null, 
createModelBuildingRequest(context), collector));
+
+        assertEquals(RootLocator.UNABLE_TO_FIND_ROOT_PROJECT_MESSAGE, 
e.getMessage());
+    }
+
     @Test
     public void testEnvars() throws Exception {
-        Properties context = new Properties();
         context.put("env.HOME", "/path/to/home");
 
         Map<String, String> modelProperties = new HashMap<>();
diff --git 
a/maven-model-builder/src/test/java/org/apache/maven/model/interpolation/StringVisitorModelInterpolatorTest.java
 
b/maven-model-builder/src/test/java/org/apache/maven/model/interpolation/StringVisitorModelInterpolatorTest.java
index 5c29e8708..02ea9ba6b 100644
--- 
a/maven-model-builder/src/test/java/org/apache/maven/model/interpolation/StringVisitorModelInterpolatorTest.java
+++ 
b/maven-model-builder/src/test/java/org/apache/maven/model/interpolation/StringVisitorModelInterpolatorTest.java
@@ -20,6 +20,6 @@ package org.apache.maven.model.interpolation;
 
 public class StringVisitorModelInterpolatorTest extends 
AbstractModelInterpolatorTest {
     protected ModelInterpolator createInterpolator() {
-        return new StringVisitorModelInterpolator(null, null);
+        return new StringVisitorModelInterpolator(null, null, bd -> true);
     }
 }
diff --git 
a/maven-model-builder/src/test/java/org/apache/maven/model/profile/activation/FileProfileActivatorTest.java
 
b/maven-model-builder/src/test/java/org/apache/maven/model/profile/activation/FileProfileActivatorTest.java
index 39312bfb6..c2ac10bd1 100644
--- 
a/maven-model-builder/src/test/java/org/apache/maven/model/profile/activation/FileProfileActivatorTest.java
+++ 
b/maven-model-builder/src/test/java/org/apache/maven/model/profile/activation/FileProfileActivatorTest.java
@@ -28,11 +28,13 @@ import org.apache.maven.api.model.Profile;
 import org.apache.maven.model.path.DefaultPathTranslator;
 import org.apache.maven.model.path.ProfileActivationFilePathInterpolator;
 import org.apache.maven.model.profile.DefaultProfileActivationContext;
+import org.apache.maven.model.root.RootLocator;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.io.TempDir;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 
 /**
  * Tests {@link FileProfileActivator}.
@@ -49,9 +51,10 @@ class FileProfileActivatorTest extends 
AbstractProfileActivatorTest<FileProfileA
     @BeforeEach
     @Override
     void setUp() throws Exception {
-        activator = new FileProfileActivator(new 
ProfileActivationFilePathInterpolator(new DefaultPathTranslator()));
+        activator = new FileProfileActivator(
+                new ProfileActivationFilePathInterpolator(new 
DefaultPathTranslator(), bd -> true));
 
-        context.setProjectDirectory(new File(tempDir.toString()));
+        context.setProjectDirectory(tempDir.toFile());
 
         File file = new File(tempDir.resolve("file.txt").toString());
         if (!file.createNewFile()) {
@@ -59,6 +62,26 @@ class FileProfileActivatorTest extends 
AbstractProfileActivatorTest<FileProfileA
         }
     }
 
+    @Test
+    void testRootDirectoryWithNull() {
+        context.setProjectDirectory(null);
+
+        IllegalStateException e = assertThrows(
+                IllegalStateException.class,
+                () -> assertActivation(false, 
newExistsProfile("${rootDirectory}"), context));
+        assertEquals(RootLocator.UNABLE_TO_FIND_ROOT_PROJECT_MESSAGE, 
e.getMessage());
+    }
+
+    @Test
+    void testRootDirectory() {
+        assertActivation(false, 
newExistsProfile("${rootDirectory}/someFile.txt"), context);
+        assertActivation(true, 
newMissingProfile("${rootDirectory}/someFile.txt"), context);
+        assertActivation(true, newExistsProfile("${rootDirectory}"), context);
+        assertActivation(true, newExistsProfile("${rootDirectory}/" + 
"file.txt"), context);
+        assertActivation(false, newMissingProfile("${rootDirectory}"), 
context);
+        assertActivation(false, newMissingProfile("${rootDirectory}/" + 
"file.txt"), context);
+    }
+
     @Test
     void testIsActiveNoFile() {
         assertActivation(false, newExistsProfile(null), context);
diff --git 
a/maven-model-transform/src/main/java/org/apache/maven/model/transform/RawToConsumerPomXMLFilterFactory.java
 
b/maven-model-transform/src/main/java/org/apache/maven/model/transform/RawToConsumerPomXMLFilterFactory.java
index 2b700a564..617d7d91f 100644
--- 
a/maven-model-transform/src/main/java/org/apache/maven/model/transform/RawToConsumerPomXMLFilterFactory.java
+++ 
b/maven-model-transform/src/main/java/org/apache/maven/model/transform/RawToConsumerPomXMLFilterFactory.java
@@ -40,6 +40,8 @@ public class RawToConsumerPomXMLFilterFactory {
 
         parser = buildPomXMLFilterFactory.get(parser, projectPath);
 
+        // Remove root model attribute
+        parser = new RootXMLFilter(parser);
         // Strip modules
         parser = new ModulesXMLFilter(parser);
         // Adjust relativePath
diff --git 
a/maven-model-transform/src/main/java/org/apache/maven/model/transform/RawToConsumerPomXMLFilterFactory.java
 
b/maven-model-transform/src/main/java/org/apache/maven/model/transform/RootXMLFilter.java
similarity index 50%
copy from 
maven-model-transform/src/main/java/org/apache/maven/model/transform/RawToConsumerPomXMLFilterFactory.java
copy to 
maven-model-transform/src/main/java/org/apache/maven/model/transform/RootXMLFilter.java
index 2b700a564..af40c5c54 100644
--- 
a/maven-model-transform/src/main/java/org/apache/maven/model/transform/RawToConsumerPomXMLFilterFactory.java
+++ 
b/maven-model-transform/src/main/java/org/apache/maven/model/transform/RootXMLFilter.java
@@ -18,33 +18,36 @@
  */
 package org.apache.maven.model.transform;
 
-import java.nio.file.Path;
+import java.io.IOException;
+import java.util.stream.Stream;
 
+import org.apache.maven.model.transform.pull.BufferingParser;
 import org.codehaus.plexus.util.xml.pull.XmlPullParser;
+import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
 
 /**
+ * Remove the root attribute on the model
+ *
  * @author Guillaume Nodet
- * @author Robert Scholte
  * @since 4.0.0
  */
-public class RawToConsumerPomXMLFilterFactory {
-    private BuildToRawPomXMLFilterFactory buildPomXMLFilterFactory;
-
-    public RawToConsumerPomXMLFilterFactory(BuildToRawPomXMLFilterFactory 
buildPomXMLFilterFactory) {
-        this.buildPomXMLFilterFactory = buildPomXMLFilterFactory;
+class RootXMLFilter extends BufferingParser {
+    RootXMLFilter(XmlPullParser xmlPullParser) {
+        super(xmlPullParser);
     }
 
-    public final XmlPullParser get(XmlPullParser orgParser, Path projectPath) {
-        // Ensure that xs:any elements aren't touched by next filters
-        XmlPullParser parser = orgParser instanceof FastForwardFilter ? 
orgParser : new FastForwardFilter(orgParser);
-
-        parser = buildPomXMLFilterFactory.get(parser, projectPath);
-
-        // Strip modules
-        parser = new ModulesXMLFilter(parser);
-        // Adjust relativePath
-        parser = new RelativePathXMLFilter(parser);
-
-        return parser;
+    @Override
+    protected boolean accept() throws XmlPullParserException, IOException {
+        if (xmlPullParser.getEventType() == XmlPullParser.START_TAG) {
+            if (xmlPullParser.getDepth() == 1 && 
"project".equals(xmlPullParser.getName())) {
+                Event event = bufferEvent();
+                event.attributes = Stream.of(event.attributes)
+                        .filter(a -> !"root".equals(a.name))
+                        .toArray(Attribute[]::new);
+                pushEvent(event);
+                return false;
+            }
+        }
+        return true;
     }
 }
diff --git 
a/maven-model-transform/src/main/java/org/apache/maven/model/transform/pull/BufferingParser.java
 
b/maven-model-transform/src/main/java/org/apache/maven/model/transform/pull/BufferingParser.java
index 4113771e4..a1aba62ba 100644
--- 
a/maven-model-transform/src/main/java/org/apache/maven/model/transform/pull/BufferingParser.java
+++ 
b/maven-model-transform/src/main/java/org/apache/maven/model/transform/pull/BufferingParser.java
@@ -395,6 +395,16 @@ public class BufferingParser implements XmlPullParser {
                 event.prefix = pp.getPrefix();
                 event.empty = pp.isEmptyElementTag();
                 event.text = pp.getText();
+                event.attributes = new Attribute[pp.getAttributeCount()];
+                for (int i = 0; i < pp.getAttributeCount(); i++) {
+                    Attribute attr = new Attribute();
+                    attr.name = pp.getAttributeName(i);
+                    attr.namespace = pp.getAttributeNamespace(i);
+                    attr.value = pp.getAttributeValue(i);
+                    attr.type = pp.getAttributeType(i);
+                    attr.isDefault = pp.isAttributeDefault(i);
+                    event.attributes[i] = attr;
+                }
                 break;
             case END_TAG:
                 event.name = pp.getName();
diff --git a/src/mdo/reader-ex.vm b/src/mdo/reader-ex.vm
index a5b7c7e14..a061e1b36 100644
--- a/src/mdo/reader-ex.vm
+++ b/src/mdo/reader-ex.vm
@@ -207,7 +207,7 @@ public class ${className}
       #if ( $field.type == "String" )
                 ${classLcapName}.${field.name}( interpolatedTrimmed( value, 
"$fieldTagName" ) );
       #elseif ( $field.type == "boolean" || $field.type == "Boolean" )
-                ${classLcapName}.${field.name}( getBooleanValue( 
interpolatedTrimmed( value, "$fieldTagName" ), "$fieldTagName", parser, strict, 
"${field.defaultValue}" ) );
+                ${classLcapName}.${field.name}( getBooleanValue( 
interpolatedTrimmed( value, "$fieldTagName" ), "$fieldTagName", parser, 
${field.defaultValue} ) );
       #else
                 // TODO: type=${field.type} to=${field.to} 
multiplicity=${field.multiplicity}
       #end
diff --git a/src/mdo/reader-modified.vm b/src/mdo/reader-modified.vm
index 696cb1c91..be4f49506 100644
--- a/src/mdo/reader-modified.vm
+++ b/src/mdo/reader-modified.vm
@@ -236,7 +236,7 @@ public class ${className}
       #if ( $field.type == "String" )
                 ${classLcapName}.${field.name}( interpolatedTrimmed( value, 
"$fieldTagName" ) );
       #elseif ( $field.type == "boolean" || $field.type == "Boolean" )
-                ${classLcapName}.${field.name}( getBooleanValue( 
interpolatedTrimmed( value, "$fieldTagName" ), "$fieldTagName", parser, 
"${field.defaultValue}" ) );
+                ${classLcapName}.${field.name}( getBooleanValue( 
interpolatedTrimmed( value, "$fieldTagName" ), "$fieldTagName", parser, 
${field.defaultValue} ) );
       #else
                 // TODO: type=${field.type} to=${field.to} 
multiplicity=${field.multiplicity}
       #end
diff --git a/src/mdo/reader.vm b/src/mdo/reader.vm
index f43847aaf..8f04323b2 100644
--- a/src/mdo/reader.vm
+++ b/src/mdo/reader.vm
@@ -175,7 +175,7 @@ public class ${className}
     private boolean getBooleanValue( String s, String attribute, XmlPullParser 
parser )
         throws XmlPullParserException
     {
-        return getBooleanValue( s, attribute, parser, null );
+        return getBooleanValue( s, attribute, parser, false );
     } //-- boolean getBooleanValue( String, String, XmlPullParser )
 
     /**
@@ -189,18 +189,14 @@ public class ${className}
      * any.
      * @return boolean
      */
-    private boolean getBooleanValue( String s, String attribute, XmlPullParser 
parser, String defaultValue )
+    private boolean getBooleanValue( String s, String attribute, XmlPullParser 
parser, boolean defaultValue )
         throws XmlPullParserException
     {
         if ( s != null && s.length() != 0 )
         {
             return Boolean.valueOf( s ).booleanValue();
         }
-        if ( defaultValue != null )
-        {
-            return Boolean.valueOf( defaultValue ).booleanValue();
-        }
-        return false;
+        return defaultValue;
     } //-- boolean getBooleanValue( String, String, XmlPullParser, String )
 
     /**
@@ -709,7 +705,7 @@ public class ${className}
       #if ( $field.type == "String" )
                 ${classLcapName}.${field.name}( interpolatedTrimmed( value, 
"$fieldTagName" ) );
       #elseif ( $field.type == "boolean" || $field.type == "Boolean" )
-                ${classLcapName}.${field.name}( getBooleanValue( 
interpolatedTrimmed( value, "$fieldTagName" ), "$fieldTagName", parser, 
"${field.defaultValue}" ) );
+                ${classLcapName}.${field.name}( getBooleanValue( 
interpolatedTrimmed( value, "$fieldTagName" ), "$fieldTagName", parser, 
${field.defaultValue} ) );
       #else
                 // TODO: type=${field.type} to=${field.to} 
multiplicity=${field.multiplicity}
       #end
@@ -749,7 +745,7 @@ public class ${className}
                     ${classLcapName}.${field.name}( interpolatedTrimmed( 
parser.nextText(), "${fieldTagName}" ) );
                     break;
       #elseif ( $field.type == "boolean" || $field.type == "Boolean" )
-                    ${classLcapName}.${field.name}( getBooleanValue( 
interpolatedTrimmed( parser.nextText(), "${fieldTagName}" ), "${fieldTagName}", 
parser, "${field.defaultValue}" ) );
+                    ${classLcapName}.${field.name}( getBooleanValue( 
interpolatedTrimmed( parser.nextText(), "${fieldTagName}" ), "${fieldTagName}", 
parser, ${field.defaultValue} ) );
                     break;
       #elseif ( $field.type == "int" )
                     ${classLcapName}.${field.name}( getIntegerValue( 
interpolatedTrimmed( parser.nextText(), "${fieldTagName}" ), "${fieldTagName}", 
parser, strict, ${field.defaultValue} ) );
diff --git a/src/mdo/writer-ex.vm b/src/mdo/writer-ex.vm
index a204149c3..903184ce8 100644
--- a/src/mdo/writer-ex.vm
+++ b/src/mdo/writer-ex.vm
@@ -210,8 +210,15 @@ public class ${className}
       #set ( $fieldCapName = $Helper.capitalise( $field.name ) )
       #if ( $field.type == "String" )
             writeAttr( "$fieldTagName", ${classLcapName}.get${fieldCapName}(), 
serializer );
+      #elseif ( $field.type == "boolean" )
+        #set ( $def = ${field.defaultValue} )
+        #if ( ${def} == "true" )
+            writeAttr( "$fieldTagName", ${classLcapName}.is${fieldCapName}() ? 
null : "false", serializer );
+        #else
+            writeAttr( "$fieldTagName", ${classLcapName}.is${fieldCapName}() ? 
"true" : null, serializer );
+        #end
       #else
-        // TODO: type=${field.type} to=${field.to} 
multiplicity=${field.multiplicity}
+            // TODO: type=${field.type} to=${field.to} 
multiplicity=${field.multiplicity}
       #end
     #end
   #end
diff --git a/src/mdo/writer.vm b/src/mdo/writer.vm
index e968ec1a9..a589b603f 100644
--- a/src/mdo/writer.vm
+++ b/src/mdo/writer.vm
@@ -192,8 +192,15 @@ public class ${className}
       #set ( $fieldCapName = $Helper.capitalise( $field.name ) )
       #if ( $field.type == "String" )
             writeAttr( "$fieldTagName", ${classLcapName}.get${fieldCapName}(), 
serializer );
+      #elseif ( $field.type == "boolean" )
+        #set ( $def = ${field.defaultValue} )
+        #if ( ${def} == "true" )
+            writeAttr( "$fieldTagName", ${classLcapName}.is${fieldCapName}() ? 
null : "false", serializer );
+        #else
+            writeAttr( "$fieldTagName", ${classLcapName}.is${fieldCapName}() ? 
"true" : null, serializer );
+        #end
       #else
-        // TODO: type=${field.type} to=${field.to} 
multiplicity=${field.multiplicity}
+            // TODO: type=${field.type} to=${field.to} 
multiplicity=${field.multiplicity}
       #end
     #end
   #end


Reply via email to