HIBERNATE JBoss.org
 |  Register  | 
     
News 
About 
   Feature List 
   Road Map 
Documentation 
   Related Projects 
   External Documentation 
Download 
Forum & Mailinglists 
Support & Training 
JIRA Issue Tracking
Wiki Community Area


Hibernate Public Training Courses


Get Hibernate in Action eBook!


JavaWorld 2003 Finalist


Jolt Award 2004 Winner
      
Documentation > Community Area > Complex Validations using Interceptor

Complex Validations using Interceptor

Hibernate offers a couple of different ways to implement validation checking. Typically, entities are validated either internally by implementing the Validatable interface or externally by implementing the Interceptor interface. Either is a viable approach for simple "invariant" type checking (not null checks, range checks, etc).

However, both of these approaches have the (sometimes severe) limitation of not allowing session manipulation during "lifecycle" callbacks. For the Validatable interface, this includes all of its callback methods; for Interceptor interface, this includes the onFlushDirty(), onSave(), and onDelete() methods. What this means is that a validation done in these methods cannot perform a query (nor can they even trigger a lazy initialization!).

One solution is to simply perform these validations in the application's business tier prior to persisting the entities. However, this tends to lead to code duplication, or even worse forgetting to code the validation check in a certain execution branch.

The other solution, which will be discussed here, is to defer validations till after the flush using the Interceptor.onPostFlush() callback. The onPostFlush() method does not have the limitation of not being allowed to perform calls on the session. This approach does have the drawback that validations do not occur until after SQL statements have been executed against the RDBMS, but even this can be a "pro" as it allows all validation messages across all entities involved in a flush to be collected at a single time.

Below is a sample implemenation of such an interceptor. It assumes a base interface or class for a mapped entities named IDomainEntity.

public class ValidationInterceptor implements Interceptor, Serializable
{
    private List inserts = new ArrayList();
    private List deletes = new ArrayList();
    private Map updates = new Hashtable();

    public boolean onLoad(
            Object entity,
            Serializable id,
            Object[] state,
            String[] propertyNames,
            Type[] types )
    {
        return false;
    }

    public boolean onSave(
            Object entity,
            Serializable id,
            Object[] state,
            String[] propertyNames,
            Type[] types )
    {
        inserts.add(entity);
        return false;
    }

    public boolean onFlushDirty(
            Object entity,
            Serializable id,
            Object[] currentState,
            Object[] previousState,
            String[] propertyNames,
            Type[] types )
    {
        DeltaSet changes = DeltaSetCalculator.calculateDeltaSet(propertyNames, previousState, currentState);
        updates.put(entity, changes);
        return false;
    }

    public void onDelete(
            Object entity,
            Serializable id,
            Object[] state,
            String[] propertyNames,
            Type[] types )
    {
        deletes.add(entity);
    }

    public void preFlush(java.util.Iterator entities)
    {
        // Can add invariant checking here...
    }

    public void postFlush( java.util.Iterator entities )
    {
        // This implementation does not attempt to "bunch" validations for the entire flush.
        // Instead, as soon as an entity fails, the exception is thrown
        try
        {
            for (Iterator insertItr = inserts.iterator(); insertItr.hasNext(); )
            {
                Validator.validateCreation(insertItr.next());
            }

            for (Iterator updateItr = updates.entrySet().iterator(); updateItr.hasNext(); )
            {
                final Map.Entry updateEntry = (Map.Entry)updateItr.next();
                final Object entity = updateEntry.getKey();
                final DeltaSet changes = (DeltaSet)updateEntry.getValue();
                Validator.validateModification(entity, changes);
            }

            for (Iterator deleteItr = deletes.iterator(); deleteItr.hasNext(); )
            {
                Validator.validateDeletion(deleteItr.next());
            }
        }
        catch(ValidationException e)
        {
            throw new ValidationExceptionRuntimeWrapper(e);
        }
        finally
        {
            inserts.clear();
            updates.clear();
            deletes.clear();
        }
    }
    ...
}

Just be careful that "calculating changes" does not cause initialization of proxies or lazy collections. Below is a set of utility classes I use to "determine changes".

