diff --git a/documentation/src/main/asciidoc/userguide/chapters/tooling/ant.adoc b/documentation/src/main/asciidoc/userguide/chapters/tooling/ant.adoc index 9bd8b7c2079d..ae7dfa475a3c 100644 --- a/documentation/src/main/asciidoc/userguide/chapters/tooling/ant.adoc +++ b/documentation/src/main/asciidoc/userguide/chapters/tooling/ant.adoc @@ -1,8 +1,170 @@ [[tooling-ant]] -=== Ant Plugin +=== Ant -Hibernate provides https://ant.apache.org/[Ant] support ... +Hibernate provides https://ant.apache.org/[Ant] support. +Everything Ant related is available from the +https://central.sonatype.com/artifact/org.hibernate.orm/hibernate-ant[hibernate-ant] +library. +[[tooling-ant-enhancement]] +==== Bytecode Enhancement ==== + +Hibernate provides an https://ant.apache.org/[Ant] task implementation +that you can use to do build-time bytecode enhancement of your domain +model. You can visit <> for discussion of the capabilities +of an enhanced model. + +[[ant-enhance-task]] +===== *Enhance Task* ===== + +The task implementation is in class `org.hibernate.tool.enhance.EnhancementTask`, +so you will need to include a `taskdef` in your `build.xml` that uses this class +to define your task. Below is a minimal Ant `build.xml` file that shows the use +of the enhancement task. + +[[enhance-task-example]] +.Enhance Task Example +==== +[source, XML]] +---- +include::extras/ant-enhance-example.xml[] +---- +==== + +As you can see above, https://ant.apache.org/ivy[Apache Ivy] was used to +handle the dependency on the +https://central.sonatype.com/artifact/org.hibernate.orm/hibernate-ant[hibernate-ant] +library. Now let's dive a little deeper in the configuration possibilities for the +enhancement task. + +[[ant-enhance-configuration]] +===== *Enhance Configuration* ===== + +[[ant-enhance-base-attribute]] +====== `*base*` ====== +This attribute is mandatory. It points to the base folder where the enhancement task will look +to discover classes that have to be enhanced. It is either combined with the `dir` +attribute that specifies a subfolder of `base` where the classes are located (or of course +the entire `base` folder) or else with a `` child element that has a similar role. +If neither `dir` nor `` are used, no classes will be enhanced. + +==== +[source, XML]] +---- + +---- +==== + +[[ant-enhance-dir-attribute]] +====== `*dir*` ====== +This attribute is combined with the (mandatory) `base` attribute described above. It points to +a subfolder of `base` where the enhancement task will look to discover the classes to be enhanced. +If the `dir` attribute is specified, the use of a `` child element will be ignored. +If neither `dir` nor `` are used, no classes will be enhanced. + +==== +[source, XML]] +---- + +---- +==== + +[[ant-enhance-fileset-element]] +===== `**` ===== +This child element is combined with the (mandatory) `base` attribute described above. It can be used +to detail which classes should be selected for enhancement. The use of `` is well documented on the +https://ant.apache.org/manual/Types/fileset.html[Ant FileSet documentation page]. If the `dir` attribute +described above is specified, the `` element will be ignored. +If neither `dir` nor `` are used, no classes will be enhanced. + +==== +[source, XML]] +---- + + + + + +---- +==== + +[[ant-enhance-enableLazyInitialization-attribute]] +===== `*enableLazyInitialization*` ===== +This attribute has a default value of `true`. It indicates that the enhance task should perform the changes +to enable lazy loading. To disable, set the value of this attribute to `false`. + +==== +[source, XML]] +---- + +---- +==== + + +[[ant-enhance-enableDirtyTracking-attribute]] +===== `*enableDirtyTracking*` ===== +This attribute has a default value of `true`. It indicates that the enhance task should perform the changes +to enable dirty tracking. To disable, set the value of this attribute to `false`. + +==== +[source, XML]] +---- + +---- +==== + +[[ant-enhance-enableAssociationManagement-attribute]] +===== `*enableAssociationManagement*` ===== +This attribute has a default value of `false`. It indicates that the enhance task should not perform the changes +to enable association management. To enable, set the value of this attribute to `true`. + +==== +[source, XML]] +---- + +---- +==== + +[[ant-enhance-enableExtendedEnhancement-attribute]] +===== `*enableExtendedEnhancement*` ===== +This attribute has a default value of `false`. It indicates that the enhance task should not perform the changes +to enable the extended enhancement (i.e. even on non-entities). +To enable this, set the value of this attribute to `true`. + +==== +[source, XML]] +---- + +---- +==== + +[[ant-enhance-failOnError-attribute]] +===== `*failOnError*` ===== +This attribute has a default value of `true`. It indicates that the enhance task will throw an Ant BuildException +when it encounters a problem. If you prefer the build to continue, you can set the value to `false`. + +==== +[source, XML]] +---- + +---- +==== + +===== *Final Remark* ===== +If the values of the four attributes `enableLazyInitialization`, `enableDirtyTracking`, `enableAssociationManagement`, +`enableExtendedEnhancement` are all `false`, the enhancement task is not executed. [[tooling-ant-modelgen]] ==== Static Metamodel Generation in Ant diff --git a/documentation/src/main/asciidoc/userguide/chapters/tooling/extras/ant-enhance-example.xml b/documentation/src/main/asciidoc/userguide/chapters/tooling/extras/ant-enhance-example.xml new file mode 100644 index 000000000000..2e9f378f09c2 --- /dev/null +++ b/documentation/src/main/asciidoc/userguide/chapters/tooling/extras/ant-enhance-example.xml @@ -0,0 +1,20 @@ + + [...] + + [...] + + [...] + + + + [...] + + diff --git a/tooling/hibernate-ant/hibernate-ant.gradle b/tooling/hibernate-ant/hibernate-ant.gradle index a9da9efd0fe3..a6af63b9a823 100644 --- a/tooling/hibernate-ant/hibernate-ant.gradle +++ b/tooling/hibernate-ant/hibernate-ant.gradle @@ -10,4 +10,5 @@ description = 'Annotation Processor to generate JPA 2 static metamodel classes' dependencies { compileOnly libs.ant implementation project( ':hibernate-core' ) + testImplementation libs.ant } \ No newline at end of file diff --git a/tooling/hibernate-ant/src/test/java/org/hibernate/tool/enhance/EnhancementTaskTest.java b/tooling/hibernate-ant/src/test/java/org/hibernate/tool/enhance/EnhancementTaskTest.java new file mode 100644 index 000000000000..c643e49aecbb --- /dev/null +++ b/tooling/hibernate-ant/src/test/java/org/hibernate/tool/enhance/EnhancementTaskTest.java @@ -0,0 +1,349 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.tool.enhance; + +import org.apache.tools.ant.DefaultLogger; +import org.apache.tools.ant.Project; +import org.apache.tools.ant.ProjectHelper; +import org.hibernate.bytecode.enhance.spi.EnhancementInfo; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.File; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Files; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class EnhancementTaskTest { + + @TempDir + private File projectDir; + + @BeforeEach + public void beforeEach() throws Exception { + copyJavFiles(); + prepareDestFolder(); + } + + @Test + public void testEnhancementDefault() throws Exception { + // The default settings for the enhancement task are as follows: + // enableLazyInitialization = 'true' + // enableDirtyTracking = 'true' + // enableAssociationManagement = 'false' + // enableExtendedEnhancement = 'false' + // The files are read from folder 'dir' which needs to be a subfolder of 'base' + // The property 'base' is mandatory + // If 'dir' is not specified, a 'fileset' element can be used (see #testEnhancementFileSet) + String enhanceTag = + "\n"; + Project project = createProject(enhanceTag); + executeCompileTarget( project ); + executeEnhanceTarget( project ); + // Both Bar and Baz should be enhanced + assertTrue(isEnhanced( "Bar" )); + assertTrue(isEnhanced( "Baz" )); + // Both Bar and Baz contain the method '$$_hibernate_getInterceptor' + // because of the default setting of 'enableLazyInitialization' + assertTrue( methodIsPresentInClass("$$_hibernate_getInterceptor", "Bar")); + assertTrue( methodIsPresentInClass("$$_hibernate_getInterceptor", "Baz")); + // Both Bar and Baz contain the method '$$_hibernate_hasDirtyAttributes' + // because of the default setting of 'enableDirtyTracking' + assertTrue( methodIsPresentInClass("$$_hibernate_hasDirtyAttributes", "Bar")); + assertTrue( methodIsPresentInClass("$$_hibernate_hasDirtyAttributes", "Baz")); + // Foo is not an entity and extended enhancement is not enabled so the class is not enhanced + assertFalse(isEnhanced("Foo")); + // Association management should not be present + assertFalse(isAssociationManagementPresent()); + } + + @Test + public void testEnhancementFileSet() throws Exception { + // Use the defaults settings for enhancement (see #testEnhancementDefault) + // The property 'base' is mandatory + // The files are read from the specified 'fileset' element: + // - the folder specified by 'dir' + // - the 'Baz.class' file is excluded + String enhanceTag = + "\n" + + " \n" + + " \n" + + " \n" + + "\n"; + Project project = createProject(enhanceTag); + executeCompileTarget(project); + executeEnhanceTarget(project); + // Bar is enhanced + assertTrue(isEnhanced( "Bar" )); + // Baz is not enhanced because it was excluded from the file set + assertFalse(isEnhanced( "Baz" )); + // Foo is not enhanced because it is not an entity and extended enhancement was not enabled + assertFalse( isEnhanced( "Foo" ) ); + // Association management should not be present + assertFalse(isAssociationManagementPresent()); + } + + @Test + public void testEnhancementNoLazyInitialization() throws Exception { + // Change the default setting for 'enableLazyInitialization' to 'false' + // Otherwise use the settings of #testEnhancementDefault + String enhanceTag = + "\n"; + Project project = createProject(enhanceTag); + executeCompileTarget(project); + executeEnhanceTarget(project); + // Both Bar and Baz are enhanced, Foo is not + assertTrue( isEnhanced( "Bar" )); + assertTrue( isEnhanced( "Baz" )); + // Foo is not enhanced because it is not an entity and extended enhancement was not enabled + assertFalse( isEnhanced( "Foo" ) ); + // but $$_hibernate_getInterceptor is not present in the enhanced classes + // because of the 'false' value of 'enableLazyInitialization' + assertFalse( methodIsPresentInClass("$$_hibernate_getInterceptor", "Bar")); + assertFalse( methodIsPresentInClass("$$_hibernate_getInterceptor", "Baz")); + // Association management should not be present + assertFalse(isAssociationManagementPresent()); + } + + @Test + public void testEnhancementNoDirtyTracking() throws Exception { + // Change the default setting for 'enableDirtyTracking' to 'false' + // Otherwise use the settings of #testEnhancementDefault + String enhanceTag = + "\n"; + Project project = createProject(enhanceTag); + executeCompileTarget(project); + executeEnhanceTarget(project); + // Both Bar and Baz should be enhanced + assertTrue( isEnhanced( "Bar" )); + assertTrue( isEnhanced( "Baz" )); + // Foo is not enhanced because it is not an entity and extended enhancement was not enabled + assertFalse( isEnhanced( "Foo" ) ); + // $$_hibernate_hasDirtyAttributes is not present in the enhanced classes + // because of the 'false' value of 'enableLazyInitialization' + assertFalse( methodIsPresentInClass("$$_hibernate_hasDirtyAttributes", "Bar")); + assertFalse( methodIsPresentInClass("$$_hibernate_hasDirtyAttributes", "Baz")); + // Association management should not be present + assertFalse(isAssociationManagementPresent()); + } + + @Test + public void testEnhancementEnableAssociationManagement() throws Exception { + // Change the default setting for 'enableAssociationManagement' to 'true' + // Otherwise use the settings of #testEnhancementDefault + String enhanceTag = + "\n"; + Project project = createProject(enhanceTag); + executeCompileTarget(project); + executeEnhanceTarget(project); + // Both Bar and Baz are enhanced, Foo is not + assertTrue( isEnhanced( "Bar" )); + assertTrue( isEnhanced( "Baz" )); + assertFalse( isEnhanced( "Foo" ) ); + // Now verify that the association management is in place; + assertTrue(isAssociationManagementPresent()); + } + + @Test + public void testEnhancementEnableExtendedEnhancement() throws Exception { + // Change the default setting for 'enableExtendedEnhancement' to 'true' + // Otherwise use the settings of #testEnhancementDefault + String enhanceTag = + "\n"; + Project project = createProject(enhanceTag); + executeCompileTarget(project); + executeEnhanceTarget(project); + // Both Bar and Baz are enhanced because they are entities + assertTrue( isEnhanced( "Bar" )); + assertTrue( isEnhanced( "Baz" )); + // Though Foo is not an entity, it is enhanced because of the setting of 'enableExtendedEnhancement' + assertTrue( isEnhanced( "Foo" ) ); + // No association management is in place; + assertFalse(isAssociationManagementPresent()); + } + + @Test + public void testNoEnhancement() throws Exception { + // Setting the values of all the settings to 'false' has the effect + // of not executing the enhancement at all. + // The setting of 'enableAssociationManagement' and 'enableExtendedEnhancement' to + // false is not really needed in this case as that's what their default is + String enhanceTag = + "\n"; + Project project = createProject(enhanceTag); + executeCompileTarget(project); + executeEnhanceTarget(project); + // None of the classes should be enhanced + assertFalse( isEnhanced( "Bar" )); + assertFalse( isEnhanced( "Baz" )); + assertFalse( isEnhanced( "Foo" ) ); + // No association management is in place; + assertFalse(isAssociationManagementPresent()); + } + + private boolean isAssociationManagementPresent() throws Exception { + // Some dynamic programming + ClassLoader loader = getTestClassLoader(); + // Obtain the class objects for 'Baz' and 'Bar' + Class bazClass = loader.loadClass( "Baz" ); + Class barClass = loader.loadClass( "Bar" ); + // Create an instance of both 'Baz' and 'Bar' + Object bazObject = bazClass.getDeclaredConstructor().newInstance(); + Object barObject = barClass.getDeclaredConstructor().newInstance(); + // Lookup the 'bars' field of class 'Baz' (an ArrayList of 'Bar' objects) + Field bazBarsField = bazClass.getDeclaredField( "bars" ); + bazBarsField.setAccessible( true ); + // Obtain the 'bars' list of the 'Baz' object; it should be empty + List bazBarsList = (List) bazBarsField.get( bazObject ); // baz.bars + assertTrue(bazBarsList.isEmpty()); + // Lookup the 'setBaz' method of class 'Bar' and invoke it on the 'Bar' object + Method barSetBazMethod = barClass.getDeclaredMethod( "setBaz", new Class[] { bazClass } ); + barSetBazMethod.invoke( barObject, bazObject ); // bar.setBaz(baz) + // Reobtain the 'bars' list of the 'Baz' object + bazBarsList = (List) bazBarsField.get( bazObject ); + // If there is association management, the 'bars' list should contain the 'Bar' object + return bazBarsList.contains( barObject ); // baz.bars.contains(bar) + } + + private Project createProject(String enhanceTag) throws Exception { + File buildXmlFile = createBuildXmlFile(enhanceTag); + Project result = new Project(); + result.setBaseDir(projectDir); + result.addBuildListener(createConsoleLogger()); + ProjectHelper.getProjectHelper().parse(result, buildXmlFile); + return result; + } + + private void executeCompileTarget(Project project) { + // The class files should not exist + assertFalse(fileExists("dest/Bar.class")); + assertFalse(fileExists("dest/Baz.class")); + assertFalse(fileExists("dest/Foo.class")); + // Execute the 'compile' target + project.executeTarget( "compile" ); + // The class files should exist now + assertTrue( fileExists( "dest/Bar.class" ) ); + assertTrue( fileExists( "dest/Baz.class" ) ); + assertTrue( fileExists( "dest/Foo.class" ) ); + } + + private void executeEnhanceTarget(Project project) throws Exception { + // The class files should not be enhanced at this point + assertFalse( isEnhanced( "Bar" )); + assertFalse( isEnhanced( "Baz" )); + assertFalse( isEnhanced( "Foo" )); + // Execute the 'enhance' target + project.executeTarget( "enhance" ); + // The results are verified in the respective tests + } + + private File createBuildXmlFile(String enhanceTag) throws Exception { + File result = new File( projectDir, "build.xml" ); + assertFalse(result.exists()); + Files.writeString( + result.toPath(), + BUILD_XML_TEMPLATE.replace( "@enhanceTag@", enhanceTag ) ); + assertTrue( result.exists() ); + assertTrue(result.isFile()); + return result; + } + + private DefaultLogger createConsoleLogger() { + DefaultLogger consoleLogger = new DefaultLogger(); + consoleLogger.setErrorPrintStream(System.err); + consoleLogger.setOutputPrintStream(System.out); + consoleLogger.setMessageOutputLevel(Project.MSG_INFO); + return consoleLogger; + } + + private boolean fileExists(String relativePath) { + return new File( projectDir, relativePath ).exists(); + } + + private void prepareDestFolder() { + File destFolder = new File(projectDir, "dest"); + assertFalse( destFolder.exists() ); + assertTrue( destFolder.mkdir() ); + assertTrue( destFolder.exists() ); + assertTrue( destFolder.isDirectory() ); + } + + private boolean isEnhanced(String className) throws Exception { + return getTestClassLoader().loadClass( className ).isAnnotationPresent( EnhancementInfo.class ); + } + + private boolean methodIsPresentInClass(String methodName, String className) throws Exception { + Class classToCheck = getTestClassLoader().loadClass( className ); + try { + Object m = classToCheck.getMethod( methodName, new Class[] {} ); + return true; + } catch (NoSuchMethodException e) { + return false; + } + } + + private ClassLoader getTestClassLoader() throws Exception { + return new URLClassLoader( new URL[] { new File(projectDir, "dest").toURI().toURL() } ); + } + + private void copyJavFiles() throws Exception { + File srcDir = new File(projectDir, "src"); + srcDir.mkdir(); + String[] javFileNames = {"Bar.jav_", "Baz.jav_", "Foo.jav_"}; + for (String javFileName : javFileNames) { + copyJavFile( javFileName, srcDir ); + } + } + + private void copyJavFile(String javFileName, File toFolder) throws Exception { + URL url = getClass().getClassLoader().getResource( javFileName ); + assert url != null; + File source = new File(url.toURI()); + File destination = new File(toFolder, javFileName.replace( '_', 'a' )); + assertTrue(source.exists()); + assertTrue(source.isFile()); + Files.copy(source.toPath(), destination.toPath()); + assertTrue(destination.exists()); + assertTrue(destination.isFile()); + } + + private static String BUILD_XML_TEMPLATE = + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " @enhanceTag@\n" + + " \n" + + ""; +} diff --git a/tooling/hibernate-ant/src/test/resources/Bar.jav_ b/tooling/hibernate-ant/src/test/resources/Bar.jav_ new file mode 100644 index 000000000000..a5b7325842f7 --- /dev/null +++ b/tooling/hibernate-ant/src/test/resources/Bar.jav_ @@ -0,0 +1,24 @@ +import jakarta.persistence.Entity; +import jakarta.persistence.ManyToOne; + +@Entity +public class Bar { + + private String foo; + + @ManyToOne + private Baz baz; + + public String getFoo() { + return foo; + } + + public void setFoo(String f) { + foo = f; + } + + public Baz getBaz() { return baz; } + + public void setBaz(Baz baz) { this.baz = baz; } + +} diff --git a/tooling/hibernate-ant/src/test/resources/Baz.jav_ b/tooling/hibernate-ant/src/test/resources/Baz.jav_ new file mode 100644 index 000000000000..5e33d11767bb --- /dev/null +++ b/tooling/hibernate-ant/src/test/resources/Baz.jav_ @@ -0,0 +1,27 @@ +import jakarta.persistence.Entity; +import jakarta.persistence.OneToMany; + +import java.util.List; +import java.util.ArrayList; + +@Entity +public class Baz { + + private String foo; + + @OneToMany(mappedBy = "baz") + private List bars = new ArrayList(); + + String getFoo() { + return foo; + } + + public void setFoo(String f) { + foo = f; + } + + public void addBar(Bar bar) { + bars.add( bar ); + } + +} diff --git a/tooling/hibernate-ant/src/test/resources/Foo.jav_ b/tooling/hibernate-ant/src/test/resources/Foo.jav_ new file mode 100644 index 000000000000..4c3df431118c --- /dev/null +++ b/tooling/hibernate-ant/src/test/resources/Foo.jav_ @@ -0,0 +1,13 @@ +public class Foo { + + private Bar bar; + + Bar getBar() { + return bar; + } + + public void setBar(Bar b) { + bar = b; + } + +}