Skip to content
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

Support scanning for classpath resources #3705

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
Expand Up @@ -34,6 +34,7 @@ repository on GitHub.
* Error messages for type mismatches in `NamespacedHierarchicalStore` now include the
actual type and value in addition to the required type.

* `ReflectionUtils` now supports scanning for classpath resources.

[[release-notes-5.11.0-M1-junit-jupiter]]
=== JUnit Jupiter
Expand Down
Expand Up @@ -67,7 +67,6 @@ private ReflectionSupport() {
*/
@API(status = DEPRECATED, since = "1.4")
@Deprecated
@SuppressWarnings("deprecation")
public static Optional<Class<?>> loadClass(String name) {
return ReflectionUtils.loadClass(name);
}
Expand Down Expand Up @@ -137,6 +136,30 @@ public static List<Class<?>> findAllClassesInClasspathRoot(URI root, Predicate<C
return ReflectionUtils.findAllClassesInClasspathRoot(root, classFilter, classNameFilter);
}

/**
* Find all {@linkplain Resource resources} in the supplied classpath {@code root}
* that match the specified {@code resourceFilter} and {@code resourceNameFilter}
* predicates.
*
* <p>The classpath scanning algorithm searches recursively in subpackages
* beginning with the root of the classpath.
*
* @param root the URI for the classpath root in which to scan; never
* {@code null}
* @param resourceFilter the resource type filter; never {@code null}
* @param resourceNameFilter the resource name filter; never {@code null}
* @return an immutable list of all such resources found; never {@code null}
* but potentially empty
* @see #findAllResourcesInPackage(String, Predicate, Predicate)
* @see #findAllResourcesInModule(String, Predicate, Predicate)
*/
@API(status = EXPERIMENTAL, since = "1.11")
public static List<Resource> findAllResourcesInClasspathRoot(URI root, Predicate<Resource> resourceFilter,
Predicate<String> resourceNameFilter) {

return ReflectionUtils.findAllResourcesInClasspathRoot(root, resourceFilter, resourceNameFilter);
}

/**
* Find all {@linkplain Class classes} in the supplied classpath {@code root}
* that match the specified {@code classFilter} and {@code classNameFilter}
Expand All @@ -162,6 +185,31 @@ public static Stream<Class<?>> streamAllClassesInClasspathRoot(URI root, Predica
return ReflectionUtils.streamAllClassesInClasspathRoot(root, classFilter, classNameFilter);
}

/**
* Find all {@linkplain Resource resources} in the supplied classpath {@code root}
* that match the specified {@code resourceFilter} and {@code resourceNameFilter}
* predicates.
*
* <p>The classpath scanning algorithm searches recursively in subpackages
* beginning with the root of the classpath.
*
* @param root the URI for the classpath root in which to scan; never
* {@code null}
* @param resourceFilter the resource type filter; never {@code null}
* @param resourceNameFilter the resources name filter; never {@code null}
* @return a stream of all such classes found; never {@code null}
* but potentially empty
* @since 1.10
* @see #streamAllResourcesInPackage(String, Predicate, Predicate)
* @see #streamAllResourcesInModule(String, Predicate, Predicate)
*/
@API(status = EXPERIMENTAL, since = "1.11")
public static Stream<Resource> streamAllResourcesInClasspathRoot(URI root, Predicate<Resource> resourceFilter,
Predicate<String> resourceNameFilter) {

return ReflectionUtils.streamAllResourcesInClasspathRoot(root, resourceFilter, resourceNameFilter);
}

/**
* Find all {@linkplain Class classes} in the supplied {@code basePackageName}
* that match the specified {@code classFilter} and {@code classNameFilter}
Expand All @@ -186,6 +234,31 @@ public static List<Class<?>> findAllClassesInPackage(String basePackageName, Pre
return ReflectionUtils.findAllClassesInPackage(basePackageName, classFilter, classNameFilter);
}

/**
* Find all {@linkplain Resource resources} in the supplied {@code basePackageName}
* that match the specified {@code resourceFilter} and {@code resourceNameFilter}
* predicates.
*
* <p>The classpath scanning algorithm searches recursively in subpackages
* beginning within the supplied base package.
*
* @param basePackageName the name of the base package in which to start
* scanning; must not be {@code null} and must be valid in terms of Java
* syntax
* @param resourceFilter the resource type filter; never {@code null}
* @param resourceNameFilter the resource name filter; never {@code null}
* @return an immutable list of all such classes found; never {@code null}
* but potentially empty
* @see #findAllResourcesInClasspathRoot(URI, Predicate, Predicate)
* @see #findAllResourcesInModule(String, Predicate, Predicate)
*/
@API(status = EXPERIMENTAL, since = "1.11")
public static List<Resource> findAllResourcesInPackage(String basePackageName, Predicate<Resource> resourceFilter,
Predicate<String> resourceNameFilter) {

return ReflectionUtils.findAllResourcesInPackage(basePackageName, resourceFilter, resourceNameFilter);
}

/**
* Find all {@linkplain Class classes} in the supplied {@code basePackageName}
* that match the specified {@code classFilter} and {@code classNameFilter}
Expand All @@ -212,6 +285,32 @@ public static Stream<Class<?>> streamAllClassesInPackage(String basePackageName,
return ReflectionUtils.streamAllClassesInPackage(basePackageName, classFilter, classNameFilter);
}

/**
* Find all {@linkplain Resource resources} in the supplied {@code basePackageName}
* that match the specified {@code resourceFilter} and {@code resourceNameFilter}
* predicates.
*
* <p>The classpath scanning algorithm searches recursively in subpackages
* beginning within the supplied base package.
*
* @param basePackageName the name of the base package in which to start
* scanning; must not be {@code null} and must be valid in terms of Java
* syntax
* @param resourceFilter the resource type filter; never {@code null}
* @param resourceNameFilter the resource name filter; never {@code null}
* @return a stream of all such resources found; never {@code null}
* but potentially empty
* @since 1.10
* @see #streamAllResourcesInClasspathRoot(URI, Predicate, Predicate)
* @see #streamAllResourcesInModule(String, Predicate, Predicate)
*/
@API(status = EXPERIMENTAL, since = "1.11")
public static Stream<Resource> streamAllResourcesInPackage(String basePackageName,
Predicate<Resource> resourceFilter, Predicate<String> resourceNameFilter) {

return ReflectionUtils.streamAllResourcesInPackage(basePackageName, resourceFilter, resourceNameFilter);
}

/**
* Find all {@linkplain Class classes} in the supplied {@code moduleName}
* that match the specified {@code classFilter} and {@code classNameFilter}
Expand All @@ -236,6 +335,31 @@ public static List<Class<?>> findAllClassesInModule(String moduleName, Predicate
return ReflectionUtils.findAllClassesInModule(moduleName, classFilter, classNameFilter);
}

/**
* Find all {@linkplain Resource resources} in the supplied {@code moduleName}
* that match the specified {@code resourceFilter} and {@code resourceNameFilter}
* predicates.
*
* <p>The module-path scanning algorithm searches recursively in all
* packages contained in the module.
*
* @param moduleName the name of the module to scan; never {@code null} or
* <em>empty</em>
* @param resourceFilter the resource type filter; never {@code null}
* @param resourceNameFilter the resource name filter; never {@code null}
* @return an immutable list of all such resources found; never {@code null}
* but potentially empty
* @since 1.1.1
* @see #findAllResourcesInClasspathRoot(URI, Predicate, Predicate)
* @see #findAllResourcesInPackage(String, Predicate, Predicate)
*/
@API(status = EXPERIMENTAL, since = "1.11")
public static List<Resource> findAllResourcesInModule(String moduleName, Predicate<Resource> resourceFilter,
Predicate<String> resourceNameFilter) {

return ReflectionUtils.findAllResourcesInModule(moduleName, resourceFilter, resourceNameFilter);
}

/**
* Find all {@linkplain Class classes} in the supplied {@code moduleName}
* that match the specified {@code classFilter} and {@code classNameFilter}
Expand All @@ -261,6 +385,31 @@ public static Stream<Class<?>> streamAllClassesInModule(String moduleName, Predi
return ReflectionUtils.streamAllClassesInModule(moduleName, classFilter, classNameFilter);
}

/**
* Find all {@linkplain Resource resources} in the supplied {@code moduleName}
* that match the specified {@code resourceFilter} and {@code resourceNameFilter}
* predicates.
*
* <p>The module-path scanning algorithm searches recursively in all
* packages contained in the module.
*
* @param moduleName the name of the module to scan; never {@code null} or
* <em>empty</em>
* @param resourceFilter the resource type filter; never {@code null}
* @param resourceNameFilter the resource name filter; never {@code null}
* @return a stream of all such resources found; never {@code null}
* but potentially empty
* @since 1.10
* @see #streamAllResourcesInClasspathRoot(URI, Predicate, Predicate)
* @see #streamAllResourcesInPackage(String, Predicate, Predicate)
*/
@API(status = EXPERIMENTAL, since = "1.11")
public static Stream<Resource> streamAllResourcesInModule(String moduleName, Predicate<Resource> resourceFilter,
Predicate<String> resourceNameFilter) {

return ReflectionUtils.streamAllResourcesInModule(moduleName, resourceFilter, resourceNameFilter);
}

/**
* Create a new instance of the specified {@link Class} by invoking
* the constructor whose argument list matches the types of the supplied
Expand Down
@@ -0,0 +1,61 @@
/*
* Copyright 2015-2024 the original author or authors.
*
* All rights reserved. This program and the accompanying materials are
* made available under the terms of the Eclipse Public License v2.0 which
* accompanies this distribution and is available at
*
* https://www.eclipse.org/legal/epl-v20.html
*/

package org.junit.platform.commons.support;

import static org.apiguardian.api.API.Status.EXPERIMENTAL;

import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.util.function.Predicate;

import org.apiguardian.api.API;

/**
* Represents a resource on the classpath.
*
* @see ReflectionSupport#findAllResourcesInClasspathRoot(URI, Predicate, Predicate)
* @see ReflectionSupport#findAllResourcesInPackage(String, Predicate, Predicate)
* @see ReflectionSupport#findAllResourcesInModule(String, Predicate, Predicate)
* @see ReflectionSupport#streamAllResourcesInClasspathRoot(URI, Predicate, Predicate)
* @see ReflectionSupport#streamAllResourcesInPackage(String, Predicate, Predicate)
* @see ReflectionSupport#streamAllResourcesInModule(String, Predicate, Predicate)
*/
@API(status = EXPERIMENTAL, since = "1.11")
public interface Resource {

/**
* Get the resource name.
* <p>
* The resource name is a {@code /}-separated path. The path is relative to
* the classpath root in which the resource is located.
*
* @return the resource name; never {@code null}
*/
String getName();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have intentionally expanded the original definition of Resource somewhat.

A few reasons:

  • While the URI does identify a resource uniquely, it is rooted in the file system. This makes it a less convenient as a human readable identifier. For example in Cucumber, feature files locations are identified by their classpath resource name or the path relative to the current working directory. For example
Scenario: Addition                   # io/cucumber/examples/calculator/basic_arithmetic.feature:7    <<<<<<<<<<<<
 Given a calculator I just turned on # io.cucumber.examples.calculator.RpnCalculatorStepDefinitions.a_calculator_I_just_turned_on()
 When I add 4 and 5                  # io.cucumber.examples.calculator.RpnCalculatorStepDefinitions.adding(int,int)
 Then the result is 9                # io.cucumber.examples.calculator.RpnCalculatorStepDefinitions.the_result_is(double)

Scenario Outline: Many additions     # io/cucumber/examples/calculator/basic_arithmetic.feature:30     <<<<<<<<<<<<
 Given a calculator I just turned on # io.cucumber.examples.calculator.RpnCalculatorStepDefinitions.a_calculator_I_just_turned_on()
 Given the previous entries:         # io.cucumber.examples.calculator.RpnCalculatorStepDefinitions.thePreviousEntries(java.util.List<io.cucumber.examples.calculator.RpnCalculatorStepDefinitions$Entry>)
   | first | second | operation |
   | 1     | 1      | +         |
   | 2     | 1      | +         |
 When I press +                      # io.cucumber.examples.calculator.RpnCalculatorStepDefinitions.I_press(java.lang.String)
 And I add 2 and 3                   # io.cucumber.examples.calculator.RpnCalculatorStepDefinitions.adding(int,int)
 And I press +                       # io.cucumber.examples.calculator.RpnCalculatorStepDefinitions.I_press(java.lang.String)
 Then the result is 10               # io.cucumber.examples.calculator.RpnCalculatorStepDefinitions.the_result_is(double)
  • To make it possible to use the package filter in the EngineDiscoveryRequestResolver in combination with the discovery to resources, ReflectionUtils. needs a Predicate<String> resourceNameFilter. The Predicate<Resource> resourceFilter should have access to the at least as much information as the Predicate<String>.

    To actually implement the package filter with resources some more work will have to be done to get the resource path and change it from a / separated path into . separated package name. But that is for later.


/**
* Get URI to a resource.
*
* @return the uri of the resource; never {@code null}
*/
URI getUri();

/**
* Returns an input stream for reading this resource.
*
* @return an input stream for this resource; never {@code null}
* @throws IOException if an I/O exception occurs
*/
default InputStream getInputStream() throws IOException {
return getUri().toURL().openStream();
}
}
Expand Up @@ -17,32 +17,33 @@
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.function.Consumer;
import java.util.function.BiConsumer;
import java.util.function.Predicate;

import org.junit.platform.commons.logging.Logger;
import org.junit.platform.commons.logging.LoggerFactory;

/**
* @since 1.0
* @since 1.11
*/
class ClassFileVisitor extends SimpleFileVisitor<Path> {
class ClasspathFileVisitor extends SimpleFileVisitor<Path> {

private static final Logger logger = LoggerFactory.getLogger(ClassFileVisitor.class);
private static final Logger logger = LoggerFactory.getLogger(ClasspathFileVisitor.class);

static final String CLASS_FILE_SUFFIX = ".class";
private static final String PACKAGE_INFO_FILE_NAME = "package-info" + CLASS_FILE_SUFFIX;
private static final String MODULE_INFO_FILE_NAME = "module-info" + CLASS_FILE_SUFFIX;
private final Path basePath;
private final BiConsumer<Path, Path> consumer;
private final Predicate<Path> filter;

private final Consumer<Path> classFileConsumer;

ClassFileVisitor(Consumer<Path> classFileConsumer) {
this.classFileConsumer = classFileConsumer;
ClasspathFileVisitor(Path basePath, Predicate<Path> filter, BiConsumer<Path, Path> consumer) {
this.basePath = basePath;
this.filter = filter;
this.consumer = consumer;
}

@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attributes) {
if (isNotPackageInfo(file) && isNotModuleInfo(file) && isClassFile(file)) {
classFileConsumer.accept(file);
if (filter.test(file)) {
consumer.accept(basePath, file);
}
return CONTINUE;
}
Expand All @@ -61,16 +62,4 @@ public FileVisitResult postVisitDirectory(Path dir, IOException ex) {
return CONTINUE;
}

private static boolean isNotPackageInfo(Path path) {
return !path.endsWith(PACKAGE_INFO_FILE_NAME);
}

private static boolean isNotModuleInfo(Path path) {
return !path.endsWith(MODULE_INFO_FILE_NAME);
}

private static boolean isClassFile(Path file) {
return file.getFileName().toString().endsWith(CLASS_FILE_SUFFIX);
}

}
@@ -0,0 +1,45 @@
/*
* Copyright 2015-2024 the original author or authors.
*
* All rights reserved. This program and the accompanying materials are
* made available under the terms of the Eclipse Public License v2.0 which
* accompanies this distribution and is available at
*
* https://www.eclipse.org/legal/epl-v20.html
*/

package org.junit.platform.commons.util;

import java.nio.file.Path;
import java.util.function.Predicate;

/**
* @since 1.11
*/
class ClasspathFilters {

static final String CLASS_FILE_SUFFIX = ".class";
private static final String PACKAGE_INFO_FILE_NAME = "package-info" + CLASS_FILE_SUFFIX;
private static final String MODULE_INFO_FILE_NAME = "module-info" + CLASS_FILE_SUFFIX;

static Predicate<Path> classFiles() {
return file -> isNotPackageInfo(file) && isNotModuleInfo(file) && isClassFile(file);
}

static Predicate<Path> resourceFiles() {
return file -> !isClassFile(file);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am explicitly excluding .class files when scanning for resources. Tests that are located in class files will always be selected with a class selector, so it doesn't make sense to all consider them when scanning for resources. This does assume that every .class file is a Java class file, but that seems reasonable enough.

}

private static boolean isNotPackageInfo(Path path) {
return !path.endsWith(PACKAGE_INFO_FILE_NAME);
}

private static boolean isNotModuleInfo(Path path) {
return !path.endsWith(MODULE_INFO_FILE_NAME);
}

private static boolean isClassFile(Path file) {
return file.getFileName().toString().endsWith(CLASS_FILE_SUFFIX);
}

}