alvinalexander.com | career | drupal | java | mac | mysql | perl | scala | uml | unix  

Hibernate example source code file (DefaultMergeEventListener.java)

This example Hibernate source code file (DefaultMergeEventListener.java) is included in the DevDaily.com "Java Source Code Warehouse" project. The intent of this project is to help you "Learn Java by Example" TM.

Java - Hibernate tags/keywords

entitypersister, entitypersister, eventcache, eventcache, eventsource, hibernateexception, io, map, map, object, object, override, property, serializable, string, util

The Hibernate DefaultMergeEventListener.java source code

/*
 * Hibernate, Relational Persistence for Idiomatic Java
 *
 * Copyright (c) 2008-2011, 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.event.internal;

import java.io.Serializable;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

import org.jboss.logging.Logger;

import org.hibernate.AssertionFailure;
import org.hibernate.HibernateException;
import org.hibernate.engine.internal.Cascade;
import org.hibernate.engine.spi.EntityEntry;
import org.hibernate.engine.spi.SessionImplementor;
import org.hibernate.event.spi.EventSource;
import org.hibernate.event.spi.MergeEvent;
import org.hibernate.event.spi.MergeEventListener;
import org.hibernate.internal.CoreMessageLogger;
import org.hibernate.ObjectDeletedException;
import org.hibernate.PropertyValueException;
import org.hibernate.StaleObjectStateException;
import org.hibernate.TransientObjectException;
import org.hibernate.WrongClassException;
import org.hibernate.bytecode.instrumentation.internal.FieldInterceptionHelper;
import org.hibernate.bytecode.instrumentation.spi.FieldInterceptor;
import org.hibernate.engine.spi.CascadingAction;
import org.hibernate.engine.spi.EntityKey;
import org.hibernate.engine.spi.Status;
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 CoreMessageLogger LOG = Logger.getMessageLogger(CoreMessageLogger.class,
                                                                       DefaultMergeEventListener.class.getName());

	@Override
    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 );
                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 );
				EntityState entityState = null;

				// 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 ) {
						final EntityKey key = source.generateEntityKey( id, persister );
						final 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 = EntityState.DETACHED;
						}
					}
				}

				if ( entityState == null ) {
					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 );
			Object propertyFromEntity = persister.getPropertyValue( entity, propertyName );
			Type propertyType = persister.getPropertyType( propertyName );
			EntityEntry copyEntry = source.getPersistenceContext().getEntry( copy );
			if ( propertyFromCopy == null ||
					propertyFromEntity == null ||
					! propertyType.isEntityType() ||
					! copyCache.containsKey( propertyFromEntity ) ) {
				if ( LOG.isTraceEnabled() ) {
                    LOG.trace("Property '" + copyEntry.getEntityName() + "." + propertyName + "' in copy is "
                              + (propertyFromCopy == null ? "null" : propertyFromCopy));
                    LOG.trace("Property '" + copyEntry.getEntityName() + "." + propertyName + "' in original is "
                              + (propertyFromCopy == null ? "null" : propertyFromCopy));
                    LOG.trace("Property '" + copyEntry.getEntityName() + "." + propertyName + "' is"
                              + (propertyType.isEntityType() ? "" : " not") + " an entity type");
                    if (propertyFromEntity != null && !copyCache.containsKey(propertyFromEntity)) {
						LOG.tracef(
								"Property '%s.%s' is not in copy cache",
								copyEntry.getEntityName(),
								propertyName
						);
					}
	            }
                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.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.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 ),
				persister.getVersion( entity )
		);

		// 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 ) {
				final EntityKey key = source.generateEntityKey( id, persister );
				final Object managedEntity = source.getPersistenceContext().getEntity( key );
				entry = source.getPersistenceContext().getEntry( managedEntity );
			}
		}

		return entry != null && 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 ),
				persister.getPropertyValues( target ),
				persister.getPropertyTypes(),
				source,
				target,
				copyCache
		);

		persister.setPropertyValues( target, copiedValues );
	}

	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 ),
					persister.getPropertyValues( target ),
					persister.getPropertyTypes(),
					source,
					target,
					copyCache,
					foreignKeyDirection
			);
		}
		else {
			copiedValues = TypeHelper.replace(
					persister.getPropertyValues( entity ),
					persister.getPropertyValues( target ),
					persister.getPropertyTypes(),
					source,
					target,
					copyCache,
					foreignKeyDirection
			);
		}

		persister.setPropertyValues( target, copiedValues );
	}

	/**
	 * 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();
		}
	}


	@Override
    protected CascadingAction getCascadeAction() {
		return CascadingAction.MERGE;
	}

	@Override
    protected Boolean getAssumedUnsaved() {
		return Boolean.FALSE;
	}

	/**
	 * Cascade behavior is redefined by this subclass, disable superclass behavior
	 */
	@Override
    protected void cascadeAfterSave(EventSource source, EntityPersister persister, Object entity, Object anything)
	throws HibernateException {
	}

	/**
	 * Cascade behavior is redefined by this subclass, disable superclass behavior
	 */
	@Override
    protected void cascadeBeforeSave(EventSource source, EntityPersister persister, Object entity, Object anything)
	throws HibernateException {
	}
}

Other Hibernate examples (source code examples)

Here is a short list of links related to this Hibernate DefaultMergeEventListener.java source code file:

... this post is sponsored by my books ...

#1 New Release!

FP Best Seller

 

new blog posts

 

Copyright 1998-2021 Alvin Alexander, alvinalexander.com
All Rights Reserved.

A percentage of advertising revenue from
pages under the /java/jwarehouse URI on this website is
paid back to open source projects.