HHH-8489 - bytecode enhancement: bi-directional associtation management

Closes #933
This commit is contained in:
barreiro 2015-03-31 19:01:56 +01:00 committed by Steve Ebersole
parent 74161c9bfa
commit 165f037bad
12 changed files with 553 additions and 75 deletions

View File

@ -67,7 +67,7 @@ public abstract class AttributeTypeDescriptor {
|| currentValue.getType().getName().startsWith( "java.sql.Date" ) || currentValue.getType().getName().startsWith( "java.sql.Date" )
|| currentValue.getType().getName().startsWith( "java.util.Date" ) || currentValue.getType().getName().startsWith( "java.util.Date" )
|| currentValue.getType().getName().startsWith( "java.util.Calendar" ) ) { || currentValue.getType().getName().startsWith( "java.util.Calendar" ) ) {
builder.append( String.format( "&& ((%s == null) || (!%<s.equals($1))))", currentValue.getName() ) ); builder.append( String.format( " && ((%s == null) || (!%<s.equals($1))))", currentValue.getName() ) );
} }
// all other objects // all other objects
else { else {
@ -81,7 +81,7 @@ public abstract class AttributeTypeDescriptor {
} }
} }
// TODO: for now just call equals, should probably do something else here // TODO: for now just call equals, should probably do something else here
builder.append( String.format( "&& ((%s == null) || (!%<s.equals($1))))", currentValue.getName() ) ); builder.append( String.format( " && ((%s == null) || (!%<s.equals($1))))", currentValue.getName() ) );
} }
builder.append( String.format( " { %s(\"%s\"); }", EnhancerConstants.TRACKER_CHANGER_NAME, currentValue.getName() ) ); builder.append( String.format( " { %s(\"%s\"); }", EnhancerConstants.TRACKER_CHANGER_NAME, currentValue.getName() ) );
} }

View File

