From ce50ee78bf2f9adb21a617b08636ff8f1ee78344 Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Thu, 20 Apr 2023 10:57:49 +0200 Subject: [PATCH] [MNG-7038] Introduce public properties to point to the root and top directories of (multi-module) project 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 current directory if there's no such argument. * 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. --- .../java/org/apache/maven/api/Project.java | 37 ++ .../java/org/apache/maven/api/Session.java | 29 +- api/maven-api-model/src/main/mdo/maven.mdo | 13 + .../maven/project/TestProjectBuilder.java | 7 +- .../DefaultMavenExecutionRequest.java | 33 ++ .../execution/MavenExecutionRequest.java | 47 ++ .../apache/maven/execution/MavenSession.java | 21 + .../maven/internal/impl/DefaultProject.java | 16 + .../maven/internal/impl/DefaultSession.java | 13 +- .../ConsumerPomArtifactTransformer.java | 3 + .../maven/project/DefaultProjectBuilder.java | 14 +- .../apache/maven/project/MavenProject.java | 20 + .../internal/impl/DefaultSessionTest.java | 62 +++ ...luginParameterExpressionEvaluatorTest.java | 32 ++ ...ginParameterExpressionEvaluatorV4Test.java | 468 ++++++++++++++++++ .../apache/maven/cli/CLIReportingUtils.java | 7 + .../java/org/apache/maven/cli/CliRequest.java | 5 + .../java/org/apache/maven/cli/MavenCli.java | 54 ++ .../org/apache/maven/cli/MavenCliTest.java | 9 + .../projects/root-attribute/child/pom.xml | 3 + .../src/test/projects/root-attribute/pom.xml | 3 + .../building/DefaultModelBuilderFactory.java | 18 +- .../AbstractStringBasedModelInterpolator.java | 22 +- .../StringVisitorModelInterpolator.java | 6 +- ...ProfileActivationFilePathInterpolator.java | 19 +- .../maven/model/root/DefaultRootLocator.java | 59 +++ .../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 + .../maven/model/transform/RootXMLFilter.java | 53 ++ .../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, 1246 insertions(+), 37 deletions(-) create mode 100644 maven-core/src/test/java/org/apache/maven/internal/impl/DefaultSessionTest.java create mode 100644 maven-core/src/test/java/org/apache/maven/plugin/PluginParameterExpressionEvaluatorV4Test.java create mode 100644 maven-embedder/src/test/projects/root-attribute/child/pom.xml create mode 100644 maven-embedder/src/test/projects/root-attribute/pom.xml create mode 100644 maven-model-builder/src/main/java/org/apache/maven/model/root/DefaultRootLocator.java create mode 100644 maven-model-builder/src/main/java/org/apache/maven/model/root/RootLocator.java create mode 100644 maven-model-builder/src/main/resources/META-INF/services/org.apache.maven.model.root.RootLocator create mode 100644 maven-model-transform/src/main/java/org/apache/maven/model/transform/RootXMLFilter.java 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 7d80f3ae4709..256fe9479ed4 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 @@ default String getId() { 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 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 5616c3dd93f8..32baea23e11e 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. * - * Implementation note: while this method return type is {@link Map}, the returned map instance - * implements {@link java.util.concurrent.ConcurrentMap} as well. + * Implementation note: 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 a42a655314ee..fc8418fb6def 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 @@ String + + root + 4.0.0+ + + Since: Maven 4.0.0 + ]]> + + boolean + false + inceptionYear 3.0.0+ 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 6a3f15509171..612d8e7dd711 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.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 TestProjectBuilder( RepositorySystem repoSystem, RemoteRepositoryManager repositoryManager, ProjectDependenciesResolver dependencyResolver, - ModelCacheFactory modelCacheFactory) { + ModelCacheFactory modelCacheFactory, + RootLocator rootLocator) { super( modelBuilder, modelProcessor, @@ -54,7 +56,8 @@ public TestProjectBuilder( 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 e0a073b91b25..310574f379f7 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 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 goals; private boolean useReactor = false; @@ -1051,16 +1057,43 @@ public MavenExecutionRequest setToolchains(Map> too 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 9d86df5e8bec..66a49a1f20bf 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 bd4e34ad522d..fc430696e9f0 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 List getProjects() { 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 0951f26f8510..c234608ffbd7 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 boolean isExecutionRoot() { 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 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 05a72ff97347..011710778da2 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 Path getMultiModuleProjectDirectory() { @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 1b6c5a8d0c94..eabbe03f7ac5 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 void injectTransformedArtifacts(MavenProject project, RepositorySystemSes 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 5bb9dab0f233..027ca74ee6d0 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.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 DefaultProjectBuilder( 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 DefaultProjectBuilder( this.repositoryManager = repositoryManager; this.dependencyResolver = dependencyResolver; this.modelCacheFactory = modelCacheFactory; + this.rootLocator = rootLocator; } // ---------------------------------------------------------------------- // MavenProjectBuilder Implementation @@ -162,6 +167,11 @@ private ProjectBuildingResult build(File pomFile, ModelSource modelSource, Inter 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 @@ private boolean build( 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 b024001bf189..84a4090b23ca 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 @@ 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.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 resolvedArtifacts; private ArtifactFilter artifactFilter; @@ -1679,4 +1683,20 @@ public ProjectBuildingRequest getProjectBuildingRequest() { 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 000000000000..dd516c91a34a --- /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 39db5828af00..c01c6f56c359 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 @@ 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.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 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 @@ void testShouldExtractPluginArtifacts() throws Exception { 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 @@ private ExpressionEvaluator createExpressionEvaluator( 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/PluginParameterExpressionEvaluatorV4Test.java b/maven-core/src/test/java/org/apache/maven/plugin/PluginParameterExpressionEvaluatorV4Test.java new file mode 100644 index 000000000000..aee3309830c3 --- /dev/null +++ b/maven-core/src/test/java/org/apache/maven/plugin/PluginParameterExpressionEvaluatorV4Test.java @@ -0,0 +1,468 @@ +/* + * 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.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; +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; +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; +import org.apache.maven.project.MavenProject; +import org.apache.maven.repository.RepositorySystem; +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 + */ +public class PluginParameterExpressionEvaluatorV4Test extends AbstractCoreMavenComponentTestCase { + private static final String FS = File.separator; + + @Inject + private RepositorySystem factory; + + private Path rootDirectory; + + @Test + public void testPluginDescriptorExpressionReference() throws Exception { + MojoExecution exec = newMojoExecution(); + + Session session = newSession(); + + Object result = new PluginParameterExpressionEvaluatorV4(session, null, exec).evaluate("${plugin}"); + + System.out.println("Result: " + result); + + assertSame( + exec.getMojoDescriptor().getPluginDescriptor(), + result, + "${plugin} expression does not return plugin descriptor."); + } + + @Test + public void testPluginArtifactsExpressionReference() throws Exception { + MojoExecution exec = newMojoExecution(); + + Artifact depArtifact = createArtifact("group", "artifact", "1"); + + List deps = new ArrayList<>(); + deps.add(depArtifact); + + exec.getMojoDescriptor().getPluginDescriptor().setArtifacts(deps); + + Session session = newSession(); + + @SuppressWarnings("unchecked") + List depResults = (List) + new PluginParameterExpressionEvaluatorV4(session, null, exec).evaluate("${plugin.artifacts}"); + + System.out.println("Result: " + depResults); + + assertNotNull(depResults); + assertEquals(1, depResults.size()); + assertSame(depArtifact, depResults.get(0), "dependency artifact is wrong."); + } + + @Test + public void testPluginArtifactMapExpressionReference() throws Exception { + MojoExecution exec = newMojoExecution(); + + Artifact depArtifact = createArtifact("group", "artifact", "1"); + + List deps = new ArrayList<>(); + deps.add(depArtifact); + + exec.getMojoDescriptor().getPluginDescriptor().setArtifacts(deps); + + Session session = newSession(); + + @SuppressWarnings("unchecked") + Map depResults = (Map) + new PluginParameterExpressionEvaluatorV4(session, null, exec).evaluate("${plugin.artifactMap}"); + + System.out.println("Result: " + depResults); + + assertNotNull(depResults); + assertEquals(1, depResults.size()); + assertSame( + depArtifact, + depResults.get(ArtifactUtils.versionlessKey(depArtifact)), + "dependency artifact is wrong."); + } + + @Test + public void testPluginArtifactIdExpressionReference() throws Exception { + MojoExecution exec = newMojoExecution(); + + Session session = newSession(); + + Object result = new PluginParameterExpressionEvaluatorV4(session, null, exec).evaluate("${plugin.artifactId}"); + + System.out.println("Result: " + result); + + assertSame( + exec.getMojoDescriptor().getPluginDescriptor().getArtifactId(), + result, + "${plugin.artifactId} expression does not return plugin descriptor's artifactId."); + } + + @Test + public void testValueExtractionWithAPomValueContainingAPath() throws Exception { + String expected = getTestFile("target/test-classes/target/classes").getCanonicalPath(); + + Build build = new Build(); + build.setDirectory(expected.substring(0, expected.length() - "/classes".length())); + + Model model = new Model(); + model.setBuild(build); + + MavenProject project = new MavenProject(model); + project.setFile(new File("pom.xml").getCanonicalFile()); + + ExpressionEvaluator expressionEvaluator = createExpressionEvaluator(project, null, new Properties()); + + Object value = expressionEvaluator.evaluate("${project.build.directory}/classes"); + String actual = new File(value.toString()).getCanonicalPath(); + + assertEquals(expected, actual); + } + + @Test + public void testEscapedVariablePassthrough() throws Exception { + String var = "${var}"; + + Model model = new Model(); + model.setVersion("1"); + + MavenProject project = new MavenProject(model); + + ExpressionEvaluator ee = createExpressionEvaluator(project, null, new Properties()); + + Object value = ee.evaluate("$" + var); + + assertEquals(var, value); + } + + @Test + public void testEscapedVariablePassthroughInLargerExpression() throws Exception { + String var = "${var}"; + String key = var + " with version: ${project.version}"; + + Model model = new Model(); + model.setVersion("1"); + + MavenProject project = new MavenProject(model); + + ExpressionEvaluator ee = createExpressionEvaluator(project, null, new Properties()); + + Object value = ee.evaluate("$" + key); + + assertEquals("${var} with version: 1", value); + } + + @Test + public void testMultipleSubExpressionsInLargerExpression() throws Exception { + String key = "${project.artifactId} with version: ${project.version}"; + + Model model = new Model(); + model.setArtifactId("test"); + model.setVersion("1"); + + MavenProject project = new MavenProject(model); + + ExpressionEvaluator ee = createExpressionEvaluator(project, null, new Properties()); + + Object value = ee.evaluate(key); + + assertEquals("test with version: 1", value); + } + + @Test + public void testMissingPOMPropertyRefInLargerExpression() throws Exception { + String expr = "/path/to/someproject-${baseVersion}"; + + MavenProject project = new MavenProject(new Model()); + + ExpressionEvaluator ee = createExpressionEvaluator(project, null, new Properties()); + + Object value = ee.evaluate(expr); + + assertEquals(expr, value); + } + + @Test + public void testPOMPropertyExtractionWithMissingProject_WithDotNotation() throws Exception { + String key = "m2.name"; + String checkValue = "value"; + + Properties properties = new Properties(); + properties.setProperty(key, checkValue); + + Model model = new Model(); + model.setProperties(properties); + + MavenProject project = new MavenProject(model); + + ExpressionEvaluator ee = createExpressionEvaluator(project, null, new Properties()); + + Object value = ee.evaluate("${" + key + "}"); + + assertEquals(checkValue, value); + } + + @Test + public void testBasedirExtractionWithMissingProject() throws Exception { + ExpressionEvaluator ee = createExpressionEvaluator(null, null, new Properties()); + + Object value = ee.evaluate("${basedir}"); + + assertEquals(System.getProperty("user.dir"), value); + } + + @Test + public void testValueExtractionFromSystemPropertiesWithMissingProject() throws Exception { + String sysprop = "PPEET_sysprop1"; + + Properties executionProperties = new Properties(); + + if (executionProperties.getProperty(sysprop) == null) { + executionProperties.setProperty(sysprop, "value"); + } + + ExpressionEvaluator ee = createExpressionEvaluator(null, null, executionProperties); + + Object value = ee.evaluate("${" + sysprop + "}"); + + assertEquals("value", value); + } + + @Test + public void testValueExtractionFromSystemPropertiesWithMissingProject_WithDotNotation() throws Exception { + String sysprop = "PPEET.sysprop2"; + + Properties executionProperties = new Properties(); + + if (executionProperties.getProperty(sysprop) == null) { + executionProperties.setProperty(sysprop, "value"); + } + + ExpressionEvaluator ee = createExpressionEvaluator(null, null, executionProperties); + + Object value = ee.evaluate("${" + sysprop + "}"); + + assertEquals("value", value); + } + + @SuppressWarnings("deprecation") + private static MavenSession createSession(PlexusContainer container, ArtifactRepository repo, Properties properties) + throws CycleDetectedException, DuplicateProjectException, NoLocalRepositoryManagerException { + MavenExecutionRequest request = new DefaultMavenExecutionRequest() + .setSystemProperties(properties) + .setGoals(Collections.emptyList()) + .setBaseDirectory(new File("")) + .setLocalRepository(repo); + + 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.emptyList()); + return session; + } + + @Test + public void testLocalRepositoryExtraction() throws Exception { + ExpressionEvaluator expressionEvaluator = + createExpressionEvaluator(createDefaultProject(), null, new Properties()); + Object value = expressionEvaluator.evaluate("${localRepository}"); + + assertEquals("local", ((org.apache.maven.api.LocalRepository) value).getId()); + } + + @Test + public void testTwoExpressions() throws Exception { + Build build = new Build(); + build.setDirectory("expected-directory"); + build.setFinalName("expected-finalName"); + + Model model = new Model(); + model.setBuild(build); + + ExpressionEvaluator expressionEvaluator = + createExpressionEvaluator(new MavenProject(model), null, new Properties()); + + Object value = expressionEvaluator.evaluate("${project.build.directory}" + FS + "${project.build.finalName}"); + + assertEquals("expected-directory" + File.separatorChar + "expected-finalName", value); + } + + @Test + public void testShouldExtractPluginArtifacts() throws Exception { + PluginDescriptor pd = new PluginDescriptor(); + + Artifact artifact = createArtifact("testGroup", "testArtifact", "1.0"); + + pd.setArtifacts(Collections.singletonList(artifact)); + + ExpressionEvaluator ee = createExpressionEvaluator(createDefaultProject(), pd, new Properties()); + + Object value = ee.evaluate("${plugin.artifacts}"); + + assertTrue(value instanceof List); + + @SuppressWarnings("unchecked") + List artifacts = (List) value; + + assertEquals(1, artifacts.size()); + + Artifact result = artifacts.get(0); + + 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()); + } + + private ExpressionEvaluator createExpressionEvaluator( + MavenProject project, PluginDescriptor pluginDescriptor, Properties executionProperties) throws Exception { + ArtifactRepository repo = factory.createDefaultLocalRepository(); + + MutablePlexusContainer container = (MutablePlexusContainer) getContainer(); + 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); + mojo.setGoal("goal"); + + MojoExecution mojoExecution = new MojoExecution(mojo); + + return new PluginParameterExpressionEvaluatorV4( + session, project != null ? new DefaultProject(session, project) : null, mojoExecution); + } + + protected Artifact createArtifact(String groupId, String artifactId, String version) throws Exception { + Dependency dependency = new Dependency(); + dependency.setGroupId(groupId); + dependency.setArtifactId(artifactId); + dependency.setVersion(version); + dependency.setType("jar"); + dependency.setScope("compile"); + + return factory.createDependencyArtifact(dependency); + } + + private MojoExecution newMojoExecution() { + PluginDescriptor pd = new PluginDescriptor(); + pd.setArtifactId("my-plugin"); + pd.setGroupId("org.myco.plugins"); + pd.setVersion("1"); + + MojoDescriptor md = new MojoDescriptor(); + md.setPluginDescriptor(pd); + + pd.addComponentDescriptor(md); + + 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); + } + + @Override + protected String getProjectsDirectory() { + // TODO Auto-generated method stub + return 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 bc879c640913..704212be5701 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 @@ static Properties getBuildProperties() { } 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 9a315f12ce72..29bff7b38ad0 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 d5fd812158cd..6e9ae1a0ca31 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.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.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.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 @@ void initialize(CliRequest cliRequest) throws ExitException { } } + // We need to locate the top level project which may be pointed at using + // the -f/--file option. However, the command line isn't parsed yet, so + // we need to iterate through the args to find it and act upon it. + Path topDirectory = Paths.get(cliRequest.workingDirectory); + boolean isAltFile = false; + for (String arg : cliRequest.args) { + if (isAltFile) { + // this is the argument following -f/--file + Path path = 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 @@ private MavenExecutionRequest populateRequest(CliRequest cliRequest, MavenExecut 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 2d58ebeb2782..eea6a87f97a1 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.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.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 @@ void populatePropertiesOverwrite() throws Exception { 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 000000000000..7e5b53c8e003 --- /dev/null +++ b/maven-embedder/src/test/projects/root-attribute/child/pom.xml @@ -0,0 +1,3 @@ + + + \ 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 000000000000..c44d4f0c7c7d --- /dev/null +++ b/maven-embedder/src/test/projects/root-attribute/pom.xml @@ -0,0 +1,3 @@ + + + \ 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 3ddecdc5700d..75bc40381a1e 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.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 DefaultModelBuilderFactory setVersionProcessor(ModelVersionProcessor vers return this; } + public DefaultModelBuilderFactory setRootLocator(RootLocator rootLocator) { + this.rootLocator = rootLocator; + return this; + } + protected ModelProcessor newModelProcessor() { return new DefaultModelProcessor(newModelLocator(), newModelReader()); } @@ -227,7 +236,7 @@ protected ProfileActivator[] newProfileActivators() { } protected ProfileActivationFilePathInterpolator newProfileActivationFilePathInterpolator() { - return new ProfileActivationFilePathInterpolator(newPathTranslator()); + return new ProfileActivationFilePathInterpolator(newPathTranslator(), newRootLocator()); } protected UrlNormalizer newUrlNormalizer() { @@ -238,10 +247,15 @@ protected PathTranslator newPathTranslator() { 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 8e84836998c2..37e7b02363ac 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 @@ 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.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 Object getValue(String expression) { 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 88318f7b65ef..f2a511c03f5a 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.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 @@ @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 3ec95ab455f7..1455b7fcc441 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.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 Object getValue(String expression) { 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 000000000000..4b4a36a22a0f --- /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 000000000000..c1b458db7ff0 --- /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 000000000000..4b81cf5c840e --- /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 270d2a8eb0e7..38faeff9b319 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: <<>> ({{{./apidocs/org/apache/maven/model/validation/ModelValidator.html}javadoc}}), with its <<>> 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 <<>> - ({{{./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 <<>> + ({{{./apidocs/org/apache/maven/model/interpolation/StringVisitorModelInterpolator.html}javadoc}}, + {{{./xref/org/apache/maven/model/interpolation/StringVisitorModelInterpolator.html}source}}). - Notice that model interpolation happens profile activation, then profile activation doesn't benefit from every values: + Notice that model interpolation happens 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 | <<>>\ <<>> () | the directory containing the <<>> file as URI | <<<$\{project.baseUri\}>>> | *----+------+------+ +| <<>> | the project's root directory (containing a <<<.mvn>>> directory or with the <<>> xml attribute) | <<<$\{project.rootDirectory\}>>> | +*----+------+------+ | <<>>\ <<>> | the UTC timestamp of build start, in <<>> default format, which can be overridden with <<>> 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 b50789efacd8..44a2a59ef575 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.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 void testBaseUri() throws Exception { 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 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 5c29e870839e..02ea9ba6bf0f 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 @@ 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 39312bfb6ac6..c2ac10bd1fd3 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.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 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 @@ void setUp() throws Exception { } } + @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 2b700a564af3..617d7d91f0d2 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 final XmlPullParser get(XmlPullParser orgParser, Path projectPath) { 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/RootXMLFilter.java b/maven-model-transform/src/main/java/org/apache/maven/model/transform/RootXMLFilter.java new file mode 100644 index 000000000000..af40c5c54dee --- /dev/null +++ b/maven-model-transform/src/main/java/org/apache/maven/model/transform/RootXMLFilter.java @@ -0,0 +1,53 @@ +/* + * 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.transform; + +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 + * @since 4.0.0 + */ +class RootXMLFilter extends BufferingParser { + RootXMLFilter(XmlPullParser xmlPullParser) { + super(xmlPullParser); + } + + @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 4113771e4082..a1aba62ba575 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 @@ protected Event bufferEvent() throws XmlPullParserException { 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 a5b7c7e14b87..a061e1b36322 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 696cb1c91b21..be4f49506802 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 f43847aafce3..8f04323b2aa3 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 a204149c3b3b..903184ce89a6 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 e968ec1a94d3..a589b603f641 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