diff --git a/documentation/src/main/asciidoc/userguide/chapters/bytecode/BytecodeEnhancement.adoc b/documentation/src/main/asciidoc/userguide/chapters/bytecode/BytecodeEnhancement.adoc new file mode 100644 index 0000000000..f79afe18da --- /dev/null +++ b/documentation/src/main/asciidoc/userguide/chapters/bytecode/BytecodeEnhancement.adoc @@ -0,0 +1,23 @@ +[[enhancement]] +== Enhancement +:sourcedir: ../../../../../test/java/org/hibernate/userguide/bytecode +:extrasdir: extras + +Hibernate offers a number of services that can be added into an application's domain model classes +through bytecode enhancement... + + +[[enhancement-laziness]] +=== Laziness + + +[[enhancement-bidir]] +=== Bi-directionality + + +[[enhancement-dirty]] +=== In-line dirty checking + + +[[enhancement-extended]] +=== Extended enhancement \ No newline at end of file diff --git a/tooling/hibernate-gradle-plugin/src/main/groovy/org/hibernate/orm/tooling/gradle/EnhanceTask.groovy b/tooling/hibernate-gradle-plugin/src/main/groovy/org/hibernate/orm/tooling/gradle/EnhanceTask.groovy new file mode 100644 index 0000000000..f1e624ce72 --- /dev/null +++ b/tooling/hibernate-gradle-plugin/src/main/groovy/org/hibernate/orm/tooling/gradle/EnhanceTask.groovy @@ -0,0 +1,33 @@ +/* + * 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 + +import org.gradle.api.DefaultTask +import org.gradle.api.tasks.SourceSet +import org.gradle.api.tasks.TaskAction +import org.gradle.util.ConfigureUtil + +/** + * @author Steve Ebersole + */ +@SuppressWarnings("unused") +class EnhanceTask extends DefaultTask { + EnhanceExtension options + SourceSet[] sourceSets + + @TaskAction + void enhance() { + for ( SourceSet sourceSet: sourceSets ) { + EnhancementHelper.enhance( sourceSet, options, project ) + } + } + + void options(Closure closure) { + options = new EnhanceExtension() + ConfigureUtil.configure( closure, options ) + } +} diff --git a/tooling/hibernate-gradle-plugin/src/main/groovy/org/hibernate/orm/tooling/gradle/EnhancementHelper.java b/tooling/hibernate-gradle-plugin/src/main/groovy/org/hibernate/orm/tooling/gradle/EnhancementHelper.java new file mode 100644 index 0000000000..927fa254e5 --- /dev/null +++ b/tooling/hibernate-gradle-plugin/src/main/groovy/org/hibernate/orm/tooling/gradle/EnhancementHelper.java @@ -0,0 +1,178 @@ +/* + * 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; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.ArrayList; +import java.util.List; + +import org.gradle.api.GradleException; +import org.gradle.api.Project; +import org.gradle.api.file.FileCollection; +import org.gradle.api.file.FileTree; +import org.gradle.api.logging.Logger; +import org.gradle.api.tasks.SourceSet; + +import org.hibernate.bytecode.enhance.spi.DefaultEnhancementContext; +import org.hibernate.bytecode.enhance.spi.EnhancementContext; +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; + +/** + * @author Steve Ebersole + */ +public class EnhancementHelper { + static void enhance(SourceSet sourceSet, EnhanceExtension options, Project project) { + final ClassLoader classLoader = toClassLoader( sourceSet.getRuntimeClasspath() ); + + final EnhancementContext enhancementContext = new DefaultEnhancementContext() { + @Override + public ClassLoader getLoadingClassLoader() { + return classLoader; + } + + @Override + public boolean doBiDirectionalAssociationManagement(UnloadedField field) { + return options.getEnableAssociationManagement(); + } + + @Override + public boolean doDirtyCheckingInline(UnloadedClass classDescriptor) { + return options.getEnableDirtyTracking(); + } + + @Override + public boolean hasLazyLoadableAttributes(UnloadedClass classDescriptor) { + return options.getEnableLazyInitialization(); + } + + @Override + public boolean isLazyLoadable(UnloadedField field) { + return options.getEnableLazyInitialization(); + } + + @Override + public boolean doExtendedEnhancement(UnloadedClass classDescriptor) { + return options.getEnableExtendedEnhancement(); + } + }; + + if ( options.getEnableExtendedEnhancement() ) { + project.getLogger().warn("Extended enhancement is enabled. Classes other than entities may be modified. You should consider access the entities using getter/setter methods and disable this property. Use at your own risk." ); + } + + final Enhancer enhancer = Environment.getBytecodeProvider().getEnhancer( enhancementContext ); + + for ( File classesDir: sourceSet.getOutput().getClassesDirs() ) { + final FileTree fileTree = project.fileTree( classesDir ); + for ( File file : fileTree ) { + if ( !file.getName().endsWith( ".class" ) ) { + continue; + } + + final byte[] enhancedBytecode = doEnhancement( classesDir, file, enhancer ); + if ( enhancedBytecode != null ) { + writeOutEnhancedClass( enhancedBytecode, file, project.getLogger() ); + project.getLogger().info( "Successfully enhanced class [" + file + "]" ); + } + else { + project.getLogger().info( "Skipping class [" + file.getAbsolutePath() + "], not an entity nor embeddable" ); + } + } + } + } + + public static ClassLoader toClassLoader(FileCollection runtimeClasspath) { + List urls = new ArrayList<>(); + for ( File file : runtimeClasspath ) { + try { + urls.add( file.toURI().toURL() ); + } + catch (MalformedURLException e) { + throw new GradleException( "Unable to resolve classpath entry to URL : " + file.getAbsolutePath(), e ); + } + } + return new URLClassLoader( urls.toArray( new URL[0] ), Enhancer.class.getClassLoader() ); + } + + @SuppressWarnings("WeakerAccess") + static byte[] doEnhancement(File root, File javaClassFile, Enhancer enhancer) { + try { + final String className = determineClassName( root, javaClassFile ); + final ByteArrayOutputStream originalBytes = new ByteArrayOutputStream(); + try (final FileInputStream fileInputStream = new FileInputStream( javaClassFile )) { + byte[] buffer = new byte[1024]; + int length; + while ( ( length = fileInputStream.read( buffer ) ) != -1 ) { + originalBytes.write( buffer, 0, length ); + } + } + return enhancer.enhance( className, originalBytes.toByteArray() ); + } + catch (Exception e) { + throw new GradleException( "Unable to enhance class : " + javaClassFile, e ); + } + } + + private static String determineClassName(File root, File javaClassFile) { + return javaClassFile.getAbsolutePath().substring( + root.getAbsolutePath().length() + 1, + javaClassFile.getAbsolutePath().length() - ".class".length() + ).replace( File.separatorChar, '.' ); + } + + private static void writeOutEnhancedClass(byte[] enhancedBytecode, File file, Logger logger) { + try { + if ( file.delete() ) { + if ( !file.createNewFile() ) { + logger.error( "Unable to recreate class file [" + file.getName() + "]" ); + } + } + else { + logger.error( "Unable to delete class file [" + file.getName() + "]" ); + } + } + catch (IOException e) { + logger.warn( "Problem preparing class file for writing out enhancements [" + file.getName() + "]" ); + } + + try { + FileOutputStream outputStream = new FileOutputStream( file, false ); + try { + outputStream.write( enhancedBytecode ); + outputStream.flush(); + } + catch (IOException e) { + throw new GradleException( "Error writing to enhanced class [" + file.getName() + "] to file [" + file.getAbsolutePath() + "]", e ); + } + finally { + try { + outputStream.close(); + } + catch (IOException ignore) { + } + } + } + catch (FileNotFoundException e) { + throw new GradleException( "Error opening class file for writing : " + file.getAbsolutePath(), e ); + } + + } + + private EnhancementHelper() { + } +} diff --git a/tooling/hibernate-gradle-plugin/src/main/groovy/org/hibernate/orm/tooling/gradle/HibernatePlugin.java b/tooling/hibernate-gradle-plugin/src/main/groovy/org/hibernate/orm/tooling/gradle/HibernatePlugin.java index cb56a91453..77e19072ca 100644 --- a/tooling/hibernate-gradle-plugin/src/main/groovy/org/hibernate/orm/tooling/gradle/HibernatePlugin.java +++ b/tooling/hibernate-gradle-plugin/src/main/groovy/org/hibernate/orm/tooling/gradle/HibernatePlugin.java @@ -55,12 +55,9 @@ public class HibernatePlugin implements Plugin { project.getExtensions().add( "hibernate", hibernateExtension ); project.afterEvaluate( - new Action() { - @Override - public void execute(Project project) { - if ( hibernateExtension.enhance != null ) { - applyEnhancement( project, hibernateExtension ); - } + p -> { + if ( hibernateExtension.enhance != null ) { + applyEnhancement( p, hibernateExtension ); } } ); @@ -76,148 +73,11 @@ public class HibernatePlugin implements Plugin { project.getLogger().debug( "Applying Hibernate enhancement action to SourceSet.{}", sourceSet.getName() ); final Task compileTask = project.getTasks().findByName( sourceSet.getCompileJavaTaskName() ); + assert compileTask != null; compileTask.doLast( - new Action() { - @Override - public void execute(Task task) { - project.getLogger().debug( "Starting Hibernate enhancement on SourceSet.{}", sourceSet.getName() ); - - final ClassLoader classLoader = toClassLoader( sourceSet.getRuntimeClasspath() ); - - EnhancementContext enhancementContext = new DefaultEnhancementContext() { - @Override - public ClassLoader getLoadingClassLoader() { - return classLoader; - } - - @Override - public boolean doBiDirectionalAssociationManagement(UnloadedField field) { - return hibernateExtension.enhance.getEnableAssociationManagement(); - } - - @Override - public boolean doDirtyCheckingInline(UnloadedClass classDescriptor) { - return hibernateExtension.enhance.getEnableDirtyTracking(); - } - - @Override - public boolean hasLazyLoadableAttributes(UnloadedClass classDescriptor) { - return hibernateExtension.enhance.getEnableLazyInitialization(); - } - - @Override - public boolean isLazyLoadable(UnloadedField field) { - return hibernateExtension.enhance.getEnableLazyInitialization(); - } - - @Override - public boolean doExtendedEnhancement(UnloadedClass classDescriptor) { - return hibernateExtension.enhance.getEnableExtendedEnhancement(); - } - }; - - if ( hibernateExtension.enhance.getEnableExtendedEnhancement() ) { - logger.warn("Extended enhancement is enabled. Classes other than entities may be modified. You should consider access the entities using getter/setter methods and disable this property. Use at your own risk." ); - } - - final Enhancer enhancer = Environment.getBytecodeProvider().getEnhancer( enhancementContext ); - - final FileTree fileTree = project.fileTree( sourceSet.getOutput().getClassesDir() ); - for ( File file : fileTree ) { - if ( !file.getName().endsWith( ".class" ) ) { - continue; - } - - final byte[] enhancedBytecode = doEnhancement( sourceSet.getOutput().getClassesDir(), file, enhancer ); - if ( enhancedBytecode != null ) { - writeOutEnhancedClass( enhancedBytecode, file ); - logger.info( "Successfully enhanced class [" + file + "]" ); - } - else { - logger.info( "Skipping class [" + file.getAbsolutePath() + "], not an entity nor embeddable" ); - } - } - } - } + task -> EnhancementHelper.enhance( sourceSet, hibernateExtension.enhance, project ) ); } } - private ClassLoader toClassLoader(FileCollection runtimeClasspath) { - List urls = new ArrayList(); - for ( File file : runtimeClasspath ) { - try { - urls.add( file.toURI().toURL() ); - logger.debug( "Adding classpath entry for " + file.getAbsolutePath() ); - } - catch (MalformedURLException e) { - throw new GradleException( "Unable to resolve classpath entry to URL : " + file.getAbsolutePath(), e ); - } - } - return new URLClassLoader( urls.toArray( new URL[urls.size()] ), Enhancer.class.getClassLoader() ); - } - - private byte[] doEnhancement(File root, File javaClassFile, Enhancer enhancer) { - try { - String className = javaClassFile.getAbsolutePath().substring( - root.getAbsolutePath().length() + 1, - javaClassFile.getAbsolutePath().length() - ".class".length() - ).replace( File.separatorChar, '.' ); - ByteArrayOutputStream originalBytes = new ByteArrayOutputStream(); - FileInputStream fileInputStream = new FileInputStream( javaClassFile ); - try { - byte[] buffer = new byte[1024]; - int length; - while ( ( length = fileInputStream.read( buffer ) ) != -1 ) { - originalBytes.write( buffer, 0, length ); - } - } - finally { - fileInputStream.close(); - } - return enhancer.enhance( className, originalBytes.toByteArray() ); - } - catch (Exception e) { - throw new GradleException( "Unable to enhance class : " + javaClassFile, e ); - } - } - - private void writeOutEnhancedClass(byte[] enhancedBytecode, File file) { - try { - if ( file.delete() ) { - if ( !file.createNewFile() ) { - logger.error( "Unable to recreate class file [" + file.getName() + "]" ); - } - } - else { - logger.error( "Unable to delete class file [" + file.getName() + "]" ); - } - } - catch (IOException e) { - logger.warn( "Problem preparing class file for writing out enhancements [" + file.getName() + "]" ); - } - - try { - FileOutputStream outputStream = new FileOutputStream( file, false ); - try { - outputStream.write( enhancedBytecode ); - outputStream.flush(); - } - catch (IOException e) { - throw new GradleException( "Error writing to enhanced class [" + file.getName() + "] to file [" + file.getAbsolutePath() + "]", e ); - } - finally { - try { - outputStream.close(); - } - catch (IOException ignore) { - } - } - } - catch (FileNotFoundException e) { - throw new GradleException( "Error opening class file for writing : " + file.getAbsolutePath(), e ); - } - - } - } diff --git a/tooling/hibernate-gradle-plugin/src/test/groovy/org/hibernate/orm/tooling/gradle/HibernatePluginTest.groovy b/tooling/hibernate-gradle-plugin/src/test/groovy/org/hibernate/orm/tooling/gradle/HibernatePluginTest.groovy index d2eedd8562..f2e57885c5 100644 --- a/tooling/hibernate-gradle-plugin/src/test/groovy/org/hibernate/orm/tooling/gradle/HibernatePluginTest.groovy +++ b/tooling/hibernate-gradle-plugin/src/test/groovy/org/hibernate/orm/tooling/gradle/HibernatePluginTest.groovy @@ -6,6 +6,10 @@ */ package org.hibernate.orm.tooling.gradle import org.gradle.api.Project +import org.gradle.api.Task +import org.gradle.api.plugins.JavaPluginConvention +import org.gradle.api.tasks.SourceSet +import org.gradle.api.tasks.compile.JavaCompile import org.gradle.testfixtures.ProjectBuilder import org.junit.Test @@ -38,4 +42,46 @@ class HibernatePluginTest { enableExtendedEnhancement = false } } + + @Test + public void testEnhanceTask() { + Project project = ProjectBuilder.builder().build() + project.plugins.apply 'org.hibernate.orm' + + def task = project.tasks.create( "finishHim", EnhanceTask ) + + task.options { + enableLazyInitialization = true + enableDirtyTracking = true + enableAssociationManagement = false + enableExtendedEnhancement = false + } + + task.sourceSets = project.getConvention().getPlugin( JavaPluginConvention ).sourceSets.main + + task.enhance() + } + + @Test + public void testTaskAction() { + Project project = ProjectBuilder.builder().build() + project.plugins.apply 'org.hibernate.orm' + + // the test sourceSet + def sourceSet = project.getConvention().getPlugin( JavaPluginConvention ).sourceSets.test; + + // The compile task for the test sourceSet + final JavaCompile compileTestTask = project.getTasks().findByName( sourceSet.getCompileJavaTaskName() ); + + // Lets add our enhancer to enhance the test classes after the test are compiled + compileTestTask.doLast { + EnhancementHelper.enhance( + sourceSet, + project.extensions.findByType( HibernateExtension.class ).enhance, + project + ) + } + + compileTestTask.execute() + } }