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..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 @@ -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,26 @@ 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) { + if (className == null) { + throw new IllegalArgumentException("className must not be null"); + } + 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 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