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
30 changes: 30 additions & 0 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 @@ -24,6 +24,7 @@

import org.apache.maven.api.annotations.Experimental;
import org.apache.maven.api.annotations.Nonnull;
import org.apache.maven.api.annotations.Nullable;
import org.apache.maven.api.model.Build;
import org.apache.maven.api.model.Model;
import org.apache.maven.api.model.Profile;
Expand Down Expand Up @@ -172,6 +173,35 @@ default Build getBuild() {
@Nonnull
Path getBasedir();

/**
* Returns the directory where files generated by the build are placed.
* The directory depends on the scope:
*
* <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>
* </ul>
*
* @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
*
* @see SourceRoot#targetPath(Project)
*/
@Nonnull
default Path getOutputDirectory(@Nullable ProjectScope scope) {
String dir;
Build build = getBuild();
if (scope == ProjectScope.MAIN) {
dir = build.getOutputDirectory();
} else if (scope == ProjectScope.TEST) {
dir = build.getTestOutputDirectory();
} else {
dir = build.getDirectory();
}
return getBasedir().resolve(dir);
}

/**
* {@return the project direct dependencies (directly specified or inherited)}.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
import java.util.List;
import java.util.Optional;

import org.apache.maven.api.annotations.Nonnull;

/**
* A root directory of source files.
* The sources may be Java main classes, test classes, resources or anything else identified by the scope.
Expand All @@ -35,7 +37,7 @@
*/
public interface SourceRoot {
/**
* {@return the root directory where the sources are stored}.
* {@return the root directory where the sources are stored}
* The path is relative to the <abbr>POM</abbr> file.
*
* <h4>Default implementation</h4>
Expand All @@ -62,7 +64,7 @@ default Path directory() {
}

/**
* {@return the list of patterns for the files to include}.
* {@return the list of patterns for the files to include}
* The path separator is {@code /} on all platforms, including Windows.
* The prefix before the {@code :} character, if present and longer than 1 character, is the syntax.
* If no syntax is specified, or if its length is 1 character (interpreted as a Windows drive),
Expand All @@ -79,7 +81,7 @@ default List<String> includes() {
}

/**
* {@return the list of patterns for the files to exclude}.
* {@return the list of patterns for the files to exclude}
* The exclusions are applied after the inclusions.
* The default implementation returns an empty list.
*/
Expand All @@ -88,7 +90,7 @@ default List<String> excludes() {
}

/**
* {@return a matcher combining the include and exclude patterns}.
* {@return a matcher combining the include and exclude patterns}
* If the user did not specify any includes, the given {@code defaultIncludes} are used.
* These defaults depend on the plugin.
* For example, the default include of the Java compiler plugin is <code>"**&sol;*.java"</code>.
Expand All @@ -104,7 +106,7 @@ default List<String> excludes() {
PathMatcher matcher(Collection<String> defaultIncludes, boolean useDefaultExcludes);

/**
* {@return in which context the source files will be used}.
* {@return in which context the source files will be used}
* Not to be confused with dependency scope.
* The default value is {@code "main"}.
*
Expand All @@ -115,7 +117,7 @@ default ProjectScope scope() {
}

/**
* {@return the language of the source files}.
* {@return the language of the source files}
* The default value is {@code "java"}.
*
* @see Language#JAVA_FAMILY
Expand All @@ -125,15 +127,15 @@ default Language language() {
}

/**
* {@return the name of the Java module (or other language-specific module) which is built by the sources}.
* {@return the name of the Java module (or other language-specific module) which is built by the sources}
* The default value is empty.
*/
default Optional<String> module() {
return Optional.empty();
}

/**
* {@return the version of the platform where the code will be executed}.
* {@return the version of the platform where the code will be executed}
* In a Java environment, this is the value of the {@code --release} compiler option.
* The default value is empty.
*/
Expand All @@ -142,7 +144,7 @@ default Optional<Version> targetVersion() {
}

/**
* {@return an explicit target path, overriding the default value}.
* {@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()}
Expand All @@ -153,15 +155,36 @@ default Optional<Path> targetPath() {
}

/**
* {@return whether resources are filtered to replace tokens with parameterized values}.
* {@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.
*
* @param project the project to use for getting default directories
*
* @see Project#getOutputDirectory(ProjectScope)
*/
@Nonnull
default Path targetPath(@Nonnull Project project) {
Optional<Path> targetPath = targetPath();
// The test for `isAbsolute()` is a small optimization for avoiding the call to `getOutputDirectory(…)`.
return targetPath.filter(Path::isAbsolute).orElseGet(() -> {
Path base = project.getOutputDirectory(scope());
return targetPath.map(base::resolve).orElse(base);
});
}

/**
* {@return whether resources are filtered to replace tokens with parameterized values}
* The default value is {@code false}.
*/
default boolean stringFiltering() {
return false;
}

/**
* {@return whether the directory described by this source element should be included in the build}.
* {@return whether the directory described by this source element should be included in the build}
* The default value is {@code true}.
*/
default boolean enabled() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.maven.api;

import java.nio.file.Path;
import java.nio.file.PathMatcher;
import java.util.Collection;
import java.util.Optional;

import org.apache.maven.api.model.Build;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

public class SourceRootTest implements SourceRoot {
private ProjectScope scope;

private Language language;

private String moduleName;

@Override
public ProjectScope scope() {
return (scope != null) ? scope : SourceRoot.super.scope();
}

@Override
public Language language() {
return (language != null) ? language : SourceRoot.super.language();
}

@Override
public Optional<String> module() {
return Optional.ofNullable(moduleName);
}

@Override
public PathMatcher matcher(Collection<String> defaultIncludes, boolean useDefaultExcludes) {
return null; // Not used for this test.
}

@Test
void testDirectory() {
assertEquals(Path.of("src", "main", "java"), directory());

scope = ProjectScope.TEST;
assertEquals(Path.of("src", "test", "java"), directory());

moduleName = "org.foo";
assertEquals(Path.of("src", "org.foo", "test", "java"), directory());
}

@Test
void testTargetPath() {
Build build = mock(Build.class);
when(build.getDirectory()).thenReturn("target");
when(build.getOutputDirectory()).thenReturn("target/classes");
when(build.getTestOutputDirectory()).thenReturn("target/test-classes");

Project project = mock(Project.class);
when(project.getBuild()).thenReturn(build);
when(project.getBasedir()).thenReturn(Path.of("myproject"));
when(project.getOutputDirectory(any(ProjectScope.class))).thenCallRealMethod();

assertEquals(Path.of("myproject", "target", "classes"), targetPath(project));

scope = ProjectScope.TEST;
assertEquals(Path.of("myproject", "target", "test-classes"), targetPath(project));
}
}
12 changes: 10 additions & 2 deletions api/maven-api-model/src/main/mdo/maven.mdo
Original file line number Diff line number Diff line change
Expand Up @@ -2129,8 +2129,16 @@
<description>
<![CDATA[
Specifies an explicit target path, overriding the default value.
The path is relative to the {@code ${project.build.outputDirectory}} directory,
which is typically {@code target/classes} in a Java project.
If unspecified, then the default value is one of the following:

<ul>
<li>{@code ${project.build.outputDirectory}} (typically {@code target/classes}) if {@code scope} is "main",</li>
<li>{@code ${project.build.testOutputDirectory}} (typically {@code target/test-classes}) if {@code scope} is "test",</li>
<li>{@code ${project.build.directory}} (typically {@code target}) otherwise.</li>
</ul>

<p>If this property is specified but is a relative path,
then the path is resolved against the above-cited default value.</p>

<p>When a target path is explicitly specified, the values of the {@code module} and {@code targetVersion}
elements are not used for inferring the path (they are still used as compiler options however).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
Expand Down Expand Up @@ -644,11 +645,20 @@ private void initProject(MavenProject project, ModelBuilderResult result) {
Build build = project.getBuild().getDelegate();
List<org.apache.maven.api.model.Source> sources = build.getSources();
Path baseDir = project.getBaseDirectory();
Function<ProjectScope, String> outputDirectory = (scope) -> {
if (scope == ProjectScope.MAIN) {
return build.getOutputDirectory();
} else if (scope == ProjectScope.TEST) {
return build.getTestOutputDirectory();
} else {
return build.getDirectory();
}
};
boolean hasScript = false;
boolean hasMain = false;
boolean hasTest = false;
for (var source : sources) {
var src = new DefaultSourceRoot(session, baseDir, source);
var src = DefaultSourceRoot.fromModel(session, baseDir, outputDirectory, source);
project.addSourceRoot(src);
Language language = src.language();
if (Language.JAVA_FAMILY.equals(language)) {
Expand Down
Loading