Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/workflows/maven.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ jobs:
build:
name: Build it
uses: codehaus-plexus/.github/.github/workflows/maven.yml@master
with:
matrix-include: >
[
{ "os": "ubuntu-latest", "jdk": "26", "distribution": "zulu" },
{ "os": "ubuntu-latest", "jdk": "27", "distribution": "zulu" },
{ "os": "ubuntu-latest", "jdk": "28-ea", "distribution": "temurin" }
]

# deploy:
# name: Deploy
Expand Down
35 changes: 35 additions & 0 deletions plexus-java/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,41 @@
</pluginManagement>
</build>
</profile>
<profile>
<!-- Prototype "option C" for #165: parse module-info.class with java.lang.classfile
(JEP 484, final since Java 24) instead of ASM. Only takes effect when this module is
built with a Java 24+ JDK; on an older JDK, src/main/java24 is simply not compiled and
the resulting jar has no META-INF/versions/24 layer. -->
<id>jdk24</id>
<activation>
<jdk>[24,)</jdk>
</activation>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<executions>
<execution>
<id>jdk24</id>
<goals>
<goal>compile</goal>
</goals>
<configuration>
<release>24</release>
<multiReleaseOutput>true</multiReleaseOutput>
<compileSourceRoots>
<compileSourceRoot>${project.basedir}/src/main/java24</compileSourceRoot>
</compileSourceRoots>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</pluginManagement>
</build>
</profile>
</profiles>

</project>
Original file line number Diff line number Diff line change
Expand Up @@ -109,4 +109,24 @@ static Path findVersionedModuleInfo(Path directory, JavaVersion jdkVersion) {
}

abstract JavaModuleDescriptor parse(InputStream in) throws IOException;

