Skip to content

Commit 6c93a04

Browse files
committed
Add java bytecode class file version detection
Signed-off-by: Jorge Solórzano <[email protected]>
1 parent 9d5683f commit 6c93a04

20 files changed

+276
-1
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
package org.codehaus.plexus.languages.java.version;
2+
3+
import java.io.IOException;
4+
import java.io.UncheckedIOException;
5+
import java.nio.file.Files;
6+
import java.nio.file.Path;
7+
8+
/*
9+
* Licensed to the Apache Software Foundation (ASF) under one
10+
* or more contributor license agreements. See the NOTICE file
11+
* distributed with this work for additional information
12+
* regarding copyright ownership. The ASF licenses this file
13+
* to you under the Apache License, Version 2.0 (the
14+
* "License"); you may not use this file except in compliance
15+
* with the License. You may obtain a copy of the License at
16+
*
17+
* http://www.apache.org/licenses/LICENSE-2.0
18+
*
19+
* Unless required by applicable law or agreed to in writing,
20+
* software distributed under the License is distributed on an
21+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
22+
* KIND, either express or implied. See the License for the
23+
* specific language governing permissions and limitations
24+
* under the License.
25+
*/
26+
27+
/**
28+
* Reads the bytecode of a Java class to detect the major, minor and Java
29+
* version that was compiled.
30+
*
31+
* @author Jorge Solórzano
32+
*/
33+
public final class JavaClassVersion {
34+
35+
private final int major;
36+
private final int minor;
37+
38+
JavaClassVersion(int major, int minor) {
39+
if (major < 45) {
40+
throw new IllegalArgumentException("Java class major version must be 45 or above.");
41+
}
42+
this.major = major;
43+
this.minor = minor;
44+
}
45+
46+
/**
47+
* Reads the bytecode of a Java class file and returns the
48+
* {@link JavaClassVersion}.
49+
*
50+
* @param bytes {@code byte[]} of the Java class file
51+
* @return the {@link JavaClassVersion} of the byte array
52+
*/
53+
public static JavaClassVersion of(byte[] bytes) {
54+
return JavaClassVersionParser.of(bytes);
55+
}
56+
57+
/**
58+
* Reads the bytecode of a Java class file and returns the
59+
* {@link JavaClassVersion}.
60+
*
61+
* @param path {@link Path} of the Java class file
62+
* @return the {@link JavaClassVersion} of the path java class
63+
*/
64+
public static JavaClassVersion of(Path path) {
65+
try {
66+
byte[] readAllBytes = Files.readAllBytes(path);
67+
return of(readAllBytes);
68+
} catch (IOException ex) {
69+
throw new UncheckedIOException(ex);
70+
}
71+
}
72+
73+
/**
74+
* JavaVersion of the class file version detected.
75+
*
76+
* @return JavaVersion based on the major version of the class file.
77+
*/
78+
public JavaVersion javaVersion() {
79+
int javaVer = major - 44;
80+
String javaVersion = javaVer < 9 ? "1." + javaVer : Integer.toString(javaVer);
81+
82+
return JavaVersion.parse(javaVersion);
83+
}
84+
85+
/**
86+
* Returns the major version of the parsed classfile.
87+
*
88+
* @return the major classfile version
89+
*/
90+
public int majorVersion() {
91+
return major;
92+
}
93+
94+
/**
95+
* Returns the minor version of the parsed classfile.
96+
*
97+
* @return the minor classfile version
98+
*/
99+
public int minorVersion() {
100+
return minor;
101+
}
102+
103+
/**
104+
* Returns if the classfile use preview features.
105+
*
106+
* @return {@code true} if the classfile use preview features.
107+
*/
108+
public boolean isPreview() {
109+
return minor == 65535;
110+
}
111+
112+
/**
113+
* Returns a String representation of the Java class file version, e.g.
114+
* {@code 65.0 (Java 21)}.
115+
*
116+
* @return String representation of the Java class file version
117+
*/
118+
@Override
119+
public String toString() {
120+
return major + "." + minor + " (Java " + javaVersion() + ")";
121+
}
122+
123+
@Override
124+
public int hashCode() {
125+
final int prime = 31;
126+
int result = 1;
127+
result = prime * result + major;
128+
result = prime * result + minor;
129+
return result;
130+
}
131+
132+
@Override
133+
public boolean equals(Object obj) {
134+
if (this == obj) return true;
135+
if (!(obj instanceof JavaClassVersion)) return false;
136+
JavaClassVersion other = (JavaClassVersion) obj;
137+
if (major != other.major) return false;
138+
if (minor != other.minor) return false;
139+
return true;
140+
}
141+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package org.codehaus.plexus.languages.java.version;
2+
3+
import java.io.ByteArrayInputStream;
4+
import java.io.DataInputStream;
5+
import java.io.IOException;
6+
import java.io.UncheckedIOException;
7+
8+
/*
9+
* Licensed to the Apache Software Foundation (ASF) under one
10+
* or more contributor license agreements. See the NOTICE file
11+
* distributed with this work for additional information
12+
* regarding copyright ownership. The ASF licenses this file
13+
* to you under the Apache License, Version 2.0 (the
14+
* "License"); you may not use this file except in compliance
15+
* with the License. You may obtain a copy of the License at
16+
*
17+
* http://www.apache.org/licenses/LICENSE-2.0
18+
*
19+
* Unless required by applicable law or agreed to in writing,
20+
* software distributed under the License is distributed on an
21+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
22+
* KIND, either express or implied. See the License for the
23+
* specific language governing permissions and limitations
24+
* under the License.
25+
*/
26+
27+
/**
28+
* This class is intented to be package-private and consumed by
29+
* {@link JavaClassVersion}.
30+
*
31+
* @author Jorge Solórzano
32+
*/
33+
final class JavaClassVersionParser {
34+
35+
private JavaClassVersionParser() {}
36+
37+
/**
38+
* Reads the bytecode of a Java class file and returns the {@link JavaClassVersion}.
39+
*
40+
* @param in {@code byte[]} of the Java class file
41+
* @return the {@link JavaClassVersion} of the input stream
42+
*/
43+
public static JavaClassVersion of(byte[] bytes) {
44+
try (final DataInputStream data = new DataInputStream(new ByteArrayInputStream(bytes))) {
45+
if (0xCAFEBABE != data.readInt()) {
46+
throw new IOException("Invalid java class header");
47+
}
48+
int minor = data.readUnsignedShort();
49+
int major = data.readUnsignedShort();
50+
return new JavaClassVersion(major, minor);
51+
} catch (IOException ex) {
52+
throw new UncheckedIOException(ex);
53+
}
54+
}
55+
}

plexus-java/src/main/java/org/codehaus/plexus/languages/java/version/JavaVersion.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ private JavaVersion(String rawVersion, boolean isMajor) {
6060
* Actual parsing is done when calling {@link #compareTo(JavaVersion)}
6161
*
6262
* @param s the version string, never {@code null}
63-
* @return the version wrapped in a JavadocVersion
63+
* @return the version wrapped in a JavaVersion
6464
*/
6565
public static JavaVersion parse(String s) {
6666
return new JavaVersion(s, !s.startsWith("1."));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package org.codehaus.plexus.languages.java.version;
2+
3+
import java.io.IOException;
4+
import java.io.UncheckedIOException;
5+
import java.nio.file.DirectoryStream;
6+
import java.nio.file.Files;
7+
import java.nio.file.Path;
8+
import java.nio.file.Paths;
9+
import java.util.List;
10+
import java.util.stream.Collectors;
11+
import java.util.stream.Stream;
12+
import java.util.stream.StreamSupport;
13+
14+
import org.junit.jupiter.api.Test;
15+
import org.junit.jupiter.params.ParameterizedTest;
16+
import org.junit.jupiter.params.provider.MethodSource;
17+
18+
import static org.junit.jupiter.api.Assertions.assertEquals;
19+
import static org.junit.jupiter.api.Assertions.assertNotEquals;
20+
import static org.junit.jupiter.api.Assertions.assertThrows;
21+
import static org.junit.jupiter.api.Assertions.assertTrue;
22+
23+
class JavaClassVersionTest {
24+
25+
@ParameterizedTest
26+
@MethodSource("provideClassFiles")
27+
void testFilesClassVersions(Path filePath) {
28+
String fileName = filePath.getFileName().toString();
29+
int javaVersion = Integer.valueOf(fileName.substring(fileName.indexOf("-") + 1, fileName.length() - 6));
30+
JavaClassVersion classVersion = JavaClassVersion.of(filePath);
31+
assertEquals(javaVersion + 44, classVersion.majorVersion());
32+
assertEquals(0, classVersion.minorVersion());
33+
assertEquals(JavaVersion.parse("" + javaVersion), classVersion.javaVersion());
34+
}
35+
36+
static Stream<Path> provideClassFiles() {
37+
List<Path> paths;
38+
try (DirectoryStream<Path> directoryStream =
39+
Files.newDirectoryStream(Paths.get("src/test/resources/classfile.version/"), "*-[0-9]?.class")) {
40+
paths = StreamSupport.stream(directoryStream.spliterator(), false)
41+
.filter(Files::isRegularFile)
42+
.collect(Collectors.toList());
43+
} catch (IOException ex) {
44+
throw new UncheckedIOException(ex);
45+
}
46+
return paths.stream();
47+
}
48+
49+
@Test
50+
void testJavaClassPreview() {
51+
Path previewFile = Paths.get("src/test/resources/classfile.version/helloworld-preview.class");
52+
JavaClassVersion previewClass = JavaClassVersion.of(previewFile);
53+
assertTrue(previewClass.isPreview());
54+
assertEquals(20 + 44, previewClass.majorVersion());
55+
assertEquals(JavaVersion.parse("20"), previewClass.javaVersion());
56+
}
57+
58+
@Test
59+
void testJavaClassVersionMajor45orAbove() {
60+
assertThrows(
61+
IllegalArgumentException.class,
62+
() -> new JavaClassVersion(44, 0),
63+
"Java class major version must be 45 or above.");
64+
}
65+
66+
@Test
67+
void equalsContract() {
68+
JavaClassVersion javaClassVersion = new JavaClassVersion(65, 0);
69+
JavaClassVersion previewFeature = new JavaClassVersion(65, 65535);
70+
assertNotEquals(javaClassVersion, previewFeature);
71+
assertNotEquals(javaClassVersion.hashCode(), previewFeature.hashCode());
72+
73+
JavaClassVersion javaClassVersionOther = new JavaClassVersion(65, 0);
74+
assertEquals(javaClassVersion, javaClassVersionOther);
75+
assertEquals(javaClassVersion.hashCode(), javaClassVersionOther.hashCode());
76+
assertEquals(javaClassVersion.javaVersion(), javaClassVersionOther.javaVersion());
77+
assertEquals(javaClassVersion.javaVersion(), previewFeature.javaVersion());
78+
}
79+
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

0 commit comments

Comments
 (0)