diff --git a/hibernate-graalvm/hibernate-graalvm.gradle b/hibernate-graalvm/hibernate-graalvm.gradle index 628488a6c6..de9def0022 100644 --- a/hibernate-graalvm/hibernate-graalvm.gradle +++ b/hibernate-graalvm/hibernate-graalvm.gradle @@ -15,4 +15,5 @@ dependencies { compileOnly "org.graalvm.sdk:graal-sdk:22.2.0" testImplementation project( ':hibernate-core' ) + testImplementation libs.jandex } diff --git a/hibernate-graalvm/src/test/java/org/hibernate/graalvm/internal/CodeSource.java b/hibernate-graalvm/src/test/java/org/hibernate/graalvm/internal/CodeSource.java new file mode 100644 index 0000000000..d54b5f691a --- /dev/null +++ b/hibernate-graalvm/src/test/java/org/hibernate/graalvm/internal/CodeSource.java @@ -0,0 +1,56 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.graalvm.internal; + +import java.io.Closeable; +import java.io.IOException; +import java.net.URI; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Map; + +class CodeSource implements Closeable { + + public static CodeSource open(URI location) throws IOException { + if ( "jar".equals( location.getScheme() ) ) { + var fs = FileSystems.newFileSystem( location, Map.of() ); + return new CodeSource( fs, fs.getRootDirectories().iterator().next() ); + } + else if ( "file".equals( location.getScheme() ) && location.getPath().endsWith( ".jar" ) ) { + location = URI.create( "jar:" + location ); + var fs = FileSystems.newFileSystem( location, Map.of() ); + return new CodeSource( fs, fs.getRootDirectories().iterator().next() ); + } + else if ( "file".equals( location.getScheme() ) ) { + return new CodeSource( null, Paths.get( location ) ); + } + else { + throw new IllegalArgumentException( "Unsupported URI: " + location ); + } + } + + private final FileSystem toClose; + private final Path root; + + private CodeSource(FileSystem toClose, Path root) { + this.toClose = toClose; + this.root = root; + } + + @Override + public void close() throws IOException { + if ( toClose != null ) { + toClose.close(); + } + } + + public Path getRoot() { + return root; + } +} diff --git a/hibernate-graalvm/src/test/java/org/hibernate/graalvm/internal/JandexTestUtils.java b/hibernate-graalvm/src/test/java/org/hibernate/graalvm/internal/JandexTestUtils.java new file mode 100644 index 0000000000..ff3c6842b5 --- /dev/null +++ b/hibernate-graalvm/src/test/java/org/hibernate/graalvm/internal/JandexTestUtils.java @@ -0,0 +1,95 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.graalvm.internal; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Modifier; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Iterator; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.DotName; +import org.jboss.jandex.Index; +import org.jboss.jandex.Indexer; + +public final class JandexTestUtils { + + private JandexTestUtils() { + } + + public static Index indexJar(Class clazz) { + return indexClasses( determineJarLocation( clazz ) ); + } + + private static URI determineJarLocation(Class clazz) { + URL url = clazz.getProtectionDomain().getCodeSource().getLocation(); + try { + return url.toURI(); + } + catch (URISyntaxException e) { + throw new IllegalStateException( "Cannot retrieve URI for JAR of " + clazz + "?", e ); + } + } + + private static Index indexClasses(URI classesUri) { + try ( CodeSource cs = CodeSource.open( classesUri ) ) { + Indexer indexer = new Indexer(); + try ( Stream stream = Files.walk( cs.getRoot() ) ) { + for ( Iterator it = stream.iterator(); it.hasNext(); ) { + Path path = it.next(); + if ( path.getFileName() == null || !path.getFileName().toString().endsWith( ".class" ) ) { + continue; + } + try ( InputStream inputStream = Files.newInputStream( path ) ) { + indexer.index( inputStream ); + } + } + } + return indexer.complete(); + } + catch (RuntimeException | IOException e) { + throw new IllegalStateException( "Cannot index classes at " + classesUri, e ); + } + } + + public static Class load(DotName className) { + try { + return Class.forName( className.toString() ); + } + catch (ClassNotFoundException e) { + throw new RuntimeException( "Could not load class " + className, e ); + } + } + + public static Set> findConcreteNamedImplementors(Index index, Class... interfaces) { + return Arrays.stream( interfaces ).map( DotName::createSimple ) + .flatMap( n -> findConcreteNamedImplementors( index, n ).stream() ) + .collect( Collectors.toSet() ); + } + + private static Set> findConcreteNamedImplementors(Index index, DotName interfaceDotName) { + assertThat( index.getClassByName( interfaceDotName ) ).isNotNull(); + return index.getAllKnownImplementors( interfaceDotName ).stream() + .filter( c -> !c.isInterface() + // Ignore anonymous classes + && c.simpleName() != null ) + .map( ClassInfo::name ) + .map( JandexTestUtils::load ) + .filter( c -> ( c.getModifiers() & Modifier.ABSTRACT ) == 0 ) + .collect( Collectors.toSet() ); + } + +} diff --git a/hibernate-graalvm/src/test/java/org/hibernate/graalvm/internal/StaticClassListsTest.java b/hibernate-graalvm/src/test/java/org/hibernate/graalvm/internal/StaticClassListsTest.java index 2a73c0e026..39830fa33d 100644 --- a/hibernate-graalvm/src/test/java/org/hibernate/graalvm/internal/StaticClassListsTest.java +++ b/hibernate-graalvm/src/test/java/org/hibernate/graalvm/internal/StaticClassListsTest.java @@ -7,35 +7,187 @@ package org.hibernate.graalvm.internal; import static org.assertj.core.api.Assertions.assertThat; +import static org.hibernate.graalvm.internal.JandexTestUtils.findConcreteNamedImplementors; +import java.io.IOException; import java.lang.reflect.Array; import java.lang.reflect.Constructor; +import java.util.Arrays; import java.util.stream.Collectors; import java.util.stream.Stream; +import org.hibernate.Session; import org.hibernate.event.spi.EventType; import org.hibernate.internal.util.ReflectHelper; +import org.hibernate.persister.collection.CollectionPersister; +import org.hibernate.persister.entity.EntityPersister; import org.junit.Assert; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +import org.jboss.jandex.Index; public class StaticClassListsTest { + private static Index hibernateIndex; + + @BeforeAll + public static void index() throws IOException { + hibernateIndex = JandexTestUtils.indexJar( Session.class ); + } + + @Nested + class TypesNeedingAllConstructorsAccessible { + @ParameterizedTest + @EnumSource(TypesNeedingAllConstructorsAccessible_Category.class) + void containsAllExpectedClasses(TypesNeedingAllConstructorsAccessible_Category category) { + assertThat( StaticClassLists.typesNeedingAllConstructorsAccessible() ) + .containsAll( category.classes().collect( Collectors.toSet() ) ); + } + + @Test + void meta_noMissingTestCategory() { + assertThat( Arrays.stream( TypesNeedingAllConstructorsAccessible_Category.values() ).flatMap( TypesNeedingAllConstructorsAccessible_Category::classes ) ) + .as( "If this fails, a category is missing in " + TypesNeedingAllConstructorsAccessible_Category.class ) + .contains( StaticClassLists.typesNeedingAllConstructorsAccessible() ); + } + } + + // TODO ORM 7: Move this inside TypesNeedingAllConstructorsAccessible (requires JDK 17) and rename to simple Category + enum TypesNeedingAllConstructorsAccessible_Category { + PERSISTERS { + @Override + Stream> classes() { + return findConcreteNamedImplementors( + hibernateIndex, EntityPersister.class, CollectionPersister.class ) + .stream(); + } + }, + MISC { + @Override + Stream> classes() { + // NOTE: Please avoid putting anything here, it's really a last resort. + // Ideally you'd rather add new categories with their own way of listing classes, + // like in PERSISTERS. + // Putting anything here is running the risk of forgetting + // why it was necessary in the first place... + return Stream.of( + // Logging - sometimes looked up without a static field + org.hibernate.internal.CoreMessageLogger_$logger.class + ); + } + }; + + abstract Stream> classes(); + } + + @Nested + class TypesNeedingDefaultConstructorAccessible { + @ParameterizedTest + @EnumSource(TypesNeedingDefaultConstructorAccessible_Category.class) + void containsAllExpectedClasses(TypesNeedingDefaultConstructorAccessible_Category category) { + assertThat( StaticClassLists.typesNeedingDefaultConstructorAccessible() ) + .containsAll( category.classes().collect( Collectors.toSet() ) ); + } + + @Test + void meta_noMissingTestCategory() { + assertThat( Arrays.stream( TypesNeedingDefaultConstructorAccessible_Category.values() ).flatMap( TypesNeedingDefaultConstructorAccessible_Category::classes ) ) + .as( "If this fails, a category is missing in " + TypesNeedingDefaultConstructorAccessible_Category.class ) + .contains( StaticClassLists.typesNeedingDefaultConstructorAccessible() ); + } + } + + // TODO ORM 7: Move this inside TypesNeedingDefaultConstructorAccessible (requires JDK 17) and rename to simple Category + enum TypesNeedingDefaultConstructorAccessible_Category { + MISC { + @Override + Stream> classes() { + // NOTE: Please avoid putting anything here, it's really a last resort. + // Ideally you'd rather add new categories with their own way of listing classes, + // like in PERSISTERS. + // Putting anything here is running the risk of forgetting + // why it was necessary in the first place... + return Stream.of( + org.hibernate.resource.transaction.backend.jdbc.internal.JdbcResourceLocalTransactionCoordinatorBuilderImpl.class, + org.hibernate.id.enhanced.SequenceStyleGenerator.class, + org.hibernate.boot.model.naming.ImplicitNamingStrategyJpaCompliantImpl.class, + org.hibernate.resource.transaction.backend.jta.internal.JtaTransactionCoordinatorBuilderImpl.class, + org.hibernate.type.EnumType.class, + org.hibernate.tool.schema.internal.script.MultiLineSqlScriptExtractor.class + ); + } + }; + + abstract Stream> classes(); + } @Nested class TypesNeedingArrayCopy { - @Test - void containsEventListenerInterfaces() { + @ParameterizedTest + @EnumSource(TypesNeedingArrayCopy_Category.class) + void containsAllExpectedClasses(TypesNeedingArrayCopy_Category category) { assertThat( StaticClassLists.typesNeedingArrayCopy() ) - .containsAll( eventListenerInterfaces().collect( Collectors.toSet() ) ); + .containsAll( category.classes().collect( Collectors.toSet() ) ); } - static Stream> eventListenerInterfaces() { - return EventType.values().stream().map( EventType::baseListenerInterface ) - .map( c -> Array.newInstance( c, 0 ).getClass() ); + @Test + void meta_noMissingTestCategory() { + assertThat( Arrays.stream( TypesNeedingArrayCopy_Category.values() ).flatMap( TypesNeedingArrayCopy_Category::classes ) ) + .as( "If this fails, a category is missing in " + TypesNeedingArrayCopy_Category.class ) + .contains( StaticClassLists.typesNeedingArrayCopy() ); } } + // TODO ORM 7: Move this inside TypesNeedingArrayCopy (requires JDK 17) and rename to simple Category + enum TypesNeedingArrayCopy_Category { + EVENT_LISTENER_INTERFACES { + @Override + Stream> classes() { + return EventType.values().stream().map( EventType::baseListenerInterface ) + .map( c -> Array.newInstance( c, 0 ).getClass() ); + } + }, + MISC { + @Override + Stream> classes() { + // NOTE: Please avoid putting anything here, it's really a last resort. + // Ideally you'd rather add new categories with their own way of listing classes, + // like in EVENT_LISTENER_INTERFACES. + // Putting anything here is running the risk of forgetting + // why it was necessary in the first place... + return Stream.of( + // Java classes -- the why is lost to history + java.util.function.Function[].class, + java.util.List[].class, + java.util.Map.Entry[].class, + java.util.function.Supplier[].class, + // Graphs -- the why is lost to history + org.hibernate.graph.spi.AttributeNodeImplementor[].class, + org.hibernate.sql.results.graph.FetchParent[].class, + org.hibernate.graph.spi.GraphImplementor[].class, + org.hibernate.graph.internal.parse.SubGraphGenerator[].class, + // AST/parsing -- no way to detect this automatically, you just have to know. + org.hibernate.sql.ast.Clause[].class, + org.hibernate.query.hql.spi.DotIdentifierConsumer[].class, + org.hibernate.query.sqm.sql.FromClauseIndex[].class, + org.hibernate.query.sqm.spi.ParameterDeclarationContext[].class, + org.hibernate.sql.ast.tree.select.QueryPart[].class, + org.hibernate.sql.ast.spi.SqlAstProcessingState[].class, + org.hibernate.query.hql.spi.SqmCreationProcessingState[].class, + org.hibernate.sql.ast.tree.Statement[].class, + // Various internals -- the why is lost to history + org.hibernate.sql.results.jdbc.spi.JdbcValuesSourceProcessingState[].class + ); + } + }; + + abstract Stream> classes(); + } + @Nested class BasicConstructorsAvailable {