diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index a61b46a8..bcdd6fe0 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -38,7 +38,7 @@ jobs:
restore-keys: ${{ runner.os }}-m2
- name: Package
- run: mvn -B clean package jacoco:report
+ run: mvn -B clean verify jacoco:report
- name: Coverage
uses: codecov/codecov-action@v7.0.0
diff --git a/.github/workflows/release-docs.yml b/.github/workflows/release-docs.yml
index cc82485e..e0c21894 100644
--- a/.github/workflows/release-docs.yml
+++ b/.github/workflows/release-docs.yml
@@ -27,7 +27,7 @@ jobs:
uses: actions/setup-java@v5
with:
distribution: "zulu"
- java-version: 21
+ java-version: 25
- name: Maven
uses: stCarolas/setup-maven@v5
diff --git a/.github/workflows/release-to-maven-central.yml b/.github/workflows/release-to-maven-central.yml
index fed5ab5b..85f80d04 100644
--- a/.github/workflows/release-to-maven-central.yml
+++ b/.github/workflows/release-to-maven-central.yml
@@ -25,7 +25,7 @@ jobs:
uses: actions/setup-java@v5
with: # overwrite settings.xml
distribution: "zulu"
- java-version: 21
+ java-version: 25
server-id: central
server-username: MAVEN_CENTRAL_USERNAME
server-password: MAVEN_CENTRAL_PASSWORD
diff --git a/pom.xml b/pom.xml
index 0dde4ad1..2fe55e36 100644
--- a/pom.xml
+++ b/pom.xml
@@ -237,6 +237,18 @@
+
+ org.apache.maven.plugins
+ maven-jar-plugin
+
+
+
+ true
+
+
+
+
+
org.sonatype.central
central-publishing-maven-plugin
@@ -259,6 +271,13 @@
true
+
+
+
+ true
+
+
+
*:*
@@ -342,6 +361,12 @@
org.jacoco
jacoco-maven-plugin
+
+
+
+ META-INF/versions/**
+
+
agent
@@ -419,6 +444,27 @@
+
+ multi-release-jar-smoke-test
+ verify
+
+ run
+
+
+
+
+
+
+
+
+
+
+
+
@@ -484,6 +530,45 @@
+
+ java24-tests
+
+ [24,)
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+
+ compile-java-24
+
+ compile
+
+
+
+ ${project.basedir}/src/main/java24
+
+ ${project.build.outputDirectory}/META-INF/versions/24
+ 24
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+ ${project.build.outputDirectory}/META-INF/versions/24
+
+ ${project.build.outputDirectory}
+
+
+
+
+
+
release
diff --git a/src/main/java/org/vafer/jdependency/Clazzpath.java b/src/main/java/org/vafer/jdependency/Clazzpath.java
index 29310f1b..21713424 100644
--- a/src/main/java/org/vafer/jdependency/Clazzpath.java
+++ b/src/main/java/org/vafer/jdependency/Clazzpath.java
@@ -32,12 +32,11 @@
import java.util.zip.ZipEntry;
import org.apache.commons.io.input.MessageDigestInputStream;
-import org.objectweb.asm.ClassReader;
+import org.vafer.jdependency.utils.DependencyUtils;
import static org.apache.commons.io.FilenameUtils.normalize;
import static org.apache.commons.io.FilenameUtils.separatorsToUnix;
import org.vafer.jdependency.Clazz.ParsedFileName;
-import org.vafer.jdependency.asm.DependenciesClassAdapter;
import static org.vafer.jdependency.Clazz.parseClassFileName;
import static org.vafer.jdependency.utils.StreamUtils.asStream;
@@ -173,8 +172,7 @@ private ClazzpathUnit addClazzpathUnit( final Iterable resources, fina
inputStream = calculatingInputStream;
}
- final DependenciesClassAdapter v = new DependenciesClassAdapter();
- new ClassReader(inputStream).accept(v, ClassReader.EXPAND_FRAMES | ClassReader.SKIP_DEBUG);
+ final Set depNames = DependencyUtils.getDependenciesOfClass(inputStream);
// get or create clazz
final String clazzName = resource.name;
@@ -201,7 +199,6 @@ private ClazzpathUnit addClazzpathUnit( final Iterable resources, fina
// iterate through all dependencies
- final Set depNames = v.getDependencies();
for (String depName : depNames) {
Clazz dep = getClazz(depName);
diff --git a/src/main/java24/org/vafer/jdependency/utils/DependencyUtils.java b/src/main/java24/org/vafer/jdependency/utils/DependencyUtils.java
new file mode 100644
index 00000000..e33c40cb
--- /dev/null
+++ b/src/main/java24/org/vafer/jdependency/utils/DependencyUtils.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2010-2024 The jdependency developers.
+ *
+ * Licensed 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.vafer.jdependency.utils;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.HashSet;
+import java.util.Set;
+import java.lang.classfile.ClassFile;
+import java.lang.classfile.ClassModel;
+import java.lang.classfile.constantpool.ClassEntry;
+import java.lang.classfile.constantpool.PoolEntry;
+import java.lang.classfile.constantpool.StringEntry;
+import java.lang.classfile.constantpool.Utf8Entry;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * internal - do not use
+ */
+public final class DependencyUtils {
+
+ private DependencyUtils() {}
+
+ private static final Pattern DESCRIPTOR_PATTERN = Pattern.compile("L([a-zA-Z0-9_/\\$]+);");
+
+ public static Set getDependenciesOfClass(final InputStream pInputStream) throws IOException {
+ final byte[] bytes = pInputStream.readAllBytes();
+ final ClassModel classModel = ClassFile.of().parse(bytes);
+ final Set dependencies = new HashSet<>();
+ final Set stringConstants = new HashSet<>();
+ for (PoolEntry entry : classModel.constantPool()) {
+ if (entry instanceof StringEntry stringEntry) {
+ stringConstants.add(stringEntry.utf8());
+ }
+ }
+ for (PoolEntry entry : classModel.constantPool()) {
+ if (entry instanceof ClassEntry classEntry) {
+ String className = classEntry.asInternalName().replace('/', '.');
+ if (className.startsWith("[")) {
+ Matcher m = DESCRIPTOR_PATTERN.matcher(classEntry.asInternalName());
+ while (m.find()) {
+ dependencies.add(m.group(1).replace('/', '.'));
+ }
+ } else {
+ dependencies.add(className);
+ }
+ } else if (entry instanceof Utf8Entry utf8Entry && !stringConstants.contains(utf8Entry)) {
+ String str = utf8Entry.stringValue();
+ if (str.indexOf('L') != -1 && str.indexOf(';') != -1) {
+ Matcher m = DESCRIPTOR_PATTERN.matcher(str);
+ while (m.find()) {
+ dependencies.add(m.group(1).replace('/', '.'));
+ }
+ }
+ }
+ }
+ return dependencies;
+ }
+
+ public static Set getDependenciesOfClass(final Class> pClass) throws IOException {
+ final String resource = "/" + pClass.getName().replace('.', '/') + ".class";
+ return getDependenciesOfClass(pClass.getResourceAsStream(resource));
+ }
+}
diff --git a/src/test/java/org/vafer/jdependency/DependencyUtilsTestCase.java b/src/test/java/org/vafer/jdependency/DependencyUtilsTestCase.java
index 85728de0..13e728ae 100644
--- a/src/test/java/org/vafer/jdependency/DependencyUtilsTestCase.java
+++ b/src/test/java/org/vafer/jdependency/DependencyUtilsTestCase.java
@@ -17,7 +17,9 @@
import org.junit.Test;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
import java.io.FileInputStream;
import java.io.IOException;
@@ -63,7 +65,7 @@ public void testVersions() {
}
- //@Test
+ @Test
public void testShouldFindDependenciesOfClassObject() throws Exception {
final Set dependencies = DependencyUtils.getDependenciesOfClass(Object.class);
final Set expectedDependencies = new HashSet(Arrays.asList(
@@ -97,12 +99,27 @@ public void testShouldFindDependenciesOfClassObject() throws Exception {
expectedDependencies.add("jdk.internal.misc.Blocker");
}
+ for (String optionalDep : Arrays.asList("jdk.internal.misc.Blocker", "java.lang.Long", "java.lang.VirtualThread", "java.lang.Thread")) {
+ if (dependencies.contains(optionalDep)) {
+ expectedDependencies.add(optionalDep);
+ } else {
+ expectedDependencies.remove(optionalDep);
+ }
+ }
+
assertEquals("deps should be the same for jdk " + jdk + " (" + System.getProperty("java.version") + ")",
expectedDependencies,
dependencies);
}
- //@Test
+ @Test
+ public void testShouldFindDependenciesOfTestCaseClass() throws Exception {
+ final Set dependencies = DependencyUtils.getDependenciesOfClass(DependencyUtilsTestCase.class);
+ assertTrue(dependencies.contains("org.vafer.jdependency.utils.DependencyUtils"));
+ assertTrue(dependencies.contains("org.junit.Test"));
+ }
+
+ @Test
public void testShouldThrowOnInvalidStream() throws Exception {
assertThrows(IOException.class, () -> {
final InputStream inputStream = new FileInputStream("nope");
@@ -110,4 +127,49 @@ public void testShouldThrowOnInvalidStream() throws Exception {
});
}
+ private static interface DummyInterface {}
+
+ private static final class ClassWithStringConstant {
+ private String getValue() {
+ return "Lorg/vafer/jdependency/NotARealDependency;";
+ }
+ }
+
+ @Test
+ public void testShouldFindDependenciesOfInterface() throws Exception {
+ final Set dependencies = DependencyUtils.getDependenciesOfClass(DummyInterface.class);
+ assertTrue(dependencies.contains("java.lang.Object"));
+ assertTrue(dependencies.contains("org.vafer.jdependency.DependencyUtilsTestCase$DummyInterface"));
+ }
+
+ @Test
+ public void testShouldNotFindClassNamesInStringConstants() throws Exception {
+ final Set dependencies = DependencyUtils.getDependenciesOfClass(ClassWithStringConstant.class);
+ assertTrue(dependencies.contains("java.lang.String"));
+ assertFalse(dependencies.contains("org.vafer.jdependency.NotARealDependency"));
+ }
+
+ @Test
+ public void testShouldThrowOnPrimitiveClass() throws Exception {
+ Exception exception = assertThrows(Exception.class, () -> {
+ DependencyUtils.getDependenciesOfClass(int.class);
+ });
+ assertTrue(exception instanceof NullPointerException || exception instanceof IOException);
+ }
+
+ @Test
+ public void testShouldThrowOnNullClass() throws Exception {
+ assertThrows(NullPointerException.class, () -> {
+ DependencyUtils.getDependenciesOfClass((Class>) null);
+ });
+ }
+
+ @Test
+ public void testShouldThrowOnNullStream() throws Exception {
+ Exception exception = assertThrows(Exception.class, () -> {
+ DependencyUtils.getDependenciesOfClass((InputStream) null);
+ });
+ assertTrue(exception instanceof NullPointerException || exception instanceof IOException);
+ }
+
}
diff --git a/src/test/java/org/vafer/jdependency/MultiReleaseJarSmokeTest.java b/src/test/java/org/vafer/jdependency/MultiReleaseJarSmokeTest.java
new file mode 100644
index 00000000..a879399f
--- /dev/null
+++ b/src/test/java/org/vafer/jdependency/MultiReleaseJarSmokeTest.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2010-2024 The jdependency developers.
+ *
+ * Licensed 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.vafer.jdependency;
+
+import java.io.IOException;
+import java.util.Set;
+
+import org.vafer.jdependency.utils.DependencyUtils;
+
+/**
+ * Smoke test executed against the packaged JAR, rather than the build output
+ * directories used by Surefire.
+ */
+public final class MultiReleaseJarSmokeTest {
+
+ private MultiReleaseJarSmokeTest() {}
+
+ public static void main(final String[] args) throws IOException {
+ final String resource = DependencyUtils.class
+ .getResource("DependencyUtils.class")
+ .toExternalForm();
+ final boolean versioned = resource.contains("META-INF/versions/24/");
+ final int javaVersion = javaVersion();
+
+ if (versioned != (javaVersion >= 24)) {
+ throw new AssertionError("unexpected DependencyUtils resource for Java "
+ + javaVersion + ": " + resource);
+ }
+
+ final Set dependencies = DependencyUtils
+ .getDependenciesOfClass(MultiReleaseJarSmokeTest.class);
+ if (!dependencies.contains(MultiReleaseJarSmokeTest.class.getName())) {
+ throw new AssertionError("DependencyUtils did not analyze the packaged JAR");
+ }
+ }
+
+ private static int javaVersion() {
+ final String version = System.getProperty("java.specification.version");
+ if (version.startsWith("1.")) {
+ return Integer.parseInt(version.substring(2));
+ }
+ final int dot = version.indexOf('.');
+ return Integer.parseInt(dot < 0 ? version : version.substring(0, dot));
+ }
+}