A general interface to represent a change:

public interface PropertyDelta
extends java.io.Serializable
{
    public static class Type {}
    public static final Type SIMPLE = new Type();
    public static final Type COLLECTION = new Type();
    public static final Type ASSOCIATION = new Type();

    public String getPropertyName();
    public Class getPropertyType();
    public Type getDeltaType();
}

And its various implementations:

public class SimplePropertyDelta
implements PropertyDelta
{
    private String propertyName;
    private Class propertyType;
    private Object oldValue;
    private Object newValue;

    public SimplePropertyDelta( String propertyName, Class propertyType, Object oldValue, Object newValue )
    {
        this.propertyName = propertyName;
        this.propertyType = propertyType;
        this.oldValue = oldValue;
        this.newValue = newValue;
    }

    public String getPropertyName()
    {
        return propertyName;
    }

    public Class getPropertyType()
    {
        return propertyType;
    }

    public Object getOldValue()
    {
        return oldValue;
    }

    public Object getNewValue()
    {
        return newValue;
    }

    public Type getDeltaType()
    {
        return PropertyDelta.SIMPLE;
    }

}
public class CollectionPropertyDelta
implements PropertyDelta
{
    private String propertyName;
    private Class propertyType;
    private Set additions = new HashSet();
    private Set removals = new HashSet();

    public CollectionPropertyDelta( String propertyName, Class propertyType, Collection oldValue, Collection newValue )
    {
        this.propertyName = propertyName;
        this.propertyType = propertyType;
        calculateAdditionsAndRemovals(oldValue, newValue);
    }

    public CollectionPropertyDelta( String propertyName, Class propertyType, Object[] oldValue, Object[] newValue )
    {
        this( propertyName, propertyType, Arrays.asList(oldValue), Arrays.asList(newValue) );
    }

    public String getPropertyName()
    {
        return propertyName;
    }

    public Class getPropertyType()
    {
        return propertyType;
    }

    public Set getAdditions()
    {
        return Collections.unmodifiableSet(additions);
    }

    public Set getRemovals()
    {
        return Collections.unmodifiableSet(removals);
    }

    public boolean anyChangeDetected()
    {
        return (!getAdditions().isEmpty() && !getRemovals().isEmpty());
    }

    public PropertyDelta.Type getDeltaType()
    {
        return PropertyDelta.COLLECTION;
    }

    private void calculateAdditionsAndRemovals(Collection oldValue, Collection newValue)
    {
        ////////////////////////////////////////////////////////////////////////
        // First, determine additions
        if (newValue != null)
            additions.addAll(newValue);
        if (oldValue != null)
            additions.removeAll(oldValue);

        ////////////////////////////////////////////////////////////////////////
        // Then, determine removals
        if (oldValue != null)
            removals.addAll(oldValue);
        if (newValue != null)
            removals.removeAll(newValue);
    }
}
public class AssociationPropertyDelta implements PropertyDelta
{
    private String propertyName;
    private Class propertyType;
    private Object oldId;
    private Object newId;

    public AssociationPropertyDelta(String propertyName, Class propertyType, Long oldId, Long newId)
    {
        this.propertyName = propertyName;
        this.propertyType = propertyType;
        this.oldId = oldId;
        this.newId = newId;
    }

    public String getPropertyName()
    {
        return propertyName;
    }

    public Class getPropertyType()
    {
        return propertyType;
    }

    public Object getOldValue()
    {
        return oldId;
    }

    public Object getNewValue()
    {
        return newId;
    }

    public Type getDeltaType()
    {
        return PropertyDelta.ASSOCIATION;
    }
}

Then a container to hold a collection of changes:

public class DeltaSet implements java.io.Serializable
{
    private Set deltas = new java.util.HashSet();
    private HashMap deltaPropertyNames = new HashMap();

    DeltaSet() {}

    void addDelta( PropertyDelta delta )
    {
        deltas.add( delta );
        deltaPropertyNames.put( delta.getPropertyName(), null );
    }

    public Set getDeltas()
    {
        return java.util.Collections.unmodifiableSet( deltas );
    }

