diff --git a/plexus-java/src/main/java/org/codehaus/plexus/languages/java/version/JavaClassfileVersion.java b/plexus-java/src/main/java/org/codehaus/plexus/languages/java/version/JavaClassfileVersion.java new file mode 100644 index 0000000..e909f8f --- /dev/null +++ b/plexus-java/src/main/java/org/codehaus/plexus/languages/java/version/JavaClassfileVersion.java @@ -0,0 +1,141 @@ +package org.codehaus.plexus.languages.java.version; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; + +/* + * 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. + */ + +/** + * Reads the bytecode of a Java class to detect the major, minor and Java + * version that was compiled. + * + * @author Jorge Solórzano + */ +public final class JavaClassfileVersion { + + private final int major; + private final int minor; + + JavaClassfileVersion(int major, int minor) { + if (major < 45) { + throw new IllegalArgumentException("Java class major version must be 45 or above."); + } + this.major = major; + this.minor = minor; + } + + /** + * Reads the bytecode of a Java class file and returns the + * {@link JavaClassfileVersion}. + * + * @param bytes {@code byte[]} of the Java class file + * @return the {@link JavaClassfileVersion} of the byte array + */ + public static JavaClassfileVersion of(byte[] bytes) { + return JavaClassfileVersionParser.of(bytes); + } + + /** + * Reads the bytecode of a Java class file and returns the + * {@link JavaClassfileVersion}. + * + * @param path {@link Path} of the Java class file + * @return the {@link JavaClassfileVersion} of the path java class + */ + public static JavaClassfileVersion of(Path path) { + try { + byte[] readAllBytes = Files.readAllBytes(path); + return of(readAllBytes); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + + /** + * JavaVersion of the class file version detected. + * + * @return JavaVersion based on the major version of the class file. + */ + public JavaVersion javaVersion() { + int javaVer = major - 44; + String javaVersion = javaVer < 9 ? "1." + javaVer : Integer.toString(javaVer); + + return JavaVersion.parse(javaVersion); + } + + /** + * Returns the major version of the parsed classfile. + * + * @return the major classfile version + */ + public int majorVersion() { + return major; + } + + /** + * Returns the minor version of the parsed classfile. + * + * @return the minor classfile version + */ + public int minorVersion() { + return minor; + } + + /** + * Returns if the classfile use preview features. + * + * @return {@code true} if the classfile use preview features. + */ + public boolean isPreview() { + return minor == 65535; + } + + /** + * Returns a String representation of the Java class file version, e.g. + * {@code 65.0 (Java 21)}. + * + * @return String representation of the Java class file version + */ + @Override + public String toString() { + return major + "." + minor + " (Java " + javaVersion() + ")"; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + major; + result = prime * result + minor; + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof JavaClassfileVersion)) return false; + JavaClassfileVersion other = (JavaClassfileVersion) obj; + if (major != other.major) return false; + if (minor != other.minor) return false; + return true; + } +} diff --git a/plexus-java/src/main/java/org/codehaus/plexus/languages/java/version/JavaClassfileVersionParser.java b/plexus-java/src/main/java/org/codehaus/plexus/languages/java/version/JavaClassfileVersionParser.java new file mode 100644 index 0000000..a994180 --- /dev/null +++ b/plexus-java/src/main/java/org/codehaus/plexus/languages/java/version/JavaClassfileVersionParser.java @@ -0,0 +1,55 @@ +package org.codehaus.plexus.languages.java.version; + +import java.io.ByteArrayInputStream; +import java.io.DataInputStream; +import java.io.IOException; +import java.io.UncheckedIOException; + +/* + * 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. + */ + +/** + * This class is intented to be package-private and consumed by + * {@link JavaClassfileVersion}. + * + * @author Jorge Solórzano + */ +final class JavaClassfileVersionParser { + + private JavaClassfileVersionParser() {} + + /** + * Reads the bytecode of a Java class file and returns the {@link JavaClassfileVersion}. + * + * @param in {@code byte[]} of the Java class file + * @return the {@link JavaClassfileVersion} of the input stream + */ + public static JavaClassfileVersion of(byte[] bytes) { + try (final DataInputStream data = new DataInputStream(new ByteArrayInputStream(bytes))) { + if (0xCAFEBABE != data.readInt()) { + throw new IOException("Invalid java class file header"); + } + int minor = data.readUnsignedShort(); + int major = data.readUnsignedShort(); + return new JavaClassfileVersion(major, minor); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } +} diff --git a/plexus-java/src/main/java/org/codehaus/plexus/languages/java/version/JavaVersion.java b/plexus-java/src/main/java/org/codehaus/plexus/languages/java/version/JavaVersion.java index b80e7cc..245527e 100644 --- a/plexus-java/src/main/java/org/codehaus/plexus/languages/java/version/JavaVersion.java +++ b/plexus-java/src/main/java/org/codehaus/plexus/languages/java/version/JavaVersion.java @@ -60,7 +60,7 @@ private JavaVersion(String rawVersion, boolean isMajor) { * Actual parsing is done when calling {@link #compareTo(JavaVersion)} * * @param s the version string, never {@code null} - * @return the version wrapped in a JavadocVersion + * @return the version wrapped in a JavaVersion */ public static JavaVersion parse(String s) { return new JavaVersion(s, !s.startsWith("1.")); diff --git a/plexus-java/src/test/java/org/codehaus/plexus/languages/java/version/JavaClassVersionTest.java b/plexus-java/src/test/java/org/codehaus/plexus/languages/java/version/JavaClassVersionTest.java new file mode 100644 index 0000000..eaec9c5 --- /dev/null +++ b/plexus-java/src/test/java/org/codehaus/plexus/languages/java/version/JavaClassVersionTest.java @@ -0,0 +1,79 @@ +package org.codehaus.plexus.languages.java.version; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class JavaClassVersionTest { + + @ParameterizedTest + @MethodSource("provideClassFiles") + void testFilesClassVersions(Path filePath) { + String fileName = filePath.getFileName().toString(); + int javaVersion = Integer.valueOf(fileName.substring(fileName.indexOf("-") + 1, fileName.length() - 6)); + JavaClassfileVersion classVersion = JavaClassfileVersion.of(filePath); + assertEquals(javaVersion + 44, classVersion.majorVersion()); + assertEquals(0, classVersion.minorVersion()); + assertEquals(JavaVersion.parse("" + javaVersion), classVersion.javaVersion()); + } + + static Stream provideClassFiles() { + List paths; + try (DirectoryStream directoryStream = + Files.newDirectoryStream(Paths.get("src/test/resources/classfile.version/"), "*-[0-9]?.class")) { + paths = StreamSupport.stream(directoryStream.spliterator(), false) + .filter(Files::isRegularFile) + .collect(Collectors.toList()); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + return paths.stream(); + } + + @Test + void testJavaClassPreview() { + Path previewFile = Paths.get("src/test/resources/classfile.version/helloworld-preview.class"); + JavaClassfileVersion previewClass = JavaClassfileVersion.of(previewFile); + assertTrue(previewClass.isPreview()); + assertEquals(20 + 44, previewClass.majorVersion()); + assertEquals(JavaVersion.parse("20"), previewClass.javaVersion()); + } + + @Test + void testJavaClassVersionMajor45orAbove() { + assertThrows( + IllegalArgumentException.class, + () -> new JavaClassfileVersion(44, 0), + "Java class major version must be 45 or above."); + } + + @Test + void equalsContract() { + JavaClassfileVersion javaClassVersion = new JavaClassfileVersion(65, 0); + JavaClassfileVersion previewFeature = new JavaClassfileVersion(65, 65535); + assertNotEquals(javaClassVersion, previewFeature); + assertNotEquals(javaClassVersion.hashCode(), previewFeature.hashCode()); + + JavaClassfileVersion javaClassVersionOther = new JavaClassfileVersion(65, 0); + assertEquals(javaClassVersion, javaClassVersionOther); + assertEquals(javaClassVersion.hashCode(), javaClassVersionOther.hashCode()); + assertEquals(javaClassVersion.javaVersion(), javaClassVersionOther.javaVersion()); + assertEquals(javaClassVersion.javaVersion(), previewFeature.javaVersion()); + } +} diff --git a/plexus-java/src/test/resources/classfile.version/helloworld-10.class b/plexus-java/src/test/resources/classfile.version/helloworld-10.class new file mode 100644 index 0000000..9251951 Binary files /dev/null and b/plexus-java/src/test/resources/classfile.version/helloworld-10.class differ diff --git a/plexus-java/src/test/resources/classfile.version/helloworld-11.class b/plexus-java/src/test/resources/classfile.version/helloworld-11.class new file mode 100644 index 0000000..e7b2672 Binary files /dev/null and b/plexus-java/src/test/resources/classfile.version/helloworld-11.class differ diff --git a/plexus-java/src/test/resources/classfile.version/helloworld-12.class b/plexus-java/src/test/resources/classfile.version/helloworld-12.class new file mode 100644 index 0000000..5cd0678 Binary files /dev/null and b/plexus-java/src/test/resources/classfile.version/helloworld-12.class differ diff --git a/plexus-java/src/test/resources/classfile.version/helloworld-13.class b/plexus-java/src/test/resources/classfile.version/helloworld-13.class new file mode 100644 index 0000000..e95b0b4 Binary files /dev/null and b/plexus-java/src/test/resources/classfile.version/helloworld-13.class differ diff --git a/plexus-java/src/test/resources/classfile.version/helloworld-14.class b/plexus-java/src/test/resources/classfile.version/helloworld-14.class new file mode 100644 index 0000000..c0d0662 Binary files /dev/null and b/plexus-java/src/test/resources/classfile.version/helloworld-14.class differ diff --git a/plexus-java/src/test/resources/classfile.version/helloworld-15.class b/plexus-java/src/test/resources/classfile.version/helloworld-15.class new file mode 100644 index 0000000..1b62db6 Binary files /dev/null and b/plexus-java/src/test/resources/classfile.version/helloworld-15.class differ diff --git a/plexus-java/src/test/resources/classfile.version/helloworld-16.class b/plexus-java/src/test/resources/classfile.version/helloworld-16.class new file mode 100644 index 0000000..bade14e Binary files /dev/null and b/plexus-java/src/test/resources/classfile.version/helloworld-16.class differ diff --git a/plexus-java/src/test/resources/classfile.version/helloworld-17.class b/plexus-java/src/test/resources/classfile.version/helloworld-17.class new file mode 100644 index 0000000..461c9cd Binary files /dev/null and b/plexus-java/src/test/resources/classfile.version/helloworld-17.class differ diff --git a/plexus-java/src/test/resources/classfile.version/helloworld-18.class b/plexus-java/src/test/resources/classfile.version/helloworld-18.class new file mode 100644 index 0000000..5ae0ca1 Binary files /dev/null and b/plexus-java/src/test/resources/classfile.version/helloworld-18.class differ diff --git a/plexus-java/src/test/resources/classfile.version/helloworld-19.class b/plexus-java/src/test/resources/classfile.version/helloworld-19.class new file mode 100644 index 0000000..bd24b66 Binary files /dev/null and b/plexus-java/src/test/resources/classfile.version/helloworld-19.class differ diff --git a/plexus-java/src/test/resources/classfile.version/helloworld-20.class b/plexus-java/src/test/resources/classfile.version/helloworld-20.class new file mode 100644 index 0000000..99f757e Binary files /dev/null and b/plexus-java/src/test/resources/classfile.version/helloworld-20.class differ diff --git a/plexus-java/src/test/resources/classfile.version/helloworld-21.class b/plexus-java/src/test/resources/classfile.version/helloworld-21.class new file mode 100644 index 0000000..e57d1b9 Binary files /dev/null and b/plexus-java/src/test/resources/classfile.version/helloworld-21.class differ diff --git a/plexus-java/src/test/resources/classfile.version/helloworld-22.class b/plexus-java/src/test/resources/classfile.version/helloworld-22.class new file mode 100644 index 0000000..ff268d8 Binary files /dev/null and b/plexus-java/src/test/resources/classfile.version/helloworld-22.class differ diff --git a/plexus-java/src/test/resources/classfile.version/helloworld-8.class b/plexus-java/src/test/resources/classfile.version/helloworld-8.class new file mode 100644 index 0000000..c1ad791 Binary files /dev/null and b/plexus-java/src/test/resources/classfile.version/helloworld-8.class differ diff --git a/plexus-java/src/test/resources/classfile.version/helloworld-9.class b/plexus-java/src/test/resources/classfile.version/helloworld-9.class new file mode 100644 index 0000000..06772b5 Binary files /dev/null and b/plexus-java/src/test/resources/classfile.version/helloworld-9.class differ diff --git a/plexus-java/src/test/resources/classfile.version/helloworld-preview.class b/plexus-java/src/test/resources/classfile.version/helloworld-preview.class new file mode 100644 index 0000000..7b77d72 Binary files /dev/null and b/plexus-java/src/test/resources/classfile.version/helloworld-preview.class differ