Skip to content

Commit b32a170

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

20 files changed

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