    public boolean wasDelta( String propertyName )
    {
        return deltaPropertyNames.containsKey( propertyName );
    }

    void clear()
    {
        deltas.clear();
    }

}

Finally, a convenience class to parse all the changes from a given domain entity (taking great care to not trigger lazy initializations):

public class DeltaSetCalculator
{
    private static final Logger log = Logger.getLogger( DeltaSetCalculator.class );
    private static final SimpleDateFormat sdf = new SimpleDateFormat( "YYYY-MM-dd HH:mm:ss:SSS" );

    private DeltaSetCalculator() {}

    /* A hibernate-specific calculation.  Uses the values passed to the Hibernate
     * Interceptor.onFlushDirty() to perform the calculation.
     *
     * @param propertyNames A string array of all the property names passed in to
     * the obFlushDirty method.
     * @param previousState The Object array representing the previous state of
     * the properties named in the propertyNames array.
     * @param currentState The Object array representing the current state of
     * the properties named in the propertyNames array.
     * @return The DeltaSet representing the changes encountered in the property
     * states.
     */
    public static DeltaSet calculateDeltaSet( String[] propertyNames, Object[] previousState, Object[] currentState )
    {
        if (propertyNames == null || previousState == null || currentState == null)
            throw new IllegalArgumentException( "All three arrays passed to calculate a delta-set must be non-null" );
        if (propertyNames.length != previousState.length && previousState.length != currentState.length)
            throw new IllegalArgumentException( "All three arrays passed to calculate a delta-set must be of the same length" );

        DeltaSet deltaSet = new DeltaSet();
        try
        {
            Class propertyType = null;
            for (int i = 0; i < propertyNames.length; i++)
            {
                log.debug( "Starting property [" + propertyNames[i] + "]" );
                propertyType = null;
                final Object propertyPreviousState = previousState[i];
                final Object propertyCurrentState = currentState[i];
                final boolean wasPreviousNull = propertyPreviousState == null;
                final boolean isCurrentNull = propertyCurrentState == null;

                if (wasPreviousNull && isCurrentNull)
                {
                    log.debug( "Both were null; skipping" );
                    continue;
                }

                // Try to determine the property type from either currentState or,
                // previousState...  Side-note: if both are null, we cannot determine
                // the propertyType, but thats OK as no change has occurred (null==null)
                if (!isCurrentNull)
                {
                    if (propertyCurrentState instanceof IDomainEntity)
                    {
                        propertyType = IDomainEntity.class;
                    }
                    else
                    {
                        propertyType = propertyCurrentState.getClass();
                    }
                }
                else if (!wasPreviousNull)
                {
                    if (propertyPreviousState instanceof IDomainEntity)
                    {
                        propertyType = IDomainEntity.class;
                    }
                    else
                    {
                        propertyType = propertyPreviousState.getClass();
                    }
                }

                if (propertyType == null)
                {
                    log.debug( "Unable to determine property type; continuing" );
                    continue;
                }

                if (Hibernate.isInitialized(propertyPreviousState) || Hibernate.isInitialized(propertyCurrentState))
                {
                    final PropertyDelta delta = getDeltaOrNull( propertyNames[i], propertyType,
                                                                propertyPreviousState, propertyCurrentState );
                    if (delta != null)
                    {
                        deltaSet.addDelta( delta );
                    }
                }
            }
        }
        catch( Throwable t )
        {
            log.error( "Error determining delta-set", t );
        }
        finally
        {
            log.debug( "Done delta-set determination" );
        }
        return deltaSet;
    }


