Java 5 EnumUserType

Hibernate 3 Parameterized type for mapping a Java 5 Enum.

This allows you to avoid the need to define a concrete UserType instance for every enum that you have. Just create a new typedef for each one, giving it a unique type name. Then reference this type name in the property tag.

Example Mapping - inline <type> tag

  <property name='suit'>
    <type name="EnumUserType">
      <param name="enumClassName">com.company.project.Suit</param>
    </type>
  </property>

Example Mapping - using <typedef>

  <typedef name="suit" class='EnumUserType'>
      <param name="enumClassName">com.company.project.Suit</param>
  </typedef>

  <class ...>
    <property name='suit' type='suit'/>
  </class>

EnumUserType.java

import java.io.Serializable;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
import java.util.Properties;

import org.hibernate.HibernateException;
import org.hibernate.MappingException;
import org.hibernate.usertype.ParameterizedType;
import org.hibernate.usertype.UserType;

public class EnumUserType implements UserType, ParameterizedType {
   
   private Class clazz = null;
   
   public void setParameterValues(Properties params) {
      String enumClassName = params.getProperty("enumClassName");
      if (enumClassName == null) {
         throw new MappingException("enumClassName parameter not specified");
      }
      
      try {
            this.clazz = Class.forName(enumClassName);
        } catch (ClassNotFoundException e) {
         throw new MappingException("enumClass " + enumClassName + " not found", e);
        }
   }
   
    private static final int[] SQL_TYPES = {Types.VARCHAR};
    public int[] sqlTypes() {
        return SQL_TYPES;
    }

    public Class returnedClass() {
        return clazz;
    }

    public Object nullSafeGet(ResultSet resultSet, String[] names, Object owner)
                             throws HibernateException, SQLException {
        String name = resultSet.getString(names[0]);
        Object result = null;
        if (!resultSet.wasNull()) {
            result = Enum.valueOf(clazz, name);
        }
        return result;
    }

   public void nullSafeSet(PreparedStatement preparedStatement, Object value, int index) 
                          throws HibernateException, SQLException {
        if (null == value) {
            preparedStatement.setNull(index, Types.VARCHAR);
        } else {
            preparedStatement.setString(index, value.name());
        }
    }

    public Object deepCopy(Object value) throws HibernateException{
        return value;
    }

    public boolean isMutable() {
        return false;
    }

    public Object assemble(Serializable cached, Object owner) throws HibernateException {
        return cached;
    }

    public Serializable disassemble(Object value) throws HibernateException {
        return (Serializable)value;
    }

    public Object replace(Object original, Object target, Object owner) throws HibernateException {
        return original;
    }
    public int hashCode(Object x) throws HibernateException {
        return x.hashCode();
    }
    public boolean equals(Object x, Object y) throws HibernateException {
        if (x == y)
            return true;
        if (null == x || null == y)
            return false;
        return x.equals(y);
    }
}

Enhanced UserType

Here is my version of this, it's an EnhancedUserType which means it can be used for an id or a discriminator

package org.hibernate.demo;

import java.io.Serializable;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
import java.util.Properties;

import org.hibernate.HibernateException;
import org.hibernate.usertype.EnhancedUserType;
import org.hibernate.usertype.ParameterizedType;

/**
 * @author Gavin King
 */
public class EnumUserType implements EnhancedUserType, ParameterizedType {
    
    private Class<Enum> enumClass;

    public void setParameterValues(Properties parameters) {
        String enumClassName = parameters.getProperty("enumClassName");
        try {
            enumClass = (Class<Enum>) Class.forName(enumClassName);
        }
        catch (ClassNotFoundException cnfe) {
            throw new HibernateException("Enum class not found", cnfe);
        }
    }

    public Object assemble(Serializable cached, Object owner) 
    throws HibernateException {
        return cached;
    }

    public Object deepCopy(Object value) throws HibernateException {
        return value;
    }

    public Serializable disassemble(Object value) throws HibernateException {
        return (Enum) value;
    }

    public boolean equals(Object x, Object y) throws HibernateException {
        return x==y;
    }

    public int hashCode(Object x) throws HibernateException {
        return x.hashCode();
    }

    public boolean isMutable() {
        return false;
    }

    public Object nullSafeGet(ResultSet rs, String[] names, Object owner) 
    throws HibernateException, SQLException {
        String name = rs.getString( names[0] );
        return rs.wasNull() ? null : Enum.valueOf(enumClass, name);
    }

    public void nullSafeSet(PreparedStatement st, Object value, int index) 
    throws HibernateException, SQLException {
        if (value==null) {
            st.setNull(index, Types.VARCHAR);
        }
        else {
            st.setString( index, ( (Enum) value ).name() ); 
        }
    }

    public Object replace(Object original, Object target, Object owner) 
    throws HibernateException {
        return original;
    }

    public Class returnedClass() {
        return enumClass;
    }

    public int[] sqlTypes() {
        return new int[] { Types.VARCHAR };
    }

    public Object fromXMLString(String xmlValue) {
        return Enum.valueOf(enumClass, xmlValue);
    }

