/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* Copyright (c) 2008, Red Hat Middleware LLC 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 Middleware LLC.
*
* 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.event.def;
import java.io.Serializable;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.HashSet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.hibernate.AssertionFailure;
import org.hibernate.HibernateException;
import org.hibernate.ObjectDeletedException;
import org.hibernate.StaleObjectStateException;
import org.hibernate.TransientObjectException;
import org.hibernate.WrongClassException;
import org.hibernate.PropertyValueException;
import org.hibernate.engine.Cascade;
import org.hibernate.engine.CascadingAction;
import org.hibernate.engine.EntityEntry;
import org.hibernate.engine.EntityKey;
import org.hibernate.engine.SessionImplementor;
import org.hibernate.engine.Status;
import org.hibernate.event.EventSource;
import org.hibernate.event.MergeEvent;
import org.hibernate.event.MergeEventListener;
import org.hibernate.intercept.FieldInterceptionHelper;
import org.hibernate.intercept.FieldInterceptor;
import org.hibernate.persister.entity.EntityPersister;
import org.hibernate.proxy.HibernateProxy;
import org.hibernate.proxy.LazyInitializer;
import org.hibernate.type.ForeignKeyDirection;
import org.hibernate.type.Type;
import org.hibernate.type.TypeHelper;
/**
* Defines the default copy event listener used by hibernate for copying entities
* in response to generated copy events.
*
* @author Gavin King
*/
public class DefaultMergeEventListener extends AbstractSaveEventListener
implements MergeEventListener {
private static final Logger log = LoggerFactory.getLogger(DefaultMergeEventListener.class);
protected Map getMergeMap(Object anything) {
return ( ( EventCache ) anything ).invertMap();
}
/**
* Handle the given merge event.
*
* @param event The merge event to be handled.
* @throws HibernateException
*/
public void onMerge(MergeEvent event) throws HibernateException {
EventCache copyCache = new EventCache();
onMerge( event, copyCache );
// TODO: iteratively get transient entities and retry merge until one of the following conditions:
// 1) transientCopyCache.size() == 0
// 2) transientCopyCache.size() is not decreasing and copyCache.size() is not increasing
// TODO: find out if retrying can add entities to copyCache (don't think it can...)
// For now, just retry once; throw TransientObjectException if there are still any transient entities
Map transientCopyCache = getTransientCopyCache(event, copyCache );
if ( transientCopyCache.size() > 0 ) {
retryMergeTransientEntities( event, transientCopyCache, copyCache, true );
// find any entities that are still transient after retry
transientCopyCache = getTransientCopyCache(event, copyCache );
if ( transientCopyCache.size() > 0 ) {
Set transientEntityNames = new HashSet();
for( Iterator it=transientCopyCache.entrySet().iterator(); it.hasNext(); ) {
Object transientEntity = ( ( Map.Entry ) it.next() ).getKey();
String transientEntityName = event.getSession().guessEntityName( transientEntity );
transientEntityNames.add( transientEntityName );
log.trace( "transient instance could not be processed by merge when checking nullability: " +
transientEntityName + "[" + transientEntity + "]" );
}
if ( isNullabilityCheckedGlobal( event.getSession() ) ) {
throw new TransientObjectException(
"one or more objects is an unsaved transient instance - save transient instance(s) before merging: " +
transientEntityNames );
}
else {
log.trace( "retry saving transient instances without checking nullability" );
// failures will be detected later...
retryMergeTransientEntities( event, transientCopyCache, copyCache, false );
}
}
}
copyCache.clear();
copyCache = null;
}
protected EventCache getTransientCopyCache(MergeEvent event, EventCache copyCache) {
EventCache transientCopyCache = new EventCache();
for ( Iterator it=copyCache.entrySet().iterator(); it.hasNext(); ) {
Map.Entry mapEntry = ( Map.Entry ) it.next();
Object entity = mapEntry.getKey();
Object copy = mapEntry.getValue();
if ( copy instanceof HibernateProxy ) {
copy = ( (HibernateProxy) copy ).getHibernateLazyInitializer().getImplementation();
}
EntityEntry copyEntry = event.getSession().getPersistenceContext().getEntry( copy );
if ( copyEntry == null ) {
// entity name will not be available for non-POJO entities
// TODO: cache the entity name somewhere so that it is available to this exception
log.trace( "transient instance could not be processed by merge: " +
event.getSession().guessEntityName( copy ) + "[" + entity + "]" );
// merge did not cascade to this entity; it's in copyCache because a
// different entity has a non-nullable reference to this entity;
// this entity should not be put in transientCopyCache, because it was
// not included in the merge;
// if the global setting for checking nullability is false, the non-nullable
// reference to this entity will be detected later
if ( isNullabilityCheckedGlobal( event.getSession() ) ) {
throw new TransientObjectException(
"object is an unsaved transient instance - save the transient instance before merging: " +
event.getSession().guessEntityName( copy )
);
}
}
else if ( copyEntry.getStatus() == Status.SAVING ) {
transientCopyCache.put( entity, copy, copyCache.isOperatedOn( entity ) );
}
else if ( copyEntry.getStatus() != Status.MANAGED && copyEntry.getStatus() != Status.READ_ONLY ) {
throw new AssertionFailure( "Merged entity does not have status set to MANAGED or READ_ONLY; "+copy+" status="+copyEntry.getStatus() );
}
}
return transientCopyCache;
}
protected void retryMergeTransientEntities(
MergeEvent event,
Map transientCopyCache,
EventCache copyCache,
boolean isNullabilityChecked) {
// TODO: The order in which entities are saved may matter (e.g., a particular transient entity
// may need to be saved before other transient entities can be saved;
// Keep retrying the batch of transient entities until either:
// 1) there are no transient entities left in transientCopyCache
// or 2) no transient entities were saved in the last batch
// For now, just run through the transient entities and retry the merge
for ( Iterator it=transientCopyCache.entrySet().iterator(); it.hasNext(); ) {
Map.Entry mapEntry = ( Map.Entry ) it.next();
Object entity = mapEntry.getKey();
Object copy = transientCopyCache.get( entity );
EntityEntry copyEntry = event.getSession().getPersistenceContext().getEntry( copy );
mergeTransientEntity(
entity,
copyEntry.getEntityName(),
( entity == event.getEntity() ? event.getRequestedId() : copyEntry.getId() ),
event.getSession(),
copyCache,
isNullabilityChecked
);
}
}
/**
* Handle the given merge event.
*
* @param event The merge event to be handled.
* @throws HibernateException
*/
public void onMerge(MergeEvent event, Map copiedAlready) throws HibernateException {
final EventCache copyCache = ( EventCache ) copiedAlready;
final EventSource source = event.getSession();
final Object original = event.getOriginal();
if ( original != null ) {
final Object entity;
if ( original instanceof HibernateProxy ) {
LazyInitializer li = ( (HibernateProxy) original ).getHibernateLazyInitializer();
if ( li.isUninitialized() ) {
log.trace("ignoring uninitialized proxy");
event.setResult( source.load( li.getEntityName(), li.getIdentifier() ) );
return; //EARLY EXIT!
}
else {
entity = li.getImplementation();
}
}
else {
entity = original;
}
if ( copyCache.containsKey( entity ) &&
( copyCache.isOperatedOn( entity ) ) ) {
log.trace("already in merge process");
event.setResult( entity );
}
else {
if ( copyCache.containsKey( entity ) ) {
log.trace("already in copyCache; setting in merge process");
copyCache.setOperatedOn( entity, true );
}
event.setEntity( entity );
int entityState = -1;
// Check the persistence context for an entry relating to this
// entity to be merged...
EntityEntry entry = source.getPersistenceContext().getEntry( entity );
if ( entry == null ) {
EntityPersister persister = source.getEntityPersister( event.getEntityName(), entity );
Serializable id = persister.getIdentifier( entity, source );
if ( id != null ) {
EntityKey key = new EntityKey( id, persister, source.getEntityMode() );
Object managedEntity = source.getPersistenceContext().getEntity( key );
entry = source.getPersistenceContext().getEntry( managedEntity );
if ( entry != null ) {
// we have specialized case of a detached entity from the
// perspective of the merge operation. Specifically, we
// have an incoming entity instance which has a corresponding
// entry in the current persistence context, but registered
// under a different entity instance
entityState = DETACHED;
}
}
}
if ( entityState == -1 ) {
entityState = getEntityState( entity, event.getEntityName(), entry, source );
}
switch (entityState) {
case DETACHED:
entityIsDetached(event, copyCache);
break;
case TRANSIENT:
entityIsTransient(event, copyCache);
break;
case PERSISTENT:
entityIsPersistent(event, copyCache);
break;
default: //DELETED
throw new ObjectDeletedException(
"deleted instance passed to merge",
null,
getLoggableName( event.getEntityName(), entity )
);
}
}
}
}
protected void entityIsPersistent(MergeEvent event, Map copyCache) {
log.trace("ignoring persistent instance");
//TODO: check that entry.getIdentifier().equals(requestedId)
final Object entity = event.getEntity();
final EventSource source = event.getSession();
final EntityPersister persister = source.getEntityPersister( event.getEntityName(), entity );
( ( EventCache ) copyCache ).put( entity, entity, true ); //before cascade!
cascadeOnMerge(source, persister, entity, copyCache);
copyValues(persister, entity, entity, source, copyCache);
event.setResult(entity);
}
protected void entityIsTransient(MergeEvent event, Map copyCache) {
log.trace("merging transient instance");
final Object entity = event.getEntity();
final EventSource source = event.getSession();
final EntityPersister persister = source.getEntityPersister( event.getEntityName(), entity );
final String entityName = persister.getEntityName();
event.setResult( mergeTransientEntity( entity, entityName, event.getRequestedId(), source, copyCache, true ) );
}
protected Object mergeTransientEntity(Object entity, String entityName, Serializable requestedId, EventSource source, Map copyCache) {
return mergeTransientEntity( entity, entityName, requestedId, source, copyCache, true );
}
private Object mergeTransientEntity(
Object entity,
String entityName,
Serializable requestedId,
EventSource source,
Map copyCache,
boolean isNullabilityChecked) {
log.trace("merging transient instance");
final EntityPersister persister = source.getEntityPersister( entityName, entity );
final Serializable id = persister.hasIdentifierProperty() ?
persister.getIdentifier( entity, source ) :
null;
if ( copyCache.containsKey( entity ) ) {
persister.setIdentifier( copyCache.get( entity ), id, source );
}
else {
( ( EventCache ) copyCache ).put( entity, source.instantiate( persister, id ), true ); //before cascade!
}
final Object copy = copyCache.get( entity );
// cascade first, so that all unsaved objects get their
// copy created before we actually copy
//cascadeOnMerge(event, persister, entity, copyCache, Cascades.CASCADE_BEFORE_MERGE);
super.cascadeBeforeSave(source, persister, entity, copyCache);
copyValues(persister, entity, copy, source, copyCache, ForeignKeyDirection.FOREIGN_KEY_FROM_PARENT);
try {
// try saving; check for non-nullable properties that are null or transient entities before saving
saveTransientEntity( copy, entityName, requestedId, source, copyCache, isNullabilityChecked );
}
catch (PropertyValueException ex) {
String propertyName = ex.getPropertyName();
Object propertyFromCopy = persister.getPropertyValue( copy, propertyName, source.getEntityMode() );
Object propertyFromEntity = persister.getPropertyValue( entity, propertyName, source.getEntityMode() );
Type propertyType = persister.getPropertyType( propertyName );
EntityEntry copyEntry = source.getPersistenceContext().getEntry( copy );
if ( propertyFromCopy == null ||
propertyFromEntity == null ||
! propertyType.isEntityType() ||
! copyCache.containsKey( propertyFromEntity ) ) {
if ( log.isTraceEnabled() ) {
String fullPropertyName = "property '" + copyEntry.getEntityName() + "." + propertyName;
log.trace( fullPropertyName + " in copy is " + ( propertyFromCopy == null ? "null" : propertyFromCopy ) );
log.trace( fullPropertyName + " in original is " + ( propertyFromCopy == null ? "null" : propertyFromCopy ) );
log.trace( fullPropertyName + ( propertyType.isEntityType() ? " is" : " is not" ) + " an entity type" );
if ( propertyFromEntity != null && ! copyCache.containsKey( propertyFromEntity ) ) {
log.trace( fullPropertyName + " is not in copy cache" );
}
}
if ( isNullabilityCheckedGlobal( source ) ) {
throw ex;
}
else {
// retry save w/o checking for non-nullable properties
// (the failure will be detected later)
saveTransientEntity( copy, entityName, requestedId, source, copyCache, false );
}
}
if ( log.isTraceEnabled() && propertyFromEntity != null ) {
if ( ( ( EventCache ) copyCache ).isOperatedOn( propertyFromEntity ) ) {
log.trace( "property '" + copyEntry.getEntityName() + "." + propertyName +
"' from original entity is in copyCache and is in the process of being merged; " +
propertyName + " =["+propertyFromEntity+"]");
}
else {
log.trace( "property '" + copyEntry.getEntityName() + "." + propertyName +
"' from original entity is in copyCache and is not in the process of being merged; " +
propertyName + " =["+propertyFromEntity+"]");
}
}
// continue...; we'll find out if it ends up not getting saved later
}
// cascade first, so that all unsaved objects get their
// copy created before we actually copy
super.cascadeAfterSave(source, persister, entity, copyCache);
copyValues(persister, entity, copy, source, copyCache, ForeignKeyDirection.FOREIGN_KEY_TO_PARENT);
return copy;
}
private boolean isNullabilityCheckedGlobal(EventSource source) {
return source.getFactory().getSettings().isCheckNullability();
}
private void saveTransientEntity(
Object entity,
String entityName,
Serializable requestedId,
EventSource source,
Map copyCache,
boolean isNullabilityChecked) {
boolean isNullabilityCheckedOrig =
source.getFactory().getSettings().isCheckNullability();
try {
source.getFactory().getSettings().setCheckNullability( isNullabilityChecked );
//this bit is only *really* absolutely necessary for handling
//requestedId, but is also good if we merge multiple object
//graphs, since it helps ensure uniqueness
if (requestedId==null) {
saveWithGeneratedId( entity, entityName, copyCache, source, false );
}
else {
saveWithRequestedId( entity, requestedId, entityName, copyCache, source );
}
}
finally {
source.getFactory().getSettings().setCheckNullability( isNullabilityCheckedOrig );
}
}
protected void entityIsDetached(MergeEvent event, Map copyCache) {
log.trace("merging detached instance");
final Object entity = event.getEntity();
final EventSource source = event.getSession();
final EntityPersister persister = source.getEntityPersister( event.getEntityName(), entity );
final String entityName = persister.getEntityName();
Serializable id = event.getRequestedId();
if ( id == null ) {
id = persister.getIdentifier( entity, source );
}
else {
// check that entity id = requestedId
Serializable entityId = persister.getIdentifier( entity, source );
if ( !persister.getIdentifierType().isEqual( id, entityId, source.getEntityMode(), source.getFactory() ) ) {
throw new HibernateException( "merge requested with id not matching id of passed entity" );
}
}
String previousFetchProfile = source.getFetchProfile();
source.setFetchProfile("merge");
//we must clone embedded composite identifiers, or
//we will get back the same instance that we pass in
final Serializable clonedIdentifier = (Serializable) persister.getIdentifierType()
.deepCopy( id, source.getEntityMode(), source.getFactory() );
final Object result = source.get(entityName, clonedIdentifier);
source.setFetchProfile(previousFetchProfile);
if ( result == null ) {
//TODO: we should throw an exception if we really *know* for sure
// that this is a detached instance, rather than just assuming
//throw new StaleObjectStateException(entityName, id);
// we got here because we assumed that an instance
// with an assigned id was detached, when it was
// really persistent
entityIsTransient(event, copyCache);
}
else {
( ( EventCache ) copyCache ).put( entity, result, true ); //before cascade!
final Object target = source.getPersistenceContext().unproxy(result);
if ( target == entity ) {
throw new AssertionFailure("entity was not detached");
}
else if ( !source.getEntityName(target).equals(entityName) ) {
throw new WrongClassException(
"class of the given object did not match class of persistent copy",
event.getRequestedId(),
entityName
);
}
else if ( isVersionChanged( entity, source, persister, target ) ) {
if ( source.getFactory().getStatistics().isStatisticsEnabled() ) {
source.getFactory().getStatisticsImplementor()
.optimisticFailure( entityName );
}
throw new StaleObjectStateException( entityName, id );
}
// cascade first, so that all unsaved objects get their
// copy created before we actually copy
cascadeOnMerge(source, persister, entity, copyCache);
copyValues(persister, entity, target, source, copyCache);
//copyValues works by reflection, so explicitly mark the entity instance dirty
markInterceptorDirty( entity, target );
event.setResult(result);
}
}
private void markInterceptorDirty(final Object entity, final Object target) {
if ( FieldInterceptionHelper.isInstrumented( entity ) ) {
FieldInterceptor interceptor = FieldInterceptionHelper.extractFieldInterceptor( target );
if ( interceptor != null ) {
interceptor.dirty();
}
}
}
private boolean isVersionChanged(Object entity, EventSource source, EntityPersister persister, Object target) {
if ( ! persister.isVersioned() ) {
return false;
}
// for merging of versioned entities, we consider the version having
// been changed only when:
// 1) the two version values are different;
// *AND*
// 2) The target actually represents database state!
//
// This second condition is a special case which allows
// an entity to be merged during the same transaction
// (though during a seperate operation) in which it was
// originally persisted/saved
boolean changed = ! persister.getVersionType().isSame(
persister.getVersion( target, source.getEntityMode() ),
persister.getVersion( entity, source.getEntityMode() ),
source.getEntityMode()
);
// TODO : perhaps we should additionally require that the incoming entity
// version be equivalent to the defined unsaved-value?
return changed && existsInDatabase( target, source, persister );
}
private boolean existsInDatabase(Object entity, EventSource source, EntityPersister persister) {
EntityEntry entry = source.getPersistenceContext().getEntry( entity );
if ( entry == null ) {
Serializable id = persister.getIdentifier( entity, source );
if ( id != null ) {
EntityKey key = new EntityKey( id, persister, source.getEntityMode() );
Object managedEntity = source.getPersistenceContext().getEntity( key );
entry = source.getPersistenceContext().getEntry( managedEntity );
}
}
if ( entry == null ) {
// perhaps this should be an exception since it is only ever used
// in the above method?
return false;
}
else {
return entry.isExistsInDatabase();
}
}
protected void copyValues(
final EntityPersister persister,
final Object entity,
final Object target,
final SessionImplementor source,
final Map copyCache
) {
final Object[] copiedValues = TypeHelper.replace(
persister.getPropertyValues( entity, source.getEntityMode() ),
persister.getPropertyValues( target, source.getEntityMode() ),
persister.getPropertyTypes(),
source,
target,
copyCache
);
persister.setPropertyValues( target, copiedValues, source.getEntityMode() );
}
protected void copyValues(
final EntityPersister persister,
final Object entity,
final Object target,
final SessionImplementor source,
final Map copyCache,
final ForeignKeyDirection foreignKeyDirection) {
final Object[] copiedValues;
if ( foreignKeyDirection == ForeignKeyDirection.FOREIGN_KEY_TO_PARENT ) {
// this is the second pass through on a merge op, so here we limit the
// replacement to associations types (value types were already replaced
// during the first pass)
copiedValues = TypeHelper.replaceAssociations(
persister.getPropertyValues( entity, source.getEntityMode() ),
persister.getPropertyValues( target, source.getEntityMode() ),
persister.getPropertyTypes(),
source,
target,
copyCache,
foreignKeyDirection
);
}
else {
copiedValues = TypeHelper.replace(
persister.getPropertyValues( entity, source.getEntityMode() ),
persister.getPropertyValues( target, source.getEntityMode() ),
persister.getPropertyTypes(),
source,
target,
copyCache,
foreignKeyDirection
);
}
persister.setPropertyValues( target, copiedValues, source.getEntityMode() );
}
/**
* Perform any cascades needed as part of this copy event.
*
* @param source The merge event being processed.
* @param persister The persister of the entity being copied.
* @param entity The entity being copied.
* @param copyCache A cache of already copied instance.
*/
protected void cascadeOnMerge(
final EventSource source,
final EntityPersister persister,
final Object entity,
final Map copyCache
) {
source.getPersistenceContext().incrementCascadeLevel();
try {
new Cascade( getCascadeAction(), Cascade.BEFORE_MERGE, source )
.cascade(persister, entity, copyCache);
}
finally {
source.getPersistenceContext().decrementCascadeLevel();
}
}
protected CascadingAction getCascadeAction() {
return CascadingAction.MERGE;
}
protected Boolean getAssumedUnsaved() {
return Boolean.FALSE;
}
/**
* Cascade behavior is redefined by this subclass, disable superclass behavior
*/
protected void cascadeAfterSave(EventSource source, EntityPersister persister, Object entity, Object anything)
throws HibernateException {
}
/**
* Cascade behavior is redefined by this subclass, disable superclass behavior
*/
protected void cascadeBeforeSave(EventSource source, EntityPersister persister, Object entity, Object anything)
throws HibernateException {
}
}