    /** General use DeltaSet caluclator.
     */
    public static DeltaSet calculateDeltaSet( Object obj1, Object obj2 )
    {
        if (obj1 == null || obj2 == null)
            throw new IllegalArgumentException( "Both objects passed to calculate a delta-set must be non-null" );

        DeltaSet deltaSet = new DeltaSet();
        try
        {
            BeanInfo beanInfo = Introspector.getBeanInfo( obj1.getClass(), Object.class );
            PropertyDescriptor[] pds = beanInfo.getPropertyDescriptors();

            for (int i=0; i<pds.length; i++)
            {
                final String propertyName = pds[i].getName();
                final Class propertyType = pds[i].getPropertyType();
                final Object oldValue = PropertyUtils.getProperty( obj1, propertyName );
                final Object newValue = PropertyUtils.getProperty( obj2, propertyName );

                final PropertyDelta delta = getDeltaOrNull( propertyName, propertyType, oldValue, newValue );
                if (delta != null)
                {
                    deltaSet.addDelta( delta );
                }
            }
        }
        catch( Throwable t )
        {
            log.error( "Error determining delta-set", t );
        }
        finally
        {
            log.debug( "Done delta-set determination" );
        }
        return deltaSet;
    }

    public static PropertyDelta getDeltaOrNull( String propertyName, Class propertyType, Object oldValue, Object newValue )
    {
        PropertyDelta delta = null;
        log.debug( "Checking property [name=" + propertyName + ", type=" + propertyType + "]" );

        if (IDomainEntity.class.isAssignableFrom( propertyType ))
        {
            log.debug( "Encountered property is an association type" );
            Long oldId = null;
            Long newId = null;
            if (oldValue != null)   oldId = ((IDomainEntity)oldValue).getId();
            if (newValue != null)   newId = ((IDomainEntity)newValue).getId();

            if (!areEqual( oldId, newId ))
            {
                delta = new AssociationPropertyDelta( propertyName, propertyType, oldId, newId );
            }
        }
        else if (Collection.class.isAssignableFrom( propertyType ))
        {
            log.debug( "Encountered property is a collection type" );

            Collection oldCollectionValue = (Collection)oldValue;
            Collection newCollectionValue = (Collection)newValue;

            if (Hibernate.isInitialized(oldCollectionValue) && Hibernate.isInitialized(newCollectionValue))
            {
                CollectionPropertyDelta collectionDelta = new CollectionPropertyDelta(
                        propertyName,
                        propertyType,
                        oldCollectionValue,
                        newCollectionValue
                );
                if (collectionDelta != null && collectionDelta.anyChangeDetected())
                {
                    delta = collectionDelta;
                }
                collectionDelta = null;
            }
            else
            {
                log.info( "One (or both) of a collection property was not previously initialized; have to skip" );
            }
        }
        else if (propertyType.isArray())
        {
            log.debug( "Encountered property is an array type" );

            CollectionPropertyDelta collectionDelta = new CollectionPropertyDelta(
                    propertyName,
                    propertyType,
                    (Object[])oldValue,
                    (Object[])newValue
            );
            if (collectionDelta != null && collectionDelta.anyChangeDetected())
            {
                delta = collectionDelta;
            }
            collectionDelta = null;
        }
        else
        {
            log.debug( "Property was a simple property" );
            if (!areEqual( oldValue, newValue ))
            {
                delta = new SimplePropertyDelta( propertyName, propertyType, oldValue, newValue );
            }
        }

        if (delta == null)
        {
            log.debug( "No delta occurred" );
        }
        else
        {
            log.debug( "Delta encountered" );
        }
        return delta;
    }

    public static boolean areEqual( Object obj1, Object obj2 )
    {
        if (obj1 == null && obj2 == null)
        {
            log.debug( "Both were null" );
            return true;
        }
        else if (obj1 == null || obj2 == null)
        {
            log.debug( "One or the other were null (but not both)" );
            return false;
        }
        else if ((Date.class.isAssignableFrom( obj1.getClass() ))
        || (Timestamp.class.isAssignableFrom( obj1.getClass() ))
        || (java.sql.Date.class.isAssignableFrom( obj1.getClass() ))
        || (Time.class.isAssignableFrom( obj1.getClass() )))
        {
            Date d1 = (Date)obj1;
            Date d2 = (Date)obj2;
            return d1.equals(d2) || d2.equals(d1);
        }
        else
        {
            log.debug( "Checking [" + obj1 + "] against [" + obj2 + "]" );
            return obj1.equals( obj2 );
        }
    }

}

Using Session.isDirty to Perform Complex Validations