/**
* Whether this parser reads the {@code Module} attribute with the runtime's own
* {@code java.lang.classfile} API (JEP 484, final since Java 24) rather than ASM or
* {@code java.lang.module.ModuleDescriptor}.
* <p>
* {@link BinaryModuleInfoParser} is a multi-release class: depending on the JDK actually
* running the build, {@code new BinaryModuleInfoParser()} resolves to the Java 8 (ASM-backed),
* Java 9 ({@code java.lang.module}-backed) or Java 24 ({@code java.lang.classfile}-backed)
* implementation. {@link LocationManager#getBinaryModuleInfoParser(java.nio.file.Path)} cannot
* reference {@code java.lang.classfile} types directly &mdash; that package doesn't exist
* before Java 24, and {@code LocationManager} is compiled once at the Java 8 baseline &mdash;
* so it asks the resolved instance about its own capability instead.
*
* @return {@code true} for the {@code src/main/java24} implementation, {@code false} otherwise
* @since 1.7.0
*/
boolean isClassFileApiBased() {
return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -279,19 +279,21 @@ public <T> ResolvePathsResult<T> resolvePaths(final ResolvePathsRequest<T> reque

/**
* If the jdkHome is specified, its version it considered higher than the runtime java version.
* In that case ASM must be used to read the module descriptor
* In that case ASM must be used to read the module descriptor, unless this runtime's own
* {@link BinaryModuleInfoParser} is the Java 24 {@code java.lang.classfile}-based one: like the
* ASM parser (since codehaus-plexus/plexus-languages#239) it clamps a class file major version
* newer than it supports down to the newest one it knows, so a toolchain pointing at a
* different JDK than this process is running on is not a reason to prefer ASM over it.
*
* @param jdkHome
* @return
*/
ModuleInfoParser getBinaryModuleInfoParser(final Path jdkHome) {
final ModuleInfoParser binaryParser;
if (jdkHome == null) {
binaryParser = new BinaryModuleInfoParser();
} else {
binaryParser = new AsmModuleInfoParser();
BinaryModuleInfoParser runtimeParser = new BinaryModuleInfoParser();
if (jdkHome == null || runtimeParser.isClassFileApiBased()) {
return runtimeParser;
}
return binaryParser;
return new AsmModuleInfoParser();
}

private <T> JavaModuleDescriptor getMainModuleDescriptor(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package org.codehaus.plexus.languages.java.jpms;

/*
* 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.
*/

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.classfile.Attributes;
import java.lang.classfile.ClassFile;
import java.lang.classfile.ClassModel;
import java.lang.classfile.attribute.ModuleAttribute;
import java.lang.classfile.attribute.ModuleExportInfo;
import java.lang.classfile.attribute.ModuleProvideInfo;
import java.lang.classfile.attribute.ModuleRequireInfo;
import java.lang.classfile.constantpool.ClassEntry;
import java.lang.classfile.constantpool.ModuleEntry;
import java.lang.reflect.AccessFlag;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

import org.codehaus.plexus.languages.java.jpms.JavaModuleDescriptor.Builder;

/**
* Extract information from a module descriptor with the {@code java.lang.classfile} API
* (JEP 484), final since Java 24. Multi-release counterpart of the {@code src/main/java9}
* {@code BinaryModuleInfoParser}, which reads the same information with
* {@code java.lang.module.ModuleDescriptor}.
*
* @see <a href="https://openjdk.org/jeps/484">JEP 484: Class-File API</a>
* @see <a href="https://github.com/codehaus-plexus/plexus-languages/issues/165">plexus-languages#165</a>
* @since 1.7.0
*/
class BinaryModuleInfoParser extends AbstractBinaryModuleInfoParser {

// The highest class file major version this JDK's Class-File API knows how to read. A
// module-info.class produced by a newer toolchain than the running JDK (major version above
// this) makes ClassFile.of().parse(...) throw IllegalArgumentException, exactly like
// java.lang.module.ModuleDescriptor.read(...) and, before it was patched, ASM's ClassReader
// (see AsmModuleInfoParser and codehaus-plexus/plexus-languages#165: "Unsupported class file
// major version 72" on a JDK that only knows up to 71). The Module attribute format hasn't
// changed since Java 9, so it's safe to clamp the major version down to the newest one this
// runtime supports before parsing.
private static final int NEWEST_MAJOR_VERSION_SUPPORTED = ClassFile.latestMajorVersion();

@Override
JavaModuleDescriptor parse(InputStream in) throws IOException {
byte[] classBytes = toByteArray(in);

// bytes 6-7 are the big-endian major version
int majorVersion = ((classBytes[6] & 0xFF) << 8) | (classBytes[7] & 0xFF);
if (majorVersion > NEWEST_MAJOR_VERSION_SUPPORTED) {
classBytes[6] = (byte) (NEWEST_MAJOR_VERSION_SUPPORTED >>> 8);
classBytes[7] = (byte) NEWEST_MAJOR_VERSION_SUPPORTED;
}

ClassModel classModel = ClassFile.of().parse(classBytes);
ModuleAttribute moduleAttribute = classModel
.findAttribute(Attributes.module())
.orElseThrow(() -> new IOException("Not a module-info.class: no Module attribute found"));

Builder builder = JavaModuleDescriptor.newModule(
moduleAttribute.moduleName().name().stringValue());

for (ModuleRequireInfo requires : moduleAttribute.requires()) {
Set<AccessFlag> requiresFlags = requires.requiresFlags();
if (requiresFlags.contains(AccessFlag.STATIC_PHASE) || requiresFlags.contains(AccessFlag.TRANSITIVE)) {
Set<JavaModuleDescriptor.JavaRequires.JavaModifier> modifiers = new LinkedHashSet<>();
if (requiresFlags.contains(AccessFlag.STATIC_PHASE)) {
modifiers.add(JavaModuleDescriptor.JavaRequires.JavaModifier.STATIC);
}
if (requiresFlags.contains(AccessFlag.TRANSITIVE)) {
modifiers.add(JavaModuleDescriptor.JavaRequires.JavaModifier.TRANSITIVE);
}
builder.requires(modifiers, requires.requires().name().stringValue());
} else {
builder.requires(requires.requires().name().stringValue());
}
}

for (ModuleExportInfo exports : moduleAttribute.exports()) {
String packageName = exports.exportedPackage().name().stringValue().replace('/', '.');
if (exports.exportsTo().isEmpty()) {
builder.exports(packageName);
} else {
Set<String> targets = exports.exportsTo().stream()
.map(ModuleEntry::name)
.map(name -> name.stringValue())
.collect(Collectors.toCollection(LinkedHashSet::new));
builder.exports(packageName, targets);
}
}

for (ClassEntry uses : moduleAttribute.uses()) {
builder.uses(uses.name().stringValue().replace('/', '.'));
}

for (ModuleProvideInfo provides : moduleAttribute.provides()) {
String service = provides.provides().name().stringValue().replace('/', '.');
List<String> providers = provides.providesWith().stream()
.map(ClassEntry::name)
.map(name -> name.stringValue().replace('/', '.'))
.collect(Collectors.toList());
builder.provides(service, providers);
}

return builder.build();
}

@Override
boolean isClassFileApiBased() {
return true;
}

private static byte[] toByteArray(InputStream in) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
byte[] buffer = new byte[8192];
int read;
while ((read = in.read(buffer)) != -1) {
out.write(buffer, 0, read);
}
return out.toByteArray();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package org.codehaus.plexus.languages.java.jpms;

/*
* 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.
*/

import java.io.ByteArrayInputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

import org.codehaus.plexus.languages.java.jpms.JavaModuleDescriptor.JavaRequires;
import org.codehaus.plexus.languages.java.version.JavaVersion;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledForJreRange;
import org.junit.jupiter.api.condition.JRE;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;

/**
* Exercises the {@code java.lang.classfile}-based module descriptor parser added under
* {@code src/main/java24} to prototype "option C" for
* <a href="https://github.com/codehaus-plexus/plexus-languages/issues/165">#165</a>.
*
* <p>This class deliberately never mentions a {@code java.lang.classfile} type: it is compiled at
* this module's Java 8 baseline like every other test, and it only proves anything once run
* against the packaged multi-release jar. Directory classpaths (the {@code test} phase) are never
* multi-release aware, so {@code new BinaryModuleInfoParser()} there always resolves to whichever
* variant this module's own compilation produced last, regardless of the running JDK; only the
* jar's {@code Multi-Release: true} manifest entry, honoured by {@code JarFile}/{@code
* URLClassLoader} once failsafe reruns the suite against {@code target/*.jar} (see this module's
* pom.xml and {@link LocationManagerIT}), makes the JVM pick the {@code META-INF/versions/24}
* class on a Java 24+ runtime.
*/
@EnabledForJreRange(min = JRE.JAVA_24, disabledReason = "src/main/java24 is only in the jar's MR layer on a 24+ build")
class ClassFileModuleInfoParserIT {

private final LocationManager locationManager = new LocationManager();

private final Path moduleInfo = Paths.get("src/test/test-data/dir.descriptor.requires/out/module-info.class");

@Test
void parsesAnOrdinaryModuleDescriptor() throws Exception {
JavaModuleDescriptor descriptor =
locationManager.getBinaryModuleInfoParser(null).getModuleDescriptor(moduleInfo.getParent());

assertNotNull(descriptor);
assertThat(descriptor.requires()).hasSize(5);
assertEquals(expectedRequires(), descriptor.requires());
}

@Test
void clampsAModuleDescriptorNewerThanTheRuntimeSupports() throws Exception {
byte[] classBytes = Files.readAllBytes(moduleInfo);

// Bytes 6-7 are the big-endian class file major version. This JDK's classfile major
// version is its feature version + 44 (Java 9 -> 53, ..., Java 24 -> 68, see
// java.lang.classfile.ClassFile.JAVA_9_VERSION..JAVA_24_VERSION); one past that is a
// version no released JDK can parse yet, e.g. a toolchain one release ahead of this one.
int unsupportedMajorVersion = JavaVersion.JAVA_SPECIFICATION_VERSION.getMajorVersion() + 44 + 1;
classBytes[6] = (byte) (unsupportedMajorVersion >>> 8);
classBytes[7] = (byte) unsupportedMajorVersion;

ModuleInfoParser parser = locationManager.getBinaryModuleInfoParser(null);
JavaModuleDescriptor descriptor;
try (ByteArrayInputStream in = new ByteArrayInputStream(classBytes)) {
descriptor = ((AbstractBinaryModuleInfoParser) parser).parse(in);
}

assertNotNull(descriptor);
assertThat(descriptor.requires()).hasSize(5);
assertEquals(expectedRequires(), descriptor.requires());
}

@Test
void isPreferredOverAsmForAToolchainOnA24PlusRuntime() {
// #165: a toolchain (jdkHome) used to always fall back to AsmModuleInfoParser, on the
// assumption that only ASM could tolerate a class file version newer than this process
// understands. The java.lang.classfile parser clamps just like ASM does (see
// codehaus-plexus/plexus-languages#239), so on a 24+ runtime it should be used here too.
ModuleInfoParser parser = locationManager.getBinaryModuleInfoParser(Paths.get("some/jdk/home"));

assertThat(parser).isNotInstanceOf(AsmModuleInfoParser.class);
}

private static Set<JavaRequires> expectedRequires() {
return JavaModuleDescriptor.newAutomaticModule("_")
.requires("java.base")
.requires("mod_r")
.requires(Collections.singleton(JavaRequires.JavaModifier.STATIC), "mod_r_s")
.requires(Collections.singleton(JavaRequires.JavaModifier.TRANSITIVE), "mod_r_t")
.requires(
new HashSet<>(
Arrays.asList(JavaRequires.JavaModifier.STATIC, JavaRequires.JavaModifier.TRANSITIVE)),
"mod_r_s_t")
.build()
.requires();
}
}
Loading
Loading