    public String objectToSQLString(Object value) {
        return '\'' + ( (Enum) value ).name() + '\'';
    }

    public String toXMLString(Object value) {
        return ( (Enum) value ).name();
    }

}

Flexible solution

The version I like to present deals with various types of enum representations. It successfully handles int, long and string based enumeration patterns. The flexibilty is introduced by the identifierMethod and the valueOfMethod properties of the user type mapping - which are optional (check the code). Check out the JavaDoc comment for a simple example. If you find bugs or gaps, please drop me a line. I personally would also refactor the longer setParameters method, but this version is more compact.

import java.lang.reflect.Method;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
import java.util.Properties;

import org.hibernate.HibernateException;
import org.hibernate.type.NullableType;
import org.hibernate.type.TypeFactory;
import org.hibernate.usertype.ParameterizedType;

/**
 * Implements a generic enum user type identified / represented by a single identifier / column.
 * <p><ul>
 *    <li>The enum type being represented by the certain user type must be set
 *        by using the 'enumClass' property.</li>
 *    <li>The identifier representing a enum value is retrieved by the identifierMethod.
 *        The name of the identifier method can be specified by the 
 *        'identifierMethod' property and by default the name() method is used.</li>
 *    <li>The identifier type is automatically determined by 
 *        the return-type of the identifierMethod.</li>
 *    <li>The valueOfMethod is the name of the static factory method returning
 *        the enumeration object being represented by the given indentifier. 
 *        The valueOfMethod's name can be specified by setting the 'valueOfMethod'
 *        property. The default valueOfMethod's name is 'valueOf'.</li>
 * </p> 
 * <p>
 * Example of an enum type represented by an int value:
 * <code>
 * public enum SimpleNumber {
 *   Unknown(-1), Zero(0), One(1), Two(2), Three(3);
 *   int value;
 *   protected SimpleNumber(int value) {
 *       this.value = value;
 *       }
 *
 *   public int toInt() { return value; }
 *   public SimpleNumber fromInt(int value) {
 *         switch(value) {
 *          case 0: return Zero;
 *         case 1: return One;
 *         case 2: return Two;
 *         case 3: return Three;
 *         default:
 *                 return Unknown;
 *     }
 *   }
 * }
 * </code>
 * <p>
 * The Mapping would look like this:
 * <code>
 *    <typedef name="SimpleNumber" class="GenericEnumUserType">
 *        <param name="enumClass">SimpleNumber</param>
 *        <param name="identifierMethod">toInt</param>
 *        <param name="valueOfMethod">fromInt</param>
 *    </typedef>
 *    <class ...>
 *      ...
 *     <property name="number" column="number" type="SimpleNumber"/>
 *    </class>
 * </code>
 *
 * @author Martin Kersten
 * @since 05.05.2005
 */

public class GenericEnumUserType extends AbstractUserType implements ParameterizedType  {
    private Class<? extends Enum> enumClass;
    private Class<?> identifierType;

    private Method identifierMethod;
    private Method valueOfMethod;
    
    private static final String defaultIdentifierMethodName = "name";
    private static final String defaultValueOfMethodName = "valueOf";
    
    private NullableType type;
    private int [] sqlTypes;
    
    public void setParameterValues(Properties parameters) {
        String enumClassName = parameters.getProperty("enumClass");
        try {
            enumClass = Class.forName(enumClassName).asSubclass(Enum.class);
        }
        catch (ClassNotFoundException exception) {
            throw new HibernateException("Enum class not found", exception);
        }
        
        String identifierMethodName = 
            parameters.getProperty("identifierMethod", defaultIdentifierMethodName);

        try {
            identifierMethod = enumClass.getMethod(identifierMethodName, new Class[0]);
            identifierType = identifierMethod.getReturnType();
        }
        catch(Exception exception) {
            throw new HibernateException("Failed to optain identifier method", exception);
        }
        
        type = (NullableType)TypeFactory.basic(identifierType.getName());
        
        if(type == null)
            throw new HibernateException("Unsupported identifier type " + identifierType.getName());
        
        sqlTypes = new int [] {type.sqlType()};

        String valueOfMethodName = 
            parameters.getProperty("valueOfMethod", defaultValueOfMethodName);
        
        try {
            valueOfMethod = enumClass.getMethod(
                    valueOfMethodName, new Class[] { identifierType });
        } 
        catch(Exception exception) {
            throw new HibernateException("Failed to optain valueOf method", exception);
        }
    }
    
    public Class returnedClass() {
        return enumClass;
    }
    
    public Object nullSafeGet(ResultSet rs, String[] names, Object owner) 
                        throws HibernateException, SQLException {
        Object identifier=type.get(rs, names[0]);
        try {
            return valueOfMethod.invoke(enumClass, new Object [] {identifier});
        }
        catch(Exception exception) {
            throw new HibernateException(
                    "Exception while invoking valueOfMethod of enumeration class: ", exception);
        } 
    }

