Skip to content

Commit 5c06f1b

Browse files
jorsolrfscholte
authored andcommitted
Add java bytecode class file version detection
Signed-off-by: Jorge Solórzano <[email protected]>
1 parent 537321d commit 5c06f1b

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 JavaClassfileVersion {
34+
35+
private final int major;
36+
private final int minor;
37+
38+
JavaClassfileVersion(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 JavaClassfileVersion}.
49+
*
50+
* @param bytes {@code byte[]} of the Java class file
51+
* @return the {@link JavaClassfileVersion} of the byte array
52+
*/
53+
public static JavaClassfileVersion of(byte[] bytes) {
54+
return JavaClassfileVersionParser.of(bytes);
55+
}
56+
57+
/**
58+
* Reads the bytecode of a Java class file and returns the
59+
* {@link JavaClassfileVersion}.
60+
*
61+
* @param path {@link Path} of the Java class file
62+
* @return the {@link JavaClassfileVersion} of the path java class
63+
*/
64+
public static JavaClassfileVersion 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 JavaClassfileVersion)) return false;
136+
JavaClassfileVersion other = (JavaClassfileVersion) 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 JavaClassfileVersion}.
30+
*
31+
* @author Jorge Solórzano
32+
*/
33+
final class JavaClassfileVersionParser {
34+
35+
private JavaClassfileVersionParser() {}
36+
37+
/**
38+
* Reads the bytecode of a Java class file and returns the {@link JavaClassfileVersion}.
39+
*
40+
* @param in {@code byte[]} of the Java class file
41+
* @return the {@link JavaClassfileVersion} of the input stream
42+
*/
43+
public static JavaClassfileVersion 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 file header");
47+
}
48+
int minor = data.readUnsignedShort();
49+
int major = data.readUnsignedShort();
50+
return new JavaClassfileVersion(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+
JavaClassfileVersion classVersion = JavaClassfileVersion.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+
JavaClassfileVersion previewClass = JavaClassfileVersion.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 JavaClassfileVersion(44, 0),
63+
"Java class major version must be 45 or above.");
64+
}
65+
66+
@Test
67+
void equalsContract() {
68+
JavaClassfileVersion javaClassVersion = new JavaClassfileVersion(65, 0);
69+
JavaClassfileVersion previewFeature = new JavaClassfileVersion(65, 65535);
70+
assertNotEquals(javaClassVersion, previewFeature);
71+
assertNotEquals(javaClassVersion.hashCode(), previewFeature.hashCode());
72+
73+
JavaClassfileVersion javaClassVersionOther = new JavaClassfileVersion(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)