Here's a variation of the above, using the new method Session.isDirty. It addresses the issue in the comment below about database constraints.

Background: You want to use Hibernate to enforce invariants on your persistent entities. Every time Hibernate updates an object in the database, you want a special validate method to be called. You can do this by implementing the Validatable interface, but Hibernate doesn't allow you to use the session, trigger lazy loads, etc., in Validatable.validate. That's fairly limiting.

You can instead use an interceptor that collects the objects that have been updated, and then after flush is finished, validate all of them. This is the approach outlined in the 'Complex Validations using Interceptor' Wiki topic. We independently came up with the same approach about a year ago for our project. (Although we didn't mess with the 'ChangeDeltaSet' calculation stuff. If any property of the entity has changed, we validate the whole entity. We do pass the list of dirty properties by diffing the previous/currentState arrays Hibernate passes to onFlushDirty.)

This works pretty well, but validation occurs after the update. If there is a 'non-null' constraint on the database, it's too late, we've already blown up with an SQLException. The integrity of your domain model is still preserved, but it's hard to report a meaningful error to the user.

Another solution mentioned in the Wiki topic is to use Validatable, but start a new session in validate, and validate the entity with that session. The problem there is that the temporary sesssion won't share the same IdentityMap, etc., so you could end up loading the same entity twice, having two diferent instances of the same logical entity in memory, one with any applied changes and one without, etc. So that's out.

We can improve things by using the Session.isDirty method. This method acts just like flush, except it doesn't really issue any SQL statements to the database, and Interceptor.postFlush is not called. Interceptor.onFlushDirty, however, is called. So, we can write an interceptor that captures the updated entities (like before), and write our own wrapper method for Session.flush which validates the dirty entities before flushing. The Interceptor would look like this:

public class CollectingInterceptor implements Interceptor {

  private static class IdentityKey {
    private Serializable id;
    private Class clazz;

    public IdentityKey(Serializable id, Class clazz) {
      this.id = id;
      this.clazz = clazz;
    }

    public boolean equals(Object obj) {
      IdentityKey rhs = (IdentityKey) obj;
      return (id.equals(rhs.id) && clazz.equals(rhs.clazz));
    }

    public int hashCode() {
      return id.hashCode() * clazz.hashCode();
    }
  }
  
  private Map dirtyEntitiesMap = new HashMap();

  public boolean onFlushDirty(Object entity,
                            Serializable id,
                            Object[] currentState,
                            Object[] previousState,
                            String[] propertyNames,
                            Type[] types)
                     throws CallbackException {
    dirtyEntitiesMap.put(new IdentityKey(id, entity.getClass()), entity);
  }

  public Set getDirtyEntities() {
    return dirtyEntitiesMap.values();
  }

  // other methods just return their default values...
}

You would pass an instance of this Interceptor to SessionFactory.buildSession.

The wrapper method around Session.flush might look like this:

public class MySessionWrapper {
  private Session session;
  private CollectingInterceptor interceptor;

  public MySessionWrapper(Session session, CollectingInterceptor interceptor) {
    this.session = session;
    this.interceptor = interceptor;
  }

  public Set getDirtyEntities() {
    session.isDirty();
    return interceptor.getDirtyEntities();
  }

  public void flush() {
    Iterator iter = getDirtyEntities().iterator();
    while (iter.hasNext()) {    
      validate(iter.next());    
    }
  }

  private void validate(Object entity) {
    if (obj instanceof MyValidatable) {
      MyValidatable validatable = (MyValidatable) iter.next();
      validatable.validate();
    }
  }
}

You could of course implement onSave to collect inserted entities and validate them too. Any entity that wants automatically invoked validation would implement the MyValidatable interface, which would look like this:

public inteface MyValidatable {
  public void validate();
}

If you add a validateForDelete method to this interface, and collect deleted entities in CollectingInterceptor.onDelete, you could validate before deleted entities as well.

It might be nice if Hibernate had a 'Collection Session.getDirtyEntities()' method. Then we wouldn't need a custom Interceptor.

Steve Molitor

smolitor@erac.com

      

coWiki