    public void nullSafeSet(PreparedStatement st, Object value, int index) 
            throws HibernateException, SQLException {
        try {
            Object identifier = value != null ? identifierMethod.invoke(value, new Object[0]) : null;
            st.setObject(index, identifier);
        }
        catch(Exception exception) {
            throw new HibernateException(
                    "Exception while invoking identifierMethod of enumeration class: ", exception);

        } 
    }
    public int[] sqlTypes() {
        return sqlTypes;
        //There was a logical bug within the set-up phase of any user type
        //I reported the issue and it got instantly solved (Thanks again Garvin!)
        //But it might still exist in your Hibernate version. So if you are 
        //facing any null-pointer exceptions, use the return statement below. 
        //Note: INTEGER works even for String based mappings...
        //return new int [] {Types.INTEGER};
    }
 
        public Object assemble(Serializable cached, Object owner) throws HibernateException {
        return cached;
    }

    public Object deepCopy(Object value) throws HibernateException {
        return value;
    }

    public Serializable disassemble(Object value) throws HibernateException {
        return (Serializable)value;
    }

    public boolean equals(Object x, Object y) throws HibernateException {
        return x==y;
    }

    public int hashCode(Object x) throws HibernateException {
        return x.hashCode();
    }

    public boolean isMutable() {
        return false;
    }

    public Object replace(Object original, Object target, Object owner) throws HibernateException {
        return original;
    }
}

Flexible solution - working version

Even after applying the various recommended fixes from the comments, I still found that Martin Kersten's GenericEnumUserType had problems in some cases. After adding some additional tweaks I now have a version that seems to work great. Enjoy!

import java.io.Serializable;
import java.lang.reflect.Method;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Properties;

import org.hibernate.HibernateException;
import org.hibernate.type.NullableType;
import org.hibernate.type.TypeFactory;
import org.hibernate.usertype.ParameterizedType;
import org.hibernate.usertype.UserType;


public class GenericEnumUserType implements UserType, ParameterizedType {
    private static final String DEFAULT_IDENTIFIER_METHOD_NAME = "name";
    private static final String DEFAULT_VALUE_OF_METHOD_NAME = "valueOf";

    private Class<? extends Enum> enumClass;
    private Class<?> identifierType;
    private Method identifierMethod;
    private Method valueOfMethod;
    private NullableType type;
    private int[] sqlTypes;

    public void setParameterValues(Properties parameters) {
        String enumClassName = parameters.getProperty("enumClass");
        try {
            enumClass = Class.forName(enumClassName).asSubclass(Enum.class);
        } catch (ClassNotFoundException cfne) {
            throw new HibernateException("Enum class not found", cfne);
        }

        String identifierMethodName = parameters.getProperty("identifierMethod", DEFAULT_IDENTIFIER_METHOD_NAME);

        try {
            identifierMethod = enumClass.getMethod(identifierMethodName, new Class[0]);
            identifierType = identifierMethod.getReturnType();
        } catch (Exception e) {
            throw new HibernateException("Failed to obtain identifier method", e);
        }

        type = (NullableType) TypeFactory.basic(identifierType.getName());

        if (type == null)
            throw new HibernateException("Unsupported identifier type " + identifierType.getName());

        sqlTypes = new int[] { type.sqlType() };

        String valueOfMethodName = parameters.getProperty("valueOfMethod", DEFAULT_VALUE_OF_METHOD_NAME);

        try {
            valueOfMethod = enumClass.getMethod(valueOfMethodName, new Class[] { identifierType });
        } catch (Exception e) {
            throw new HibernateException("Failed to obtain valueOf method", e);
        }
    }

    public Class returnedClass() {
        return enumClass;
    }

    public Object nullSafeGet(ResultSet rs, String[] names, Object owner) throws HibernateException, SQLException {  
        Object identifier = type.get(rs, names[0]);
        if (rs.wasNull()) {
            return null;
        }
        
        try {
            return valueOfMethod.invoke(enumClass, new Object[] { identifier });
        } catch (Exception e) {
            throw new HibernateException("Exception while invoking valueOf method '" + valueOfMethod.getName() + "' of " +
                    "enumeration class '" + enumClass + "'", e);
        }
    }

    public void nullSafeSet(PreparedStatement st, Object value, int index) throws HibernateException, SQLException {
        try {
            if (value == null) {
                st.setNull(index, type.sqlType());
            } else {
                Object identifier = identifierMethod.invoke(value, new Object[0]);
                type.set(st, identifier, index);
            }
        } catch (Exception e) {
            throw new HibernateException("Exception while invoking identifierMethod '" + identifierMethod.getName() + "' of " +
                    "enumeration class '" + enumClass + "'", e);
        }
    }

    public int[] sqlTypes() {
        return sqlTypes;
    }

    public Object assemble(Serializable cached, Object owner) throws HibernateException {
        return cached;
    }

    public Object deepCopy(Object value) throws HibernateException {
        return value;
    }

    public Serializable disassemble(Object value) throws HibernateException {
        return (Serializable) value;
    }

    public boolean equals(Object x, Object y) throws HibernateException {
        return x == y;
    }

    public int hashCode(Object x) throws HibernateException {
        return x.hashCode();
    }

    public boolean isMutable() {
        return false;
    }

    public Object replace(Object original, Object target, Object owner) throws HibernateException {
        return original;
    }
}