Skip to content

[MNG-8686] Add SourceRoot.matcher(boolean) method #2236

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Original file line number Diff line number Diff line change
@@ -20,6 +20,7 @@

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

@@ -46,23 +47,47 @@ default Path directory() {
}

/**
* {@return the list of pattern matchers 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),
* the default is a Maven-specific variation of the {@code "glob"} pattern.
*
* <p>
* The default implementation returns an empty list, which means to apply a language-dependent pattern.
* For example, for the Java language, the pattern includes all files with the {@code .java} suffix.
*
* @see java.nio.file.FileSystem#getPathMatcher(String)
*/
default List<PathMatcher> includes() {
default List<String> includes() {
return List.of();
}

/**
* {@return the list of pattern matchers 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.
*/
default List<PathMatcher> excludes() {
default List<String> excludes() {
return List.of();
}

/**
* {@return a matcher combining the include and exclude patterns}.
* If the user did not specified 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>.
*
* <p>If the user did not specified any excludes, the default can be files generated
* by Source Code Management (<abbr>SCM</abbr>) software or by the operating system.
* Examples: <code>"**&sol;.gitignore"</code>, <code>"**&sol;.DS_Store"</code>.</p>
*
* @param defaultIncludes the default includes if unspecified by the user
* @param useDefaultExcludes whether to add the default set of patterns to exclude,
* mostly Source Code Management (<abbr>SCM</abbr>) files
*/
PathMatcher matcher(Collection<String> defaultIncludes, boolean useDefaultExcludes);

/**
* {@return in which context the source files will be used}.
* Not to be confused with dependency scope.
Original file line number Diff line number Diff line change
@@ -822,8 +822,8 @@ public boolean add(Resource resource) {
private static Resource toResource(SourceRoot sourceRoot) {
return new Resource(org.apache.maven.api.model.Resource.newBuilder()
.directory(sourceRoot.directory().toString())
.includes(sourceRoot.includes().stream().map(Object::toString).toList())
.excludes(sourceRoot.excludes().stream().map(Object::toString).toList())
.includes(sourceRoot.includes())
.excludes(sourceRoot.excludes())
.filtering(Boolean.toString(sourceRoot.stringFiltering()))
.build());
}
Original file line number Diff line number Diff line change
@@ -18,9 +18,9 @@
*/
package org.apache.maven.impl;

import java.nio.file.FileSystem;
import java.nio.file.Path;
import java.nio.file.PathMatcher;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
@@ -39,9 +39,9 @@
public final class DefaultSourceRoot implements SourceRoot {
private final Path directory;

private final List<PathMatcher> includes;
private final List<String> includes;

private final List<PathMatcher> excludes;
private final List<String> excludes;

private final ProjectScope scope;

@@ -65,9 +65,8 @@ public final class DefaultSourceRoot implements SourceRoot {
* @param source a source element from the model
*/
public DefaultSourceRoot(final Session session, final Path baseDir, final Source source) {
FileSystem fs = baseDir.getFileSystem();
includes = matchers(fs, source.getIncludes());
excludes = matchers(fs, source.getExcludes());
includes = source.getIncludes();
excludes = source.getExcludes();
stringFiltering = source.isStringFiltering();
enabled = source.isEnabled();
moduleName = nonBlank(source.getModule());
@@ -106,9 +105,8 @@ public DefaultSourceRoot(final Path baseDir, ProjectScope scope, Resource resour
throw new IllegalArgumentException("Source declaration without directory value.");
}
directory = baseDir.resolve(value).normalize();
FileSystem fs = directory.getFileSystem();
includes = matchers(fs, resource.getIncludes());
excludes = matchers(fs, resource.getExcludes());
includes = resource.getIncludes();
excludes = resource.getExcludes();
stringFiltering = Boolean.parseBoolean(resource.getFiltering());
enabled = true;
moduleName = null;
@@ -144,13 +142,15 @@ public DefaultSourceRoot(final ProjectScope scope, final Language language, fina
* @param scope scope of source code (main or test)
* @param language language of the source code
* @param directory directory of the source code
* @param includes patterns for the files to include, or {@code null} or empty if unspecified
* @param excludes patterns for the files to exclude, or {@code null} or empty if nothing to exclude
*/
public DefaultSourceRoot(
final ProjectScope scope,
final Language language,
final Path directory,
List<PathMatcher> includes,
List<PathMatcher> excludes) {
List<String> includes,
List<String> excludes) {
this.scope = Objects.requireNonNull(scope);
this.language = language;
this.directory = Objects.requireNonNull(directory);
@@ -176,38 +176,6 @@ private static String nonBlank(String value) {
return value;
}

/**
* Creates a path matcher for each pattern.
* The path separator is {@code /} on all platforms, including Windows.
* The prefix before the {@code :} character is the syntax.
* If no syntax is specified, {@code "glob"} is assumed.
*
* @param fs the file system of the root directory
* @param patterns the patterns for which to create path matcher
* @return a path matcher for each pattern
*/
private static List<PathMatcher> matchers(FileSystem fs, List<String> patterns) {
final var matchers = new PathMatcher[patterns.size()];
for (int i = 0; i < matchers.length; i++) {
String rawPattern = patterns.get(i);
String pattern = rawPattern.contains(":") ? rawPattern : "glob:" + rawPattern;
matchers[i] = new PathMatcher() {
final PathMatcher delegate = fs.getPathMatcher(pattern);

@Override
public boolean matches(Path path) {
return delegate.matches(path);
}

@Override
public String toString() {
return rawPattern;
}
};
}
return List.of(matchers);
}

/**
* {@return the root directory where the sources are stored}.
*/
@@ -217,23 +185,39 @@ public Path directory() {
}

/**
* {@return the list of pattern matchers for the files to include}.
* {@return the patterns for the files to include}.
*/
@Override
@SuppressWarnings("ReturnOfCollectionOrArrayField") // Safe because unmodifiable
public List<PathMatcher> includes() {
public List<String> includes() {
return includes;
}

/**
* {@return the list of pattern matchers for the files to exclude}.
* {@return the patterns for the files to exclude}.
*/
@Override
@SuppressWarnings("ReturnOfCollectionOrArrayField") // Safe because unmodifiable
public List<PathMatcher> excludes() {
public List<String> excludes() {
return excludes;
}

/**
* {@return a matcher combining the include and exclude patterns}.
*
* @param defaultIncludes the default includes if unspecified by the user
* @param useDefaultExcludes whether to add the default set of patterns to exclude,
* mostly Source Code Management (<abbr>SCM</abbr>) files
*/
@Override
public PathMatcher matcher(Collection<String> defaultIncludes, boolean useDefaultExcludes) {
Collection<String> actual = includes();
if (actual == null || actual.isEmpty()) {
actual = defaultIncludes;
}
return new PathSelector(directory(), actual, excludes(), useDefaultExcludes).simplify();
}

/**
* {@return in which context the source files will be used}.
*/
622 changes: 622 additions & 0 deletions impl/maven-impl/src/main/java/org/apache/maven/impl/PathSelector.java

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/*
* 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.impl;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

public class PathSelectorTest {
/**
* The temporary directory containing the files to test.
*/
private Path directory;

/**
* The filtered set of paths. Created by {@link #filter()}.
*/
private Set<Path> filtered;

/**
* Creates a temporary directory and checks its list of content based on patterns.
*
* @param tempDir temporary directory where to create a tree
* @throws IOException if an error occurred while creating a temporary file or directory
*/
@Test
public void testTree(final @TempDir Path tempDir) throws IOException {
directory = tempDir;
Path foo = Files.createDirectory(tempDir.resolve("foo"));
Path bar = Files.createDirectory(foo.resolve("bar"));
Path biz = Files.createDirectory(tempDir.resolve("biz"));
Files.createFile(tempDir.resolve("root.txt"));
Files.createFile(bar.resolve("leaf.txt"));
Files.createFile(biz.resolve("excluded.txt"));

filter("");
assertFilteredFilesContains("root.txt");
assertFilteredFilesContains("foo/bar/leaf.txt");
assertTrue(filtered.isEmpty(), filtered.toString());

filter("glob:");
assertFilteredFilesContains("foo/bar/leaf.txt");
assertTrue(filtered.isEmpty(), filtered.toString());
}

/**
* Creates the filtered paths in a modifiable set.
* The result is assigned to {@link #filtered}.
*
* @param syntax syntax to test, either an empty string of {@code "glob:"}
* @throws IOException if an error occurred while listing the files.
*/
private void filter(final String syntax) throws IOException {
var includes = List.of(syntax + "**/*.txt");
var excludes = List.of(syntax + "biz/**");
var matcher = new PathSelector(directory, includes, excludes, false);
filtered = new HashSet<>(Files.walk(directory).filter(matcher::matches).toList());
}

/**
* Asserts that the filtered set of paths contains the given item.
* If present, the path is removed from the collection of filtered files.
* It allows caller to verify that there are no unexpected elements remaining
* after all expected elements have been removed.
*
* @param path the path to test
*/
private void assertFilteredFilesContains(String path) {
assertTrue(filtered.remove(directory.resolve(path)), path);
}

/**
* Tests the omission of unnecessary excludes.
*
* Note: at the time of writing this test (April 2025), the list of excludes go down from 40 to 17 elements.
* This is not bad, but we could do better with, for example, a special treatment of the excludes that are
* for excluding an entire directory.
*/
@Test
public void testExcludeOmission() {
directory = Path.of("dummy");
var includes = List.of("**/*.java");
var excludes = List.of("biz/**");
var matcher = new PathSelector(directory, includes, excludes, true);
String s = matcher.toString();
assertTrue(s.contains("glob:**/*.java"));
assertFalse(s.contains("project.pj")); // Unnecessary exclusion should have been omitted.
assertFalse(s.contains(".DS_Store"));
}
}