Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 98 additions & 8 deletions api/maven-api-core/src/main/java/org/apache/maven/api/Project.java
Original file line number Diff line number Diff line change
Expand Up @@ -174,19 +174,109 @@ default Build getBuild() {
Path getBasedir();

/**
* Returns the directory where files generated by the build are placed.
* The directory depends on the scope:
*
* {@return the absolute path to the directory where files generated by the build are placed}
* <p>
* <strong>Purpose:</strong> This method provides the base output directory for a given scope,
* which serves as the destination for compiled classes, processed resources, and other generated files.
* The returned path is always absolute.
* </p>
* <p>
* <strong>Scope-based Directory Resolution:</strong>
* </p>
* <table class="striped">
* <caption>Output Directory by Scope</caption>
* <thead>
* <tr>
* <th>Scope Parameter</th>
* <th>Build Configuration</th>
* <th>Typical Path</th>
* <th>Contents</th>
* </tr>
* </thead>
* <tbody>
* <tr>
* <td>{@link ProjectScope#MAIN}</td>
* <td>{@code build.getOutputDirectory()}</td>
* <td>{@code target/classes}</td>
* <td>Compiled application classes and processed main resources</td>
* </tr>
* <tr>
* <td>{@link ProjectScope#TEST}</td>
* <td>{@code build.getTestOutputDirectory()}</td>
* <td>{@code target/test-classes}</td>
* <td>Compiled test classes and processed test resources</td>
* </tr>
* <tr>
* <td>{@code null} or other</td>
* <td>{@code build.getDirectory()}</td>
* <td>{@code target}</td>
* <td>Parent directory for all build outputs</td>
* </tr>
* </tbody>
* </table>
* <p>
* <strong>Role in {@link SourceRoot} Path Resolution:</strong>
* </p>
* <p>
* This method is the foundation for {@link SourceRoot#targetPath(Project)} path resolution.
* When a {@link SourceRoot} has a relative {@code targetPath}, it is resolved against the
* output directory returned by this method for the source root's scope. This ensures that:
* </p>
* <ul>
* <li>If {@link ProjectScope#MAIN}, returns the directory where compiled application classes are placed.</li>
* <li>If {@link ProjectScope#TEST}, returns the directory where compiled test classes are placed.</li>
* <li>Otherwise (including {@code null}), returns the parent directory where all generated files are placed.</li>
* <li>Main resources with {@code targetPath="META-INF"} are copied to {@code target/classes/META-INF}</li>
* <li>Test resources with {@code targetPath="test-data"} are copied to {@code target/test-classes/test-data}</li>
* <li>Resources without an explicit {@code targetPath} are copied to the root of the output directory</li>
* </ul>
* <p>
* <strong>Maven 3 Compatibility:</strong>
* </p>
* <p>
* This behavior maintains the Maven 3.x semantic where resource {@code targetPath} elements
* are resolved relative to the appropriate output directory ({@code project.build.outputDirectory}
* or {@code project.build.testOutputDirectory}), <strong>not</strong> the project base directory.
* </p>
* <p>
* In Maven 3, when a resource configuration specifies:
* </p>
* <pre>{@code
* <resource>
* <directory>src/main/resources</directory>
* <targetPath>META-INF/resources</targetPath>
* </resource>
* }</pre>
* <p>
* The maven-resources-plugin resolves {@code targetPath} as:
* {@code project.build.outputDirectory + "/" + targetPath}, which results in
* {@code target/classes/META-INF/resources}. This method provides the same base directory
* ({@code target/classes}) for Maven 4 API consumers.
* </p>
* <p>
* <strong>Example:</strong>
* </p>
* <pre>{@code
* Project project = ...; // project at /home/user/myproject
*
* // Get main output directory
* Path mainOutput = project.getOutputDirectory(ProjectScope.MAIN);
* // Result: /home/user/myproject/target/classes
*
* // Get test output directory
* Path testOutput = project.getOutputDirectory(ProjectScope.TEST);
* // Result: /home/user/myproject/target/test-classes
*
* // Get build directory
* Path buildDir = project.getOutputDirectory(null);
* // Result: /home/user/myproject/target
* }</pre>
*
* @param scope the scope of the generated files for which to get the directory, or {@code null} for all
* @return the output directory of files that are generated for the given scope
* @param scope the scope of the generated files for which to get the directory, or {@code null} for the build directory
* @return the absolute path to the output directory for the given scope
*
* @see SourceRoot#targetPath(Project)
* @see SourceRoot#targetPath()
* @see Build#getOutputDirectory()
* @see Build#getTestOutputDirectory()
* @see Build#getDirectory()
*/
@Nonnull
default Path getOutputDirectory(@Nullable ProjectScope scope) {
Expand Down
153 changes: 144 additions & 9 deletions api/maven-api-core/src/main/java/org/apache/maven/api/SourceRoot.java
Original file line number Diff line number Diff line change
Expand Up @@ -145,24 +145,159 @@ default Optional<Version> targetVersion() {

/**
* {@return an explicit target path, overriding the default value}
* When a target path is explicitly specified, the values of the {@link #module()} and {@link #targetVersion()}
* elements are not used for inferring the path (they are still used as compiler options however).
* It means that for scripts and resources, the files below the path specified by {@link #directory()}
* <p>
* <strong>Important:</strong> This method returns the target path <em>as specified in the configuration</em>,
* which may be relative or absolute. It does <strong>not</strong> perform any path resolution.
* For the fully resolved absolute path, use {@link #targetPath(Project)} instead.
* </p>
* <p>
* <strong>Return Value Semantics:</strong>
* </p>
* <ul>
* <li><strong>Empty Optional</strong> - No explicit target path was specified. Files should be copied
* to the root of the output directory (see {@link Project#getOutputDirectory(ProjectScope)}).</li>
* <li><strong>Relative Path</strong> (e.g., {@code Path.of("META-INF/resources")}) - The path is
* <em>intended to be resolved</em> relative to the output directory for this source root's {@link #scope()}.
* <ul>
* <li>For {@link ProjectScope#MAIN}: relative to {@code target/classes}</li>
* <li>For {@link ProjectScope#TEST}: relative to {@code target/test-classes}</li>
* </ul>
* The actual resolution is performed by {@link #targetPath(Project)}.</li>
* <li><strong>Absolute Path</strong> (e.g., {@code Path.of("/tmp/custom")}) - The path is used as-is
* without any resolution. Files will be copied to this exact location.</li>
* </ul>
* <p>
* <strong>Maven 3 Compatibility:</strong> This behavior maintains compatibility with Maven 3.x,
* where resource {@code targetPath} elements were always interpreted as relative to the output directory
* ({@code project.build.outputDirectory} or {@code project.build.testOutputDirectory}),
* not the project base directory. Maven 3 plugins (like maven-resources-plugin) expect to receive
* the relative path and perform the resolution themselves.
* </p>
* <p>
* <strong>Effect on Module and Target Version:</strong>
* When a target path is explicitly specified, the values of {@link #module()} and {@link #targetVersion()}
* are not used for inferring the output path (they are still used as compiler options however).
* This means that for scripts and resources, the files below the path specified by {@link #directory()}
* are copied to the path specified by {@code targetPath()} with the exact same directory structure.
* </p>
* <p>
* <strong>Usage Guidance:</strong>
* </p>
* <ul>
* <li><strong>For Maven 4 API consumers:</strong> Use {@link #targetPath(Project)} to get the
* fully resolved absolute path where files should be copied.</li>
* <li><strong>For Maven 3 compatibility layer:</strong> Use this method to get the path as specified
* in the configuration, which can then be passed to legacy plugins that expect to perform
* their own resolution.</li>
* <li><strong>For implementers:</strong> Store the path exactly as provided in the configuration.
* Do not resolve relative paths at storage time.</li>
* </ul>
*
* @see #targetPath(Project)
* @see Project#getOutputDirectory(ProjectScope)
*/
default Optional<Path> targetPath() {
return Optional.empty();
}

/**
* {@return the explicit target path resolved against the default target path}
* Invoking this method is equivalent to getting the default output directory
* by a call to {@code project.getOutputDirectory(scope())}, then resolving the
* {@linkplain #targetPath() target path} (if present) against that default directory.
* Note that if the target path is absolute, the result is that target path unchanged.
* {@return the fully resolved absolute target path where files should be copied}
* <p>
* <strong>Purpose:</strong> This method performs the complete path resolution logic, converting
* the potentially relative {@link #targetPath()} into an absolute filesystem path. This is the
* method that Maven 4 API consumers should use when they need to know the actual destination
* directory for copying files.
* </p>
* <p>
* <strong>Resolution Algorithm:</strong>
* </p>
* <ol>
* <li>Obtain the {@linkplain #targetPath() configured target path} (which may be empty, relative, or absolute)</li>
* <li>If the configured target path is absolute (e.g., {@code /tmp/custom}):
* <ul><li>Return it unchanged (no resolution needed)</li></ul></li>
* <li>Otherwise, get the output directory for this source root's {@link #scope()} by calling
* {@code project.getOutputDirectory(scope())}:
* <ul>
* <li>For {@link ProjectScope#MAIN}: typically {@code /path/to/project/target/classes}</li>
* <li>For {@link ProjectScope#TEST}: typically {@code /path/to/project/target/test-classes}</li>
* </ul></li>
* <li>If the configured target path is empty:
* <ul><li>Return the output directory as-is</li></ul></li>
* <li>If the configured target path is relative (e.g., {@code META-INF/resources}):
* <ul><li>Resolve it against the output directory using {@code outputDirectory.resolve(targetPath)}</li></ul></li>
* </ol>
* <p>
* <strong>Concrete Examples:</strong>
* </p>
* <p>
* Given a project at {@code /home/user/myproject} with {@link ProjectScope#MAIN}:
* </p>
* <table class="striped">
* <caption>Target Path Resolution Examples</caption>
* <thead>
* <tr>
* <th>Configuration ({@code targetPath()})</th>
* <th>Output Directory</th>
* <th>Result ({@code targetPath(project)})</th>
* <th>Explanation</th>
* </tr>
* </thead>
* <tbody>
* <tr>
* <td>{@code Optional.empty()}</td>
* <td>{@code /home/user/myproject/target/classes}</td>
* <td>{@code /home/user/myproject/target/classes}</td>
* <td>No explicit path → use output directory</td>
* </tr>
* <tr>
* <td>{@code Optional.of(Path.of("META-INF"))}</td>
* <td>{@code /home/user/myproject/target/classes}</td>
* <td>{@code /home/user/myproject/target/classes/META-INF}</td>
* <td>Relative path → resolve against output directory</td>
* </tr>
* <tr>
* <td>{@code Optional.of(Path.of("WEB-INF/classes"))}</td>
* <td>{@code /home/user/myproject/target/classes}</td>
* <td>{@code /home/user/myproject/target/classes/WEB-INF/classes}</td>
* <td>Relative path with subdirectories</td>
* </tr>
* <tr>
* <td>{@code Optional.of(Path.of("/tmp/custom"))}</td>
* <td>{@code /home/user/myproject/target/classes}</td>
* <td>{@code /tmp/custom}</td>
* <td>Absolute path → use as-is (no resolution)</td>
* </tr>
* </tbody>
* </table>
* <p>
* <strong>Relationship to {@link #targetPath()}:</strong>
* </p>
* <p>
* This method is the <em>resolution</em> counterpart to {@link #targetPath()}, which is the
* <em>storage</em> method. While {@code targetPath()} returns the path as configured (potentially relative),
* this method returns the absolute path where files will actually be written. The separation allows:
* </p>
* <ul>
* <li>Maven 4 API consumers to get absolute paths via this method</li>
* <li>Maven 3 compatibility layer to get relative paths via {@code targetPath()} for legacy plugins</li>
* <li>Implementations to store paths without premature resolution</li>
* </ul>
* <p>
* <strong>Implementation Note:</strong> The default implementation is equivalent to:
* </p>
* <pre>{@code
* Optional<Path> configured = targetPath();
* if (configured.isPresent() && configured.get().isAbsolute()) {
* return configured.get();
* }
* Path outputDir = project.getOutputDirectory(scope());
* return configured.map(outputDir::resolve).orElse(outputDir);
* }</pre>
*
* @param project the project to use for getting default directories
* @param project the project to use for obtaining the output directory
* @return the absolute path where files from {@link #directory()} should be copied
*
* @see #targetPath()
* @see Project#getOutputDirectory(ProjectScope)
*/
@Nonnull
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
* A Resource wrapper that maintains a connection to the underlying project model.
* When includes/excludes are modified, the changes are propagated back to the project's SourceRoots.
*/
@SuppressWarnings("deprecation")
class ConnectedResource extends Resource {
private final SourceRoot originalSourceRoot;
private final ProjectScope scope;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ void setUp() {
// Set a dummy pom file to establish the base directory
project.setFile(new java.io.File("./pom.xml"));

// Set build output directories
project.getBuild().setOutputDirectory("target/classes");
project.getBuild().setTestOutputDirectory("target/test-classes");

// Add a resource source root to the project
project.addSourceRoot(
new DefaultSourceRoot(ProjectScope.MAIN, Language.RESOURCES, Path.of("src/main/resources")));
Expand Down Expand Up @@ -199,7 +203,7 @@ void testTargetPathPreservedWithConnectedResource() {
resourceWithTarget.setDirectory("src/main/custom");
resourceWithTarget.setTargetPath("custom-output");

// Convert through DefaultSourceRoot to ensure targetPath extraction works
// Convert through DefaultSourceRoot to ensure targetPath is preserved
DefaultSourceRoot sourceRootFromResource =
new DefaultSourceRoot(project.getBaseDirectory(), ProjectScope.MAIN, resourceWithTarget.getDelegate());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,18 +142,22 @@ public static DefaultSourceRoot fromModel(
source.getIncludes(),
source.getExcludes(),
source.isStringFiltering(),
nonBlank(source.getTargetPath())
.map((targetPath) ->
baseDir.resolve(outputDir.apply(scope)).resolve(targetPath))
.orElse(null),
nonBlank(source.getTargetPath()).map(Path::of).orElse(null),
source.isEnabled());
}

/**
* Creates a new instance from the given resource.
* This is used for migration from the previous way of declaring resources.
* <p>
* <strong>Important:</strong> The {@code targetPath} from the resource is stored as-is
* (converted to a {@link Path} but not resolved against any directory). This preserves
* the Maven 3.x behavior where {@code targetPath} is relative to the output directory,
* not the project base directory. The actual resolution happens later via
* {@link SourceRoot#targetPath(Project)}.
* </p>
*
* @param baseDir the base directory for resolving relative paths
* @param baseDir the base directory for resolving relative paths (used only for the source directory)
* @param scope the scope of the resource (main or test)
* @param resource a resource element from the model
*/
Expand All @@ -169,7 +173,7 @@ public DefaultSourceRoot(final Path baseDir, ProjectScope scope, Resource resour
resource.getIncludes(),
resource.getExcludes(),
Boolean.parseBoolean(resource.getFiltering()),
nonBlank(resource.getTargetPath()).map(baseDir::resolve).orElse(null),
nonBlank(resource.getTargetPath()).map(Path::of).orElse(null),
true);
}

Expand Down Expand Up @@ -220,6 +224,11 @@ public Optional<Version> targetVersion() {

/**
* {@return an explicit target path, overriding the default value}
* <p>
* The returned path, if present, is stored as provided in the configuration and is typically
* relative to the output directory. Use {@link #targetPath(Project)} to get the fully
* resolved absolute path.
* </p>
*/
@Override
public Optional<Path> targetPath() {
Expand Down
Loading