HHH-15314 - Hibernate Gradle plugin is not working for Kotlin projects

This commit is contained in:
Steve Ebersole 2022-06-08 07:39:10 -05:00
parent 2ef7bd42bf
commit 76055475da
6 changed files with 240 additions and 205 deletions

View File

@ -12,10 +12,10 @@ import java.net.URI;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Path;
import java.util.Set;
import org.gradle.api.GradleException;
import org.gradle.api.Project;
import org.gradle.api.file.Directory;
import org.gradle.api.file.FileCollection;
import org.hibernate.bytecode.enhance.spi.Enhancer;
@ -24,15 +24,84 @@ import org.hibernate.bytecode.enhance.spi.Enhancer;
*/
public class Helper {
public static ClassLoader toClassLoader(Directory classesDir) {
final File classesDirFile = classesDir.getAsFile();
final URI classesDirUri = classesDirFile.toURI();
public static String determineCompileSourceSetName(String name) {
return determineCompileNameParts( name )[0];
}
public static String[] determineCompileNameParts(String name) {
StringBuilder firstPart = null;
StringBuilder secondPart = null;
boolean processingFirstPart = false;
boolean processingSecondPart = false;
final char[] nameChars = name.toCharArray();
for ( int i = 0; i < nameChars.length; i++ ) {
final char nameChar = nameChars[ i ];
if ( processingFirstPart ) {
if ( Character.isUpperCase( nameChar ) ) {
// this is the start of the second-part
processingFirstPart = false;
processingSecondPart = true;
secondPart = new StringBuilder( String.valueOf( Character.toLowerCase( nameChar ) ) );
}
else {
firstPart.append( nameChar );
}
}
else if ( processingSecondPart ) {
if ( Character.isUpperCase( nameChar ) ) {
throw new RuntimeException( "Unexpected compilation task name : " + name );
}
else {
secondPart.append( nameChar );
}
}
else {
if ( Character.isUpperCase( nameChar ) ) {
processingFirstPart = true;
firstPart = new StringBuilder( String.valueOf( Character.toLowerCase( nameChar ) ) );
}
}
}
if ( firstPart == null ) {
throw new RuntimeException( "Unexpected compilation task name : " + name );
}
if ( secondPart == null ) {
return new String[] { "main", firstPart.toString() };
}
return new String[] { firstPart.toString(), secondPart.toString() };
}
public static ClassLoader toClassLoader(FileCollection directories) {
final Set<File> files = directories.getFiles();
final URL[] urls = new URL[ files.size() ];
int index = 0;
for ( File classesDir : files ) {
final URI classesDirUri = classesDir.toURI();
try {
urls[index] = classesDirUri.toURL();
}
catch (MalformedURLException e) {
throw new GradleException( "Unable to resolve classpath entry to URL : " + classesDir.getAbsolutePath(), e );
}
index++;
}
return new URLClassLoader( urls, Enhancer.class.getClassLoader() );
}
public static ClassLoader toClassLoader(File classesDir) {
final URI classesDirUri = classesDir.toURI();
try {
final URL url = classesDirUri.toURL();
return new URLClassLoader( new URL[] { url }, Enhancer.class.getClassLoader() );
}
catch (MalformedURLException e) {
throw new GradleException( "Unable to resolve classpath entry to URL : " + classesDirFile.getAbsolutePath(), e );
throw new GradleException( "Unable to resolve classpath entry to URL : " + classesDir.getAbsolutePath(), e );
}
}

View File

@ -6,25 +6,34 @@
*/
package org.hibernate.orm.tooling.gradle;
import org.gradle.api.Action;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.plugins.JavaPlugin;
import org.gradle.api.Task;
import org.gradle.api.file.DirectoryProperty;
import org.gradle.api.plugins.JvmEcosystemPlugin;
import org.gradle.api.tasks.SourceSet;
import org.gradle.api.tasks.compile.AbstractCompile;
import org.hibernate.orm.tooling.gradle.enhance.EnhancementTask;
import org.hibernate.orm.tooling.gradle.enhance.EnhancementHelper;
import org.hibernate.orm.tooling.gradle.metamodel.JpaMetamodelGenerationTask;
import static org.hibernate.orm.tooling.gradle.Helper.determineCompileSourceSetName;
/**
* Hibernate ORM Gradle plugin
*/
public class HibernateOrmPlugin implements Plugin<Project> {
@Override
public void apply(Project project) {
project.getPlugins().apply( JavaPlugin.class );
// for SourceSet support and other JVM goodies
project.getPlugins().apply( JvmEcosystemPlugin.class );
project.getLogger().debug( "Adding Hibernate extensions to the build [{}]", project.getPath() );
final HibernateOrmSpec ormDsl = project.getExtensions().create( HibernateOrmSpec.DSL_NAME, HibernateOrmSpec.class, project );
EnhancementTask.apply( ormDsl, ormDsl.getSourceSetProperty().get(), project );
prepareEnhancement( ormDsl, project );
JpaMetamodelGenerationTask.apply( ormDsl, ormDsl.getSourceSetProperty().get(), project );
// project.getDependencies().add(
@ -35,4 +44,36 @@ public class HibernateOrmPlugin implements Plugin<Project> {
// )
// );
}
private void prepareEnhancement(HibernateOrmSpec ormDsl, Project project) {
project.getGradle().getTaskGraph().whenReady( (graph) -> {
if ( !ormDsl.getSupportEnhancementProperty().get() ) {
return;
}
graph.getAllTasks().forEach( (task) -> {
if ( task instanceof AbstractCompile ) {
final SourceSet sourceSetLocal = ormDsl.getSourceSetProperty().get();
final String compiledSourceSetName = determineCompileSourceSetName( task.getName() );
if ( !sourceSetLocal.getName().equals( compiledSourceSetName ) ) {
return;
}
final AbstractCompile compileTask = (AbstractCompile) task;
//noinspection Convert2Lambda
task.doLast( new Action<>() {
@Override
public void execute(Task t) {
final DirectoryProperty classesDirectory = compileTask.getDestinationDirectory();
final ClassLoader classLoader = Helper.toClassLoader( sourceSetLocal.getOutput().getClassesDirs() );
EnhancementHelper.enhance( classesDirectory, classLoader, ormDsl, project );
}
} );
task.finalizedBy( this );
}
} );
} );
}
}

View File

@ -16,7 +16,6 @@ import org.gradle.api.Project;
import org.gradle.api.file.Directory;
import org.gradle.api.file.DirectoryProperty;
import org.gradle.api.logging.Logger;
import org.gradle.work.InputChanges;
import org.hibernate.bytecode.enhance.spi.DefaultEnhancementContext;
import org.hibernate.bytecode.enhance.spi.EnhancementContext;
@ -24,7 +23,7 @@ import org.hibernate.bytecode.enhance.spi.Enhancer;
import org.hibernate.bytecode.enhance.spi.UnloadedClass;
import org.hibernate.bytecode.enhance.spi.UnloadedField;
import org.hibernate.cfg.Environment;
import org.hibernate.orm.tooling.gradle.Helper;
import org.hibernate.orm.tooling.gradle.HibernateOrmSpec;
import static org.hibernate.orm.tooling.gradle.Helper.determineClassName;
@ -34,50 +33,35 @@ import static org.hibernate.orm.tooling.gradle.Helper.determineClassName;
public class EnhancementHelper {
public static void enhance(
DirectoryProperty classesDirectoryProperty,
InputChanges inputChanges,
EnhancementSpec enhancementDsl,
ClassLoader classLoader,
HibernateOrmSpec ormDsl,
Project project) {
final Directory classesDir = classesDirectoryProperty.get();
final File classesDirFile = classesDir.getAsFile();
final Directory classesDirectory = classesDirectoryProperty.get();
final File classesDir = classesDirectory.getAsFile();
final Enhancer enhancer = generateEnhancer( classesDir, enhancementDsl );
final Enhancer enhancer = generateEnhancer( classLoader, ormDsl );
final String classesDirPath = classesDirFile.getAbsolutePath();
walk( classesDir, classesDir, enhancer, project );
}
inputChanges.getFileChanges( classesDirectoryProperty ).forEach( (change) -> {
switch ( change.getChangeType() ) {
case ADDED:
case MODIFIED: {
final File changedFile = change.getFile();
if ( changedFile.getName().endsWith( ".class" ) ) {
final String classFilePath = changedFile.getAbsolutePath();
if ( classFilePath.startsWith( classesDirPath ) ) {
// we found the directory it came from - use that to determine the class name
final String className = determineClassName( classesDirFile, changedFile );
private static void walk(File classesDir, File dir, Enhancer enhancer, Project project) {
for ( File subLocation : dir.listFiles() ) {
if ( subLocation.isDirectory() ) {
walk( classesDir, subLocation, enhancer, project );
}
else if ( subLocation.isFile() && subLocation.getName().endsWith( ".class" ) ) {
final String className = determineClassName( classesDir, subLocation );
final long lastModified = subLocation.lastModified();
final long lastModified = changedFile.lastModified();
enhance( subLocation, className, enhancer, project );
enhance( changedFile, className, enhancer, project );
final boolean timestampReset = changedFile.setLastModified( lastModified );
final boolean timestampReset = subLocation.setLastModified( lastModified );
if ( !timestampReset ) {
project.getLogger().debug( "`{}`.setLastModified failed", project.relativePath( changedFile ) );
project.getLogger().debug( "`{}`.setLastModified failed", project.relativePath( subLocation ) );
}
break;
}
}
break;
}
case REMOVED: {
// nothing to do
break;
}
default: {
throw new UnsupportedOperationException( "Unexpected ChangeType : " + change.getChangeType().name() );
}
}
} );
}
private static void enhance(
@ -104,8 +88,8 @@ public class EnhancementHelper {
}
}
private static Enhancer generateEnhancer(Directory classesDir, EnhancementSpec enhancementDsl) {
final ClassLoader classLoader = Helper.toClassLoader( classesDir );
public static Enhancer generateEnhancer(ClassLoader classLoader, HibernateOrmSpec ormDsl) {
final EnhancementSpec enhancementDsl = ormDsl.getEnhancementSpec();
final EnhancementContext enhancementContext = new DefaultEnhancementContext() {
@Override

View File

@ -10,6 +10,7 @@ import javax.inject.Inject;
import org.gradle.api.Project;
import org.gradle.api.provider.Property;
import org.gradle.api.tasks.SourceSetContainer;
import org.hibernate.orm.tooling.gradle.HibernateOrmSpec;
@ -31,6 +32,8 @@ public class EnhancementSpec {
@Inject
public EnhancementSpec(HibernateOrmSpec ormDsl, Project project) {
final SourceSetContainer sourceSets = project.getExtensions().getByType( SourceSetContainer.class );
enableLazyInitialization = makeProperty( project );
enableDirtyTracking = makeProperty( project );
enableAssociationManagement = makeProperty( project );

View File

@ -1,118 +0,0 @@
/*
* 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.orm.tooling.gradle.enhance;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.inject.Inject;
import org.gradle.api.DefaultTask;
import org.gradle.api.Project;
import org.gradle.api.Task;
import org.gradle.api.execution.TaskExecutionAdapter;
import org.gradle.api.execution.TaskExecutionGraph;
import org.gradle.api.file.DirectoryProperty;
import org.gradle.api.tasks.InputDirectory;
import org.gradle.api.tasks.OutputDirectory;
import org.gradle.api.tasks.SourceSet;
import org.gradle.api.tasks.TaskAction;
import org.gradle.api.tasks.TaskState;
import org.gradle.work.Incremental;
import org.gradle.work.InputChanges;
import org.hibernate.orm.tooling.gradle.HibernateOrmSpec;
import static org.hibernate.orm.tooling.gradle.HibernateOrmSpec.HIBERNATE;
/**
* @author Steve Ebersole
*/
public class EnhancementTask extends DefaultTask {
public static final String DSL_NAME = "hibernateEnhance";
public static void apply(HibernateOrmSpec pluginDsl, SourceSet mainSourceSet, Project project) {
final EnhancementTask enhancementTask = project.getTasks().create(
DSL_NAME,
EnhancementTask.class,
pluginDsl,
mainSourceSet,
project
);
enhancementTask.setGroup( HIBERNATE );
enhancementTask.setDescription( "Performs Hibernate ORM enhancement of the project's compiled classes" );
enhancementTask.onlyIf( (t) -> pluginDsl.getSupportEnhancementProperty().getOrElse( true ) );
final String compileJavaTaskName = mainSourceSet.getCompileJavaTaskName();
final Task compileJavaTask = project.getTasks().getByName( compileJavaTaskName );
enhancementTask.dependsOn( compileJavaTask );
compileJavaTask.finalizedBy( enhancementTask );
}
private final EnhancementSpec enhancementDsl;
private final DirectoryProperty javaCompileOutputDirectory;
private final DirectoryProperty outputDirectory;
@Inject
public EnhancementTask(HibernateOrmSpec pluginDsl, SourceSet mainSourceSet, Project project) {
enhancementDsl = pluginDsl.getEnhancementSpec();
javaCompileOutputDirectory = mainSourceSet.getJava().getDestinationDirectory();
outputDirectory = project.getObjects().directoryProperty();
outputDirectory.set( project.getLayout().getBuildDirectory().dir( "tmp/hibernateEnhancement" ) );
final AtomicBoolean didCompileRun = new AtomicBoolean( false );
final TaskExecutionGraph taskGraph = project.getGradle().getTaskGraph();
taskGraph.addTaskExecutionListener(
new TaskExecutionAdapter() {
@Override
public void afterExecute(Task task, TaskState state) {
super.afterExecute( task, state );
if ( "compileJava".equals( task.getName() ) ) {
if ( state.getDidWork() ) {
didCompileRun.set( true );
}
}
taskGraph.removeTaskExecutionListener( this );
}
}
);
getOutputs().upToDateWhen( (task) -> ! didCompileRun.get() );
}
@InputDirectory
@Incremental
public DirectoryProperty getJavaCompileDirectory() {
return javaCompileOutputDirectory;
}
@OutputDirectory
public DirectoryProperty getOutputDirectory() {
return outputDirectory;
}
@TaskAction
public void enhanceClasses(InputChanges inputChanges) {
if ( !enhancementDsl.hasAnythingToDo() ) {
return;
}
if ( !inputChanges.isIncremental() ) {
getProject().getLogger().debug( "EnhancementTask inputs were not incremental" );
}
if ( enhancementDsl.getEnableExtendedEnhancement().get() ) {
// for extended enhancement, we may need to enhance everything...
// for now, assume we don't
getProject().getLogger().info( "Performing extended enhancement" );
}
EnhancementHelper.enhance( javaCompileOutputDirectory, inputChanges, enhancementDsl, getProject() );
}
}

View File

@ -6,6 +6,7 @@
*/
package org.hibernate.orm.tooling.gradle;
import java.io.File;
import java.nio.file.Path;
import org.gradle.testkit.runner.BuildResult;
@ -13,7 +14,8 @@ import org.gradle.testkit.runner.BuildTask;
import org.gradle.testkit.runner.GradleRunner;
import org.gradle.testkit.runner.TaskOutcome;
import org.junit.jupiter.api.Disabled;
import org.hibernate.engine.spi.Managed;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
@ -38,27 +40,10 @@ class HibernateOrmPluginTest {
);
@Test
public void testEnhancementTask(@TempDir Path projectDir) {
public void testEnhancement(@TempDir Path projectDir) throws Exception {
Copier.copyProject( "simple/build.gradle", projectDir );
System.out.println( "First execution ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~" );
final GradleRunner gradleRunner = GradleRunner.create()
.withProjectDir( projectDir.toFile() )
.withPluginClasspath()
.withDebug( true )
.withArguments( "clean", "hibernateEnhance", "--stacktrace", "--no-build-cache" )
.forwardOutput();
final BuildResult result = gradleRunner.build();
final BuildTask task = result.task( ":hibernateEnhance" );
assertThat( task ).isNotNull();
assertThat( task.getOutcome() ).isEqualTo( TaskOutcome.SUCCESS );
}
@Test
public void testEnhancementTaskAsFinalizer(@TempDir Path projectDir) {
Copier.copyProject( "simple/build.gradle", projectDir );
final GradleRunner gradleRunner = GradleRunner.create()
.withProjectDir( projectDir.toFile() )
.withPluginClasspath()
@ -67,15 +52,24 @@ class HibernateOrmPluginTest {
.forwardOutput();
final BuildResult result = gradleRunner.build();
final BuildTask task = result.task( ":hibernateEnhance" );
final BuildTask task = result.task( ":compileJava" );
assertThat( task ).isNotNull();
// assertThat( task.getOutcome() ).is( anyOf( SUCCESS, UP_TO_DATE ) );
assertThat( task.getOutcome() ).isEqualTo( TaskOutcome.SUCCESS );
// make sure the class is enhanced
final File classesDir = new File( projectDir.toFile(), "build/classes/java/main" );
final ClassLoader classLoader = Helper.toClassLoader( classesDir );
verifyEnhanced( classLoader, "TheEmbeddable" );
verifyEnhanced( classLoader, "TheEntity" );
}
private void verifyEnhanced(ClassLoader classLoader, String className) throws Exception {
final Class<?> loadedClass = classLoader.loadClass( className );
assertThat( Managed.class ).isAssignableFrom( loadedClass );
}
@Test
@Disabled( "up-to-date checking not working" )
public void testEnhancementTaskUpToDate(@TempDir Path projectDir) {
public void testEnhancementUpToDate(@TempDir Path projectDir) throws Exception {
Copier.copyProject( "simple/build.gradle", projectDir );
{
@ -84,13 +78,18 @@ class HibernateOrmPluginTest {
.withProjectDir( projectDir.toFile() )
.withPluginClasspath()
.withDebug( true )
.withArguments( "clean", "hibernateEnhance", "--stacktrace", "--no-build-cache" )
.withArguments( "clean", "compileJava", "--stacktrace", "--no-build-cache" )
.forwardOutput();
final BuildResult result = gradleRunner.build();
final BuildTask task = result.task( ":hibernateEnhance" );
final BuildTask task = result.task( ":compileJava" );
assertThat( task ).isNotNull();
assertThat( task.getOutcome() ).isEqualTo( TaskOutcome.SUCCESS );
// make sure the class is enhanced
final File classesDir = new File( projectDir.toFile(), "build/classes/java/main" );
final ClassLoader classLoader = Helper.toClassLoader( classesDir );
verifyEnhanced( classLoader, "TheEntity" );
}
{
@ -99,12 +98,17 @@ class HibernateOrmPluginTest {
.withProjectDir( projectDir.toFile() )
.withPluginClasspath()
.withDebug( true )
.withArguments( "hibernateEnhance", "--stacktrace", "--no-build-cache" )
.withArguments( "compileJava", "--stacktrace", "--no-build-cache" )
.forwardOutput();
final BuildResult result = gradleRunner.build();
final BuildTask task = result.task( ":hibernateEnhance" );
final BuildTask task = result.task( ":compileJava" );
assertThat( task ).isNotNull();
assertThat( task.getOutcome() ).isEqualTo( TaskOutcome.UP_TO_DATE );
// and again
final File classesDir = new File( projectDir.toFile(), "build/classes/java/main" );
final ClassLoader classLoader = Helper.toClassLoader( classesDir );
verifyEnhanced( classLoader, "TheEntity" );
}
}
@ -126,7 +130,7 @@ class HibernateOrmPluginTest {
}
@Test
@Disabled( "up-to-date checking not working" )
// @Disabled( "up-to-date checking not working" )
public void testJpaMetamodelGenUpToDate(@TempDir Path projectDir) {
Copier.copyProject( "simple/build.gradle", projectDir );
@ -136,7 +140,7 @@ class HibernateOrmPluginTest {
.withProjectDir( projectDir.toFile() )
.withPluginClasspath()
.withDebug( true )
.withArguments( "clean", "generateJpaMetamodel", "-xhibernateEnhance", "--stacktrace", "--no-build-cache" )
.withArguments( "clean", "generateJpaMetamodel", "--stacktrace", "--no-build-cache" )
.forwardOutput();
final BuildResult result = gradleRunner.build();
@ -151,7 +155,7 @@ class HibernateOrmPluginTest {
.withProjectDir( projectDir.toFile() )
.withPluginClasspath()
.withDebug( true )
.withArguments( "clean", "generateJpaMetamodel", "-xhibernateEnhance", "--stacktrace", "--no-build-cache" )
.withArguments( "generateJpaMetamodel", "--stacktrace", "--no-build-cache" )
.forwardOutput();
final BuildResult result2 = gradleRunner2.build();
@ -162,19 +166,71 @@ class HibernateOrmPluginTest {
}
@Test
@Disabled( "HHH-15314" )
public void testEnhanceKotlinModel(@TempDir Path projectDir) {
public void testEnhanceKotlinModel(@TempDir Path projectDir) throws Exception {
Copier.copyProject( "simple-kotlin/build.gradle", projectDir );
final GradleRunner gradleRunner = GradleRunner.create()
.withProjectDir( projectDir.toFile() )
.withPluginClasspath()
.withDebug( true )
.withArguments( "clean", "hibernateEnhance", "--stacktrace", "--no-build-cache" )
.withArguments( "clean", "compileKotlin", "--stacktrace", "--no-build-cache" )
.forwardOutput();
final BuildResult result = gradleRunner.build();
final BuildTask task = result.task( ":hibernateEnhance" );
final BuildTask task = result.task( ":compileKotlin" );
assertThat( task ).isNotNull();
assertThat( task.getOutcome() ).isEqualTo( TaskOutcome.SUCCESS );
// make sure the class is enhanced
final File classesDir = new File( projectDir.toFile(), "build/classes/kotlin/main" );
final ClassLoader classLoader = Helper.toClassLoader( classesDir );
verifyEnhanced( classLoader, "TheEntity" );
}
@Test
public void testEnhanceKotlinModelUpToDate(@TempDir Path projectDir) throws Exception {
Copier.copyProject( "simple-kotlin/build.gradle", projectDir );
{
System.out.println( "First execution ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~" );
final GradleRunner gradleRunner = GradleRunner.create()
.withProjectDir( projectDir.toFile() )
.withPluginClasspath()
.withDebug( true )
.withArguments( "clean", "compileKotlin", "--stacktrace", "--no-build-cache" )
.forwardOutput();
final BuildResult result = gradleRunner.build();
final BuildTask task = result.task( ":compileKotlin" );
assertThat( task ).isNotNull();
assertThat( task.getOutcome() ).isEqualTo( TaskOutcome.SUCCESS );
// make sure the class is enhanced
final File classesDir = new File( projectDir.toFile(), "build/classes/kotlin/main" );
final ClassLoader classLoader = Helper.toClassLoader( classesDir );
verifyEnhanced( classLoader, "TheEntity" );
}
{
System.out.println( "Second execution ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~" );
final GradleRunner gradleRunner = GradleRunner.create()
.withProjectDir( projectDir.toFile() )
.withPluginClasspath()
.withDebug( true )
.withArguments( "compileKotlin", "--stacktrace", "--no-build-cache" )
.forwardOutput();
final BuildResult result = gradleRunner.build();
final BuildTask task = result.task( ":compileKotlin" );
assertThat( task ).isNotNull();
assertThat( task.getOutcome() ).isEqualTo( TaskOutcome.UP_TO_DATE );
// make sure the class is enhanced
final File classesDir = new File( projectDir.toFile(), "build/classes/kotlin/main" );
final ClassLoader classLoader = Helper.toClassLoader( classesDir );
verifyEnhanced( classLoader, "TheEntity" );
}
}
}