Skip to content

Commit 5c06f1b

Browse files
Jorge Solórzanorfscholte
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
Lines changed: 141 additions & 0 deletions
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+
}
Lines changed: 55 additions & 0 deletions
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

Lines changed: 1 addition & 1 deletion
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."));
Lines changed: 79 additions & 0 deletions
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.

0 commit comments

Comments
 (0)