From 5d8ff765fb807b53f43b1913d1171dbf5007f775 Mon Sep 17 00:00:00 2001 From: David Stephan Date: Fri, 13 Mar 2026 16:02:27 +0100 Subject: [PATCH 1/2] SED-4497 Referencing a Keyword from an AP library doesn't work --- .../step/core/scanner/AnnotationScanner.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/step-framework-core/src/main/java/step/core/scanner/AnnotationScanner.java b/step-framework-core/src/main/java/step/core/scanner/AnnotationScanner.java index f81fbd7..2bd2e56 100644 --- a/step-framework-core/src/main/java/step/core/scanner/AnnotationScanner.java +++ b/step-framework-core/src/main/java/step/core/scanner/AnnotationScanner.java @@ -35,6 +35,7 @@ import java.util.Set; import java.util.stream.Collectors; +import io.github.classgraph.ClassInfo; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -209,6 +210,23 @@ public Set getMethodsWithAnnotation(Class annotati return result; } + /** + * Returns the URL of the classpath element (JAR or directory) from which the given class was + * loaded during scanning. + * + *

This is determined from ClassGraph's scan metadata at scan time — before the class is + * actually loaded by the JVM — and is therefore not affected by {@link SecurityManager} + * restrictions on {@link Class#getProtectionDomain()} nor by JDK version differences. + * Returns {@code null} if the class name is not present in the scan result. + * + * @param className the binary class name (e.g. {@code "com.example.MyKeywords"}) + * @return the URL of the JAR or directory that contains the class, or {@code null} + */ + public URL getClasspathElementUrl(String className) { + ClassInfo classInfo = scanResult.getClassInfo(className); + return classInfo != null ? classInfo.getClasspathElementURL() : null; + } + /** * Alternative implementation of {@link Class#isAnnotationPresent(Class)} which * doesn't rely on class equality but class names. The class loaders of the From dbcf3378327e821954f481da31f824159a00458d Mon Sep 17 00:00:00 2001 From: David Stephan Date: Mon, 16 Mar 2026 12:40:13 +0100 Subject: [PATCH 2/2] SED-4497 increasing test coverage --- .../step/core/scanner/AnnotationScanner.java | 3 + .../core/scanner/AnnotationScannerTest.java | 55 +++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/step-framework-core/src/main/java/step/core/scanner/AnnotationScanner.java b/step-framework-core/src/main/java/step/core/scanner/AnnotationScanner.java index 2bd2e56..8711796 100644 --- a/step-framework-core/src/main/java/step/core/scanner/AnnotationScanner.java +++ b/step-framework-core/src/main/java/step/core/scanner/AnnotationScanner.java @@ -223,6 +223,9 @@ public Set getMethodsWithAnnotation(Class annotati * @return the URL of the JAR or directory that contains the class, or {@code null} */ public URL getClasspathElementUrl(String className) { + if (className == null) { + throw new IllegalArgumentException("className must not be null"); + } ClassInfo classInfo = scanResult.getClassInfo(className); return classInfo != null ? classInfo.getClasspathElementURL() : null; } diff --git a/step-framework-core/src/test/java/step/core/scanner/AnnotationScannerTest.java b/step-framework-core/src/test/java/step/core/scanner/AnnotationScannerTest.java index 50a3949..fdb3813 100644 --- a/step-framework-core/src/test/java/step/core/scanner/AnnotationScannerTest.java +++ b/step-framework-core/src/test/java/step/core/scanner/AnnotationScannerTest.java @@ -19,10 +19,14 @@ package step.core.scanner; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import java.io.File; import java.io.IOException; import java.lang.reflect.Method; +import java.net.URL; +import java.net.URLClassLoader; import java.util.List; import java.util.stream.Collectors; @@ -97,6 +101,57 @@ public void testAnnotationScannerForSpecificJarsWithSpacesAndSpecialCharsInPath( } } + @Test + public void testGetClasspathElementUrl_found() { + File file = FileHelper.getClassLoaderResourceAsFile(this.getClass().getClassLoader(), "annotation-test.jar"); + try (AnnotationScanner annotationScanner = AnnotationScanner.forSpecificJar(file)) { + URL url = annotationScanner.getClasspathElementUrl("step.core.scanner.AnnotatedClass"); + assertNotNull(url); + assertEquals(file.toURI().toURL(), url); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Test + public void testGetClasspathElementUrl_notFound() { + File file = FileHelper.getClassLoaderResourceAsFile(this.getClass().getClassLoader(), "annotation-test.jar"); + try (AnnotationScanner annotationScanner = AnnotationScanner.forSpecificJar(file)) { + URL url = annotationScanner.getClasspathElementUrl("com.example.NonExistentClass"); + assertNull(url); + } + } + + @Test(expected = IllegalArgumentException.class) + public void testGetClasspathElementUrl_nullClassName() { + try (AnnotationScanner annotationScanner = AnnotationScanner.forAllClassesFromContextClassLoader()) { + annotationScanner.getClasspathElementUrl(null); + } + } + + @Test + public void testGetClasspathElementUrl_multipleJars_returnsCorrectJar() throws Exception { + File annotationTestJar = FileHelper.getClassLoaderResourceAsFile(this.getClass().getClassLoader(), "annotation-test.jar"); + // Use the classgraph JAR (already a compile dependency) as a second distinct JAR + URL classgraphJarUrl = io.github.classgraph.ClassGraph.class.getProtectionDomain().getCodeSource().getLocation(); + File classgraphJar = new File(classgraphJarUrl.toURI()); + + URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{ + annotationTestJar.toURI().toURL(), + classgraphJarUrl + }, null); + + try (AnnotationScanner annotationScanner = AnnotationScanner.forSpecificJarFromURLClassLoader(urlClassLoader)) { + URL annotatedClassUrl = annotationScanner.getClasspathElementUrl("step.core.scanner.AnnotatedClass"); + assertNotNull(annotatedClassUrl); + assertEquals(annotationTestJar.getCanonicalFile(), new File(annotatedClassUrl.toURI()).getCanonicalFile()); + + URL classgraphClassUrl = annotationScanner.getClasspathElementUrl("io.github.classgraph.ClassGraph"); + assertNotNull(classgraphClassUrl); + assertEquals(classgraphJar.getCanonicalFile(), new File(classgraphClassUrl.toURI()).getCanonicalFile()); + } + } + // Don't remove this class // It is here to ensure that annotation scanning performed in // testGetMethodsWithAnnotation() isn't finding other methods that the