@ -34,9 +34,10 @@ import javassist.bytecode.CodeIterator;
import javassist.bytecode.ConstPool; import javassist.bytecode.ConstPool;
import javassist.bytecode.MethodInfo; import javassist.bytecode.MethodInfo;
import javassist.bytecode.Opcode; import javassist.bytecode.Opcode;
import javassist.bytecode.SignatureAttribute;
import javassist.bytecode.stackmap.MapMaker; import javassist.bytecode.stackmap.MapMaker;
import org.hibernate.bytecode.enhance.spi.EnhancementException;
import org.hibernate.bytecode.enhance.spi.EnhancementContext; import org.hibernate.bytecode.enhance.spi.EnhancementContext;
import org.hibernate.bytecode.enhance.spi.EnhancementException;
import org.hibernate.bytecode.enhance.spi.Enhancer; import org.hibernate.bytecode.enhance.spi.Enhancer;
import org.hibernate.bytecode.enhance.spi.EnhancerConstants; import org.hibernate.bytecode.enhance.spi.EnhancerConstants;
import org.hibernate.engine.spi.CompositeOwner; import org.hibernate.engine.spi.CompositeOwner;
@ -45,6 +46,10 @@ import org.hibernate.internal.CoreLogging;
import org.hibernate.internal.CoreMessageLogger; import org.hibernate.internal.CoreMessageLogger;
import javax.persistence.Embedded; import javax.persistence.Embedded;
import javax.persistence.ManyToMany;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;
import javax.persistence.OneToOne;
import java.util.IdentityHashMap; import java.util.IdentityHashMap;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
@ -117,7 +122,7 @@ public class PersistentAttributesEnhancer extends Enhancer {
// TODO: temporary solution... // TODO: temporary solution...
try { try {
return MethodWriter.write( managedCtClass, "private %s %s() {%n %s%n return this.%s;%n}", return MethodWriter.write( managedCtClass, "public %s %s() {%n %s%n return this.%s;%n}",
persistentField.getType().getName(), persistentField.getType().getName(),
readerName, readerName,
typeDescriptor.buildReadInterceptionBodyFragment( fieldName ), typeDescriptor.buildReadInterceptionBodyFragment( fieldName ),
@ -144,7 +149,7 @@ public class PersistentAttributesEnhancer extends Enhancer {
writer = MethodWriter.addSetter( managedCtClass, fieldName, writerName ); writer = MethodWriter.addSetter( managedCtClass, fieldName, writerName );
} }
else { else {
writer = MethodWriter.write( managedCtClass, "private void %s(%s %s) {%n %s%n}", writer = MethodWriter.write( managedCtClass, "public void %s(%s %s) {%n %s%n}",
writerName, writerName,
persistentField.getType().getName(), persistentField.getType().getName(),
fieldName, fieldName,
@ -160,36 +165,10 @@ public class PersistentAttributesEnhancer extends Enhancer {
writer.insertBefore( typeDescriptor.buildInLineDirtyCheckingBodyFragment( enhancementContext, persistentField ) ); writer.insertBefore( typeDescriptor.buildInLineDirtyCheckingBodyFragment( enhancementContext, persistentField ) );
} }
// composite fields handleCompositeField( managedCtClass, persistentField, writer );
if ( persistentField.hasAnnotation( Embedded.class ) ) {
// make sure to add the CompositeOwner interface
managedCtClass.addInterface( classPool.get( CompositeOwner.class.getName() ) );
if ( enhancementContext.isCompositeClass( managedCtClass ) ) { if ( enhancementContext.doBiDirectionalAssociationManagement( persistentField ) ) {
// if a composite have a embedded field we need to implement the TRACKER_CHANGER_NAME method as well handleBiDirectionalAssociation( managedCtClass, persistentField, writer );
MethodWriter.write( managedCtClass, "" +
"public void %1$s(String name) {%n" +
" if (%2$s != null) { %2$s.callOwner(\".\" + name) ; }%n}",
EnhancerConstants.TRACKER_CHANGER_NAME,
EnhancerConstants.TRACKER_COMPOSITE_FIELD_NAME );
}
// cleanup previous owner
writer.insertBefore( String.format( "" +
"if (%1$s != null) { ((%2$s) %1$s).%3$s(\"%1$s\"); }%n",
fieldName,
CompositeTracker.class.getName(),
EnhancerConstants.TRACKER_COMPOSITE_CLEAR_OWNER ) );
// trigger track changes
writer.insertAfter( String.format( "" +
"((%2$s) %1$s).%4$s(\"%1$s\", (%3$s) this);%n" +
"%5$s(\"%1$s\");",
fieldName,
CompositeTracker.class.getName(),
CompositeOwner.class.getName(),
EnhancerConstants.TRACKER_COMPOSITE_SET_OWNER,
EnhancerConstants.TRACKER_CHANGER_NAME ) );
} }
return writer; return writer;
} }
@ -205,6 +184,187 @@ public class PersistentAttributesEnhancer extends Enhancer {
/* --- */ /* --- */
private void handleBiDirectionalAssociation(CtClass managedCtClass, CtField persistentField, CtMethod fieldWriter) throws NotFoundException, CannotCompileException {
if ( !isPossibleBiDirectionalAssociation( persistentField ) ) {
return;
}
final CtClass targetEntity = getTargetEntityClass( persistentField );
if ( targetEntity == null ) {
log.debugf( "Could not find type of bi-directional association for field [%s#%s]", managedCtClass.getName(), persistentField.getName() );
return;
}
final String mappedBy = getMappedBy( persistentField, targetEntity );
if ( mappedBy.isEmpty() ) {
log.debugf( "Could not find bi-directional association for field [%s#%s]", managedCtClass.getName(), persistentField.getName() );
return;
}
// create a temporary getter and setter on the target entity to be able to compile our code
final String mappedByGetterName = EnhancerConstants.PERSISTENT_FIELD_READER_PREFIX + mappedBy;
final String mappedBySetterName = EnhancerConstants.PERSISTENT_FIELD_WRITER_PREFIX + mappedBy;
MethodWriter.addGetter( targetEntity, mappedBy, mappedByGetterName );
MethodWriter.addSetter( targetEntity, mappedBy, mappedBySetterName );
if ( persistentField.hasAnnotation( OneToOne.class ) ) {
// only unset when $1 != null to avoid recursion
fieldWriter.insertBefore( String.format( "if ($0.%s != null && $1 != null) $0.%<s.%s(null);%n",
persistentField.getName(),
mappedBySetterName));
fieldWriter.insertAfter( String.format( "if ($1 != null && $1.%s() != $0) $1.%s($0);%n",
mappedByGetterName,
mappedBySetterName) );
}
if ( persistentField.hasAnnotation( OneToMany.class ) ) {
// only remove elements not in the new collection or else we would loose those elements
fieldWriter.insertBefore( String.format( "if ($0.%s != null) for (java.util.Iterator itr = $0.%<s.iterator(); itr.hasNext(); ) { %s target = (%<s) itr.next(); if ($1 == null || !$1.contains(target)) target.%s(null); }%n",
persistentField.getName(),
targetEntity.getName(),
mappedBySetterName ) );
fieldWriter.insertAfter( String.format( "if ($1 != null) for (java.util.Iterator itr = $1.iterator(); itr.hasNext(); ) { %s target = (%<s) itr.next(); if (target.%s() != $0) target.%s((%s)$0); }%n",
targetEntity.getName(),
mappedByGetterName,
mappedBySetterName,
managedCtClass.getName() ) );
}
if ( persistentField.hasAnnotation( ManyToOne.class ) ) {
fieldWriter.insertBefore( String.format( "if ($0.%1$s != null && $0.%1$s.%2$s() != null) $0.%1$s.%2$s().remove($0);%n",
persistentField.getName(),
mappedByGetterName) );
// check .contains($0) to avoid double inserts (but preventing duplicates)
fieldWriter.insertAfter( String.format( "if ($1 != null && $1.%s() != null && !$1.%<s().contains($0) ) $1.%<s().add($0);%n",
mappedByGetterName));
}
if ( persistentField.hasAnnotation( ManyToMany.class ) ) {
fieldWriter.insertBefore( String.format( "if ($0.%s != null) for (java.util.Iterator itr = $0.%<s.iterator(); itr.hasNext(); ) { %s target = (%<s) itr.next(); if ($1 == null || !$1.contains(target)) target.%s().remove($0); }%n",
persistentField.getName(),
targetEntity.getName(),
mappedByGetterName));
fieldWriter.insertAfter( String.format( "if ($1 != null) for (java.util.Iterator itr = $1.iterator(); itr.hasNext(); ) { %s target = (%<s) itr.next(); if (target.%s() != $0 && target.%<s() != null) target.%<s().add($0); }%n",
targetEntity.getName(),
mappedByGetterName ) );
}
// implementation note: association management @OneToMany and @ManyToMay works for add() operations but for remove() a snapshot of the collection is needed so we know what associations to break.
// another approach that could force that behavior would be to return Collections.unmodifiableCollection() ...
}
private boolean isPossibleBiDirectionalAssociation(CtField persistentField) {
return persistentField.hasAnnotation( OneToOne.class ) ||
persistentField.hasAnnotation( OneToMany.class ) ||
persistentField.hasAnnotation( ManyToOne.class) ||
persistentField.hasAnnotation( ManyToMany.class );
}
private String getMappedBy(CtField persistentField, CtClass targetEntity) {
final String local = getMappedByFromAnnotation( persistentField );
return local.isEmpty() ? getMappedByFromTargetEntity( persistentField, targetEntity ) : local;
}
private String getMappedByFromAnnotation(CtField persistentField) {
try {
if (persistentField.hasAnnotation( OneToOne.class )) {
return ((OneToOne) persistentField.getAnnotation( OneToOne.class )).mappedBy();
}
if (persistentField.hasAnnotation( OneToMany.class )) {
return ((OneToMany) persistentField.getAnnotation( OneToMany.class )).mappedBy();
}
// For @ManyToOne associations, mappedBy must come from the @OneToMany side of the association
if (persistentField.hasAnnotation( ManyToMany.class )) {
return ((ManyToMany) persistentField.getAnnotation( ManyToMany.class )).mappedBy();
}
}
catch (ClassNotFoundException ignore) {
}
return "";
}
private String getMappedByFromTargetEntity(CtField persistentField, CtClass targetEntity) {
// get mappedBy value by searching in the fields of the target entity class
for (CtField f : targetEntity.getDeclaredFields() ) {
if ( enhancementContext.isPersistentField( f ) && getMappedByFromAnnotation( f ).equals( persistentField.getName() ) ) {
log.debugf( "mappedBy association for field [%s:%s] is [%s:%s]", persistentField.getDeclaringClass().getName(), persistentField.getName(), targetEntity.getName(), f.getName() );
return f.getName();
}
}
return "";
}
private CtClass getTargetEntityClass(CtField persistentField) throws NotFoundException {
// get targetEntity defined in the annotation
try {
Class<?> targetClass = null;
if (persistentField.hasAnnotation( OneToOne.class )) {
targetClass = ((OneToOne) persistentField.getAnnotation( OneToOne.class )).targetEntity();
}
if (persistentField.hasAnnotation( OneToMany.class )) {
targetClass = ((OneToMany) persistentField.getAnnotation( OneToMany.class )).targetEntity();
}
if (persistentField.hasAnnotation( ManyToOne.class )) {
targetClass = ((ManyToOne) persistentField.getAnnotation( ManyToOne.class )).targetEntity();
}
if (persistentField.hasAnnotation( ManyToMany.class )) {
targetClass = ((ManyToMany) persistentField.getAnnotation( ManyToMany.class )).targetEntity();
}
if ( targetClass != null && targetClass != void.class ) {
return classPool.get( targetClass.getName() );
}
}
catch (ClassNotFoundException ignore) {
}
// infer targetEntity from generic type signature
if ( persistentField.hasAnnotation( OneToOne.class ) || persistentField.hasAnnotation( ManyToOne.class )) {
return persistentField.getType();
}
if ( persistentField.hasAnnotation( OneToMany.class ) || persistentField.hasAnnotation( ManyToMany.class )) {
try {
final SignatureAttribute.TypeArgument target = ((SignatureAttribute.ClassType) SignatureAttribute.toFieldSignature( persistentField.getGenericSignature() )).getTypeArguments()[0];
return persistentField.getDeclaringClass().getClassPool().get( target.toString() );
}
catch (BadBytecode ignore) {
}
}
return null;
}
/* --- */
private void handleCompositeField(CtClass managedCtClass, CtField persistentField, CtMethod fieldWriter) throws NotFoundException, CannotCompileException {
if ( !persistentField.hasAnnotation( Embedded.class ) ) {
return;
}
// make sure to add the CompositeOwner interface
managedCtClass.addInterface( classPool.get( CompositeOwner.class.getName() ) );
if ( enhancementContext.isCompositeClass( managedCtClass ) ) {
// if a composite have a embedded field we need to implement the TRACKER_CHANGER_NAME method as well
MethodWriter.write( managedCtClass, "" +
"public void %1$s(String name) {%n" +
" if (%2$s != null) { %2$s.callOwner(\".\" + name) ; }%n}",
EnhancerConstants.TRACKER_CHANGER_NAME,
EnhancerConstants.TRACKER_COMPOSITE_FIELD_NAME );
}
// cleanup previous owner
fieldWriter.insertBefore( String.format( "" +
"if (%1$s != null) { ((%2$s) %1$s).%3$s(\"%1$s\"); }%n",
persistentField.getName(),
CompositeTracker.class.getName(),
EnhancerConstants.TRACKER_COMPOSITE_CLEAR_OWNER) );
// trigger track changes
fieldWriter.insertAfter( String.format( "" +
"((%2$s) %1$s).%4$s(\"%1$s\", (%3$s) this);%n" +
"%5$s(\"%1$s\");",
persistentField.getName(),
CompositeTracker.class.getName(),
CompositeOwner.class.getName(),
EnhancerConstants.TRACKER_COMPOSITE_SET_OWNER,
EnhancerConstants.TRACKER_CHANGER_NAME ) );
}
/* --- */
protected void enhanceAttributesAccess(CtClass managedCtClass, IdentityHashMap<String, PersistentAttributeAccessMethods> attributeDescriptorMap) { protected void enhanceAttributesAccess(CtClass managedCtClass, IdentityHashMap<String, PersistentAttributeAccessMethods> attributeDescriptorMap) {
final ConstPool constPool = managedCtClass.getClassFile().getConstPool(); final ConstPool constPool = managedCtClass.getClassFile().getConstPool();

View File

@ -61,6 +61,13 @@ public class DefaultEnhancementContext implements EnhancementContext {
return classDescriptor.hasAnnotation( Embeddable.class ); return classDescriptor.hasAnnotation( Embeddable.class );
} }
/**
* @return true
*/
public boolean doBiDirectionalAssociationManagement(CtField field) {
return true;
}
/** /**
* @return true * @return true
*/ */

View File

@ -69,6 +69,17 @@ public interface EnhancementContext {
*/ */
public boolean isCompositeClass(CtClass classDescriptor); public boolean isCompositeClass(CtClass classDescriptor);
/**
* Should we manage association of bi-directional persistent attributes for this field?
*
* @param field The field to check.
*
* @return {@code true} indicates that the field is enhanced so that for bi-directional persistent fields
* the association is managed, i.e. the associations are automatically set; {@code false} indicates that
* the management is handled by the user.
*/
public boolean doBiDirectionalAssociationManagement(CtField field);
/** /**
* Should we in-line dirty checking for persistent attributes for this class? * Should we in-line dirty checking for persistent attributes for this class?
* *

View File

@ -23,6 +23,23 @@
*/ */
package org.hibernate.tool.enhance; package org.hibernate.tool.enhance;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtField;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.DirectoryScanner;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.types.FileSet;
import org.hibernate.bytecode.enhance.spi.EnhancementContext;
import org.hibernate.bytecode.enhance.spi.Enhancer;
import javax.persistence.ElementCollection;
import javax.persistence.Embeddable;
import javax.persistence.Entity;
import javax.persistence.ManyToMany;
import javax.persistence.OneToMany;
import javax.persistence.Transient;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
@ -30,25 +47,6 @@ import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import javax.persistence.ElementCollection;
import javax.persistence.Embeddable;
import javax.persistence.Entity;
import javax.persistence.ManyToMany;
import javax.persistence.OneToMany;
import javax.persistence.Transient;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtField;
import org.hibernate.bytecode.enhance.spi.EnhancementContext;
import org.hibernate.bytecode.enhance.spi.Enhancer;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.DirectoryScanner;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.types.FileSet;
/** /**
* Ant task for performing build-time enhancement of entities and component/embeddable classes. * Ant task for performing build-time enhancement of entities and component/embeddable classes.
@ -74,7 +72,7 @@ public class EnhancementTask extends Task implements EnhancementContext {
@Override @Override
public void execute() throws BuildException { public void execute() throws BuildException {
log( "Starting Hibernate EnhancementTask execution", Project.MSG_INFO ); log("Starting Hibernate EnhancementTask execution", Project.MSG_INFO);
// we use the CtClass stuff here just as a simple vehicle for obtaining low level information about // we use the CtClass stuff here just as a simple vehicle for obtaining low level information about
// the class(es) contained in a file while still maintaining easy access to the underlying byte[] // the class(es) contained in a file while still maintaining easy access to the underlying byte[]
@ -116,7 +114,7 @@ public class EnhancementTask extends Task implements EnhancementContext {
private void processEntityClassFile(File javaClassFile, CtClass ctClass ) { private void processEntityClassFile(File javaClassFile, CtClass ctClass ) {
try { try {
byte[] result = enhancer.enhance( ctClass.getName(), ctClass.toBytecode() ); byte[] result = enhancer.enhance(ctClass.getName(), ctClass.toBytecode());
if(result != null) if(result != null)
writeEnhancedClass(javaClassFile, result); writeEnhancedClass(javaClassFile, result);
} }
@ -187,6 +185,11 @@ public class EnhancementTask extends Task implements EnhancementContext {
return classDescriptor.hasAnnotation(Embeddable.class); return classDescriptor.hasAnnotation(Embeddable.class);
} }
@Override
public boolean doBiDirectionalAssociationManagement(CtField field) {
return false;
}
@Override @Override
public boolean doDirtyCheckingInline(CtClass classDescriptor) { public boolean doDirtyCheckingInline(CtClass classDescriptor) {
return true; return true;

View File

@ -30,16 +30,24 @@ import org.hibernate.engine.spi.PersistentAttributeInterceptor;
import org.hibernate.test.bytecode.enhancement.entity.customer.Address; import org.hibernate.test.bytecode.enhancement.entity.customer.Address;
import org.hibernate.test.bytecode.enhancement.entity.customer.Customer; import org.hibernate.test.bytecode.enhancement.entity.customer.Customer;
import org.hibernate.test.bytecode.enhancement.entity.customer.CustomerInventory; import org.hibernate.test.bytecode.enhancement.entity.customer.CustomerInventory;
import org.hibernate.test.bytecode.enhancement.entity.customer.Group;
import org.hibernate.test.bytecode.enhancement.entity.customer.SupplierComponentPK; import org.hibernate.test.bytecode.enhancement.entity.customer.SupplierComponentPK;
import org.hibernate.test.bytecode.enhancement.entity.customer.User;
import org.hibernate.testing.junit4.BaseUnitTestCase; import org.hibernate.testing.junit4.BaseUnitTestCase;
import org.junit.Test; import org.junit.Test;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import static org.hibernate.testing.junit4.ExtraAssertions.assertTyping; import static org.hibernate.testing.junit4.ExtraAssertions.assertTyping;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull; import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame; import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
/** /**
* @author Steve Ebersole * @author Steve Ebersole
@ -52,7 +60,8 @@ public class CustomerEnhancerTest extends BaseUnitTestCase {
} }
private void testFor(Class entityClassToEnhance) throws Exception { private void testFor(Class entityClassToEnhance) throws Exception {
ClassLoader cl = new ClassLoader() {}; ClassLoader cl = new ClassLoader() {
};
// just for debugging // just for debugging
Class<?> addressClass = EnhancerTestUtils.enhanceAndDecompile(Address.class, cl); Class<?> addressClass = EnhancerTestUtils.enhanceAndDecompile(Address.class, cl);
@ -69,7 +78,7 @@ public class CustomerEnhancerTest extends BaseUnitTestCase {
assertNull(getter.invoke(entityInstance)); assertNull(getter.invoke(entityInstance));
setter.invoke(entityInstance, EnhancerTestUtils.makeEntityEntry()); setter.invoke(entityInstance, EnhancerTestUtils.makeEntityEntry());
assertNotNull(getter.invoke(entityInstance)); assertNotNull(getter.invoke(entityInstance));
setter.invoke(entityInstance, new Object[] {null}); setter.invoke(entityInstance, new Object[]{null});
assertNull(getter.invoke(entityInstance)); assertNull(getter.invoke(entityInstance));
Method entityInstanceGetter = entityClass.getMethod(EnhancerConstants.ENTITY_INSTANCE_GETTER_NAME); Method entityInstanceGetter = entityClass.getMethod(EnhancerConstants.ENTITY_INSTANCE_GETTER_NAME);
@ -83,7 +92,7 @@ public class CustomerEnhancerTest extends BaseUnitTestCase {
Method nextGetter = entityClass.getMethod(EnhancerConstants.PREVIOUS_GETTER_NAME); Method nextGetter = entityClass.getMethod(EnhancerConstants.PREVIOUS_GETTER_NAME);
Method nextSetter = entityClass.getMethod(EnhancerConstants.PREVIOUS_SETTER_NAME, ManagedEntity.class); Method nextSetter = entityClass.getMethod(EnhancerConstants.PREVIOUS_SETTER_NAME, ManagedEntity.class);
nextSetter.invoke(entityInstance, entityInstance); nextSetter.invoke(entityInstance, entityInstance);
assertSame( entityInstance, nextGetter.invoke(entityInstance)); assertSame(entityInstance, nextGetter.invoke(entityInstance));
// add an attribute interceptor... // add an attribute interceptor...
assertNull(entityClass.getMethod(EnhancerConstants.INTERCEPTOR_GETTER_NAME).invoke(entityInstance)); assertNull(entityClass.getMethod(EnhancerConstants.INTERCEPTOR_GETTER_NAME).invoke(entityInstance));
@ -120,4 +129,86 @@ public class CustomerEnhancerTest extends BaseUnitTestCase {
EnhancerTestUtils.checkDirtyTracking(entityInstance, "address"); EnhancerTestUtils.checkDirtyTracking(entityInstance, "address");
} }
@Test
public void testBiDirectionalAssociationManagement() throws Exception {
ClassLoader cl = new ClassLoader() {
};
Class<?> userClass = EnhancerTestUtils.enhanceAndDecompile(User.class, cl);
Class<?> groupClass = EnhancerTestUtils.enhanceAndDecompile(Group.class, cl);
Class<?> customerClass = EnhancerTestUtils.enhanceAndDecompile(Customer.class, cl);
Class<?> customerInventoryClass = EnhancerTestUtils.enhanceAndDecompile(CustomerInventory.class, cl);
Object userInstance = userClass.newInstance();
assertTyping(ManagedEntity.class, userInstance);
Object groupInstance = groupClass.newInstance();
assertTyping(ManagedEntity.class, groupInstance);
Object customerInstance = customerClass.newInstance();
assertTyping(ManagedEntity.class, customerInstance);
Object customerInventoryInstance = customerInventoryClass.newInstance();
assertTyping(ManagedEntity.class, customerInventoryInstance);
Method interceptorSetter = userClass.getMethod(EnhancerConstants.INTERCEPTOR_SETTER_NAME, PersistentAttributeInterceptor.class);
interceptorSetter.invoke(userInstance, new EnhancerTestUtils.LocalPersistentAttributeInterceptor());
/* --- @OneToOne */
userClass.getMethod("setLogin", String.class).invoke(userInstance, UUID.randomUUID().toString());
customerClass.getMethod("setUser", userClass).invoke(customerInstance, userInstance);
assertEquals(customerInstance, userClass.getMethod("getCustomer").invoke(userInstance));
// check dirty tracking is set automatically with bi-directional association management
EnhancerTestUtils.checkDirtyTracking(userInstance, "login", "customer");
Object anotherUser = userClass.newInstance();
userClass.getMethod("setLogin", String.class).invoke(anotherUser, UUID.randomUUID().toString());
customerClass.getMethod("setUser", userClass).invoke(customerInstance, anotherUser);
assertEquals(null, userClass.getMethod("getCustomer").invoke(userInstance));
assertEquals(customerInstance, userClass.getMethod("getCustomer").invoke(anotherUser));
userClass.getMethod("setCustomer", customerClass).invoke(userInstance, customerClass.newInstance());
assertEquals(userInstance, customerClass.getMethod("getUser").invoke(userClass.getMethod("getCustomer").invoke(userInstance)));
/* --- @OneToMany @ManyToOne */
assertTrue(((Collection<?>) customerClass.getMethod("getInventories").invoke(customerInstance)).isEmpty());
customerInventoryClass.getMethod("setCustomer", customerClass).invoke(customerInventoryInstance, customerInstance);
Collection<?> inventories = (Collection < ?>) customerClass.getMethod("getInventories").invoke(customerInstance);
assertTrue(inventories.size() == 1);
assertTrue(inventories.contains(customerInventoryInstance));
Object anotherCustomer = customerClass.newInstance();
customerInventoryClass.getMethod("setCustomer", customerClass).invoke(customerInventoryInstance, anotherCustomer);
assertTrue(((Collection<?>) customerClass.getMethod("getInventories").invoke(customerInstance)).isEmpty());
customerClass.getMethod("addInventory", customerInventoryClass).invoke(customerInstance, customerInventoryInstance);
assertTrue(customerInventoryClass.getMethod("getCustomer").invoke(customerInventoryInstance) == customerInstance);
inventories = (Collection < ?>) customerClass.getMethod("getInventories").invoke(customerInstance);
assertTrue(inventories.size() == 1);
customerClass.getMethod("addInventory", customerInventoryClass).invoke(customerInstance, customerInventoryClass.newInstance());
assertTrue(((Collection<?>) customerClass.getMethod("getInventories").invoke(customerInstance)).size() == 2);
/* --- @ManyToMany */
Object anotherGroup = groupClass.newInstance();
userClass.getMethod("addGroup", groupClass).invoke(userInstance, groupInstance);
userClass.getMethod("addGroup", groupClass).invoke(userInstance, anotherGroup);
userClass.getMethod("addGroup", groupClass).invoke(anotherUser, groupInstance);
assertTrue(((Collection<?>) groupClass.getMethod("getUsers").invoke(groupInstance)).size() == 2);
groupClass.getMethod("setUsers", Set.class).invoke(groupInstance, new HashSet());
assertTrue(((Collection<?>) userClass.getMethod("getGroups").invoke(userInstance)).size() == 1);
}
} }

View File

@ -32,6 +32,7 @@ import javax.persistence.Entity;
import javax.persistence.FetchType; import javax.persistence.FetchType;
import javax.persistence.Id; import javax.persistence.Id;
import javax.persistence.OneToMany; import javax.persistence.OneToMany;
import javax.persistence.OneToOne;
import javax.persistence.Table; import javax.persistence.Table;
import javax.persistence.Temporal; import javax.persistence.Temporal;
import javax.persistence.TemporalType; import javax.persistence.TemporalType;
@ -57,6 +58,9 @@ public class Customer {
@Column(name="C_ID") @Column(name="C_ID")
private int id; private int id;
@OneToOne
private User user;
@Column(name="C_FIRST") @Column(name="C_FIRST")
private String firstName; private String firstName;
@ -209,6 +213,20 @@ public class Customer {
return customerInventories; return customerInventories;
} }
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
public void addInventory(CustomerInventory inventory) {
List<CustomerInventory> list = getInventories();
list.add(inventory);
customerInventories = list;
}
public CustomerInventory addInventory(String item, int quantity, public CustomerInventory addInventory(String item, int quantity,
BigDecimal totalValue) { BigDecimal totalValue) {

View File

@ -115,6 +115,10 @@ public class CustomerInventory implements Serializable, Comparator<CustomerInven
return customer; return customer;
} }
public void setCustomer(Customer customer) {
this.customer = customer;
}
public int getCustId() { public int getCustId() {
return custId; return custId;
} }

View File

@ -0,0 +1,77 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* Copyright (c) 2012, Red Hat Inc. or third-party contributors as
* indicated by the @author tags or express copyright attribution
* statements applied by the authors. All third-party contributions are
* distributed under license by Red Hat Inc.
*
* This copyrighted material is made available to anyone wishing to use, modify,
* copy, or redistribute it subject to the terms and conditions of the GNU
* Lesser General Public License, as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
* for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this distribution; if not, write to:
* Free Software Foundation, Inc.
* 51 Franklin Street, Fifth Floor
* Boston, MA 02110-1301 USA
*/
package org.hibernate.test.bytecode.enhancement.entity.customer;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.ManyToMany;
import javax.persistence.SequenceGenerator;
import javax.persistence.Table;
import java.util.HashSet;
import java.util.Set;
/**
* @author <a href="mailto:stale.pedersen@jboss.org">Ståle W. Pedersen</a>
*/
@Entity
@Table(name = "GROUP")
@SequenceGenerator(name = "GROUP_SEQUENCE", sequenceName = "GROUP_SEQUENCE", allocationSize = 1, initialValue = 0)
public class Group {
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "GROUP_SEQUENCE")
private int id;
@Column
private String name;
@ManyToMany(mappedBy = "groups")
private Set<User> users = new HashSet<User>();
public Group() {
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Set<User> getUsers() {
return users;
}
public void setUsers(Set<User> users) {
this.users = users;
}
public void removeUser(User user) {
Set<User> set = this.users;
set.remove(user);
this.users = set;
}
}

View File

@ -0,0 +1,100 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* Copyright (c) 2012, Red Hat Inc. or third-party contributors as
* indicated by the @author tags or express copyright attribution
* statements applied by the authors. All third-party contributions are
* distributed under license by Red Hat Inc.
*
* This copyrighted material is made available to anyone wishing to use, modify,
* copy, or redistribute it subject to the terms and conditions of the GNU
* Lesser General Public License, as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
* for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this distribution; if not, write to:
* Free Software Foundation, Inc.
* 51 Franklin Street, Fifth Floor
* Boston, MA 02110-1301 USA
*/
package org.hibernate.test.bytecode.enhancement.entity.customer;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.ManyToMany;
import javax.persistence.OneToOne;
import javax.persistence.SequenceGenerator;
import javax.persistence.Table;
import java.util.HashSet;
import java.util.Set;
/**
* @author <a href="mailto:stale.pedersen@jboss.org">Ståle W. Pedersen</a>
*/
@Entity
@Table(name = "USER")
@SequenceGenerator(name = "USER_SEQUENCE", sequenceName = "USER_SEQUENCE", allocationSize = 1, initialValue = 0)
public class User {
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "USER_SEQUENCE")
private int id;
@Column
private String login;
@Column
private String password;
@OneToOne(mappedBy = "user")
private Customer customer;
@ManyToMany
private Set<Group> groups;
public User() {
}
public Customer getCustomer() {
return customer;
}
public void setCustomer(Customer customer) {
this.customer = customer;
}
public String getLogin() {
return login;
}
public void setLogin(String login) {
this.login = login;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public void addGroup(Group group) {
Set<Group> set = (groups == null ? new HashSet<Group>() : groups);
set.add(group);
groups = set;
}
public Set<Group> getGroups() {
return groups;
}
public void setGroups(Set<Group> groups) {
this.groups = groups;
}
}

View File

@ -23,26 +23,9 @@
*/ */
package org.hibernate.bytecode.enhance.plugins; package org.hibernate.bytecode.enhance.plugins;
import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import javassist.ClassPool; import javassist.ClassPool;
import javassist.CtClass; import javassist.CtClass;
import javassist.CtField; import javassist.CtField;
import javax.persistence.ElementCollection;
import javax.persistence.Embeddable;
import javax.persistence.Entity;
import javax.persistence.ManyToMany;
import javax.persistence.OneToMany;
import javax.persistence.Transient;
import org.apache.maven.plugin.AbstractMojo; import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException; import org.apache.maven.plugin.MojoFailureException;
@ -53,6 +36,21 @@ import org.apache.maven.plugins.annotations.Parameter;
import org.hibernate.bytecode.enhance.spi.EnhancementContext; import org.hibernate.bytecode.enhance.spi.EnhancementContext;
import org.hibernate.bytecode.enhance.spi.Enhancer; import org.hibernate.bytecode.enhance.spi.Enhancer;
import javax.persistence.ElementCollection;
import javax.persistence.Embeddable;
import javax.persistence.Entity;
import javax.persistence.ManyToMany;
import javax.persistence.OneToMany;
import javax.persistence.Transient;
import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/** /**
* This plugin will enhance Entity objects. * This plugin will enhance Entity objects.
* *
@ -228,6 +226,11 @@ public class MavenEnhancePlugin extends AbstractMojo implements EnhancementConte
return classDescriptor.hasAnnotation(Embeddable.class); return classDescriptor.hasAnnotation(Embeddable.class);
} }
@Override
public boolean doBiDirectionalAssociationManagement(CtField field) {
return false;
}
@Override @Override
public boolean doDirtyCheckingInline(CtClass classDescriptor) { public boolean doDirtyCheckingInline(CtClass classDescriptor) {
return true; return true;

View File

@ -146,6 +146,10 @@ public class EnhancerTask extends DefaultTask implements EnhancementContext {
return false; return false;
} }
public boolean doBiDirectionalAssociationManagement(CtField field) {
return false;
}
public boolean doDirtyCheckingInline(CtClass classDescriptor) { public boolean doDirtyCheckingInline(CtClass classDescriptor) {
return true; return true;
} }