Member Menu
 
 Monthly JBoss newsletter:
 
Hibernate Books
CaveatEmptor

Java 5 StringValuedEnumUserType

Hibernate 3 Parameterized type for mapping a Java 5 Enum with string values.

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.

Unlike other proposals this class allows for the value in the database to be diferent from the Enum constant name.

Interface for making the access to the string value uniform

/**
 * Utility class designed to allow dinamic fidding and manipulation of Enum 
 * instances which hold a string value.
 */
public interface StringValuedEnum {    
    
    /**
     * Current string value stored in the enum.
     * @return string value.
     */
    public String getValue();
    
}

Reflecting Enums to get the string value

/**
 * Utility class designed to inspect StringValuedEnums.
 */
public class StringValuedEnumReflect {
    
    /**
     * Don't let anyone instantiate this class.
     * @throws UnsupportedOperationException Always.
     */
    private StringValuedEnumReflect() {
        throw new UnsupportedOperationException("This class must not be instanciated.");
    }
    
    /**
     * All Enum constants (instances) declared in the specified class. 
     * @param enumClass Class to reflect
     * @return Array of all declared EnumConstants (instances).
     */
    private static <T extends Enum> T[] 
            getValues(Class<T> enumClass){
        return enumClass.getEnumConstants();
    }
    
    /**
     * All possible string values of the string valued enum.
     * @param enumClass Class to reflect.
     * @return Available string values.
     */
    public static <T extends Enum & StringValuedEnum> String[] 
            getStringValues(Class<T> enumClass){ 
        T[] values = getValues(enumClass);
        String[] result = new String[values.length];
        for (int i=0; i<values.length; i++){
            result[i] = values[i].getValue();
        }
        return result;
    }
    
    /**
     * Name of the enum instance which hold the especified string value.
     * If value has duplicate enum instances than returns the first occurency.
     * @param enumClass Class to inspect.
     * @param value String.
     * @return name of the enum instance.
     */
    public static <T extends Enum & StringValuedEnum> String 
            getNameFromValue(Class<T> enumClass, String value){
        T[] values = getValues(enumClass);
        for (int i=0; i<values.length; i++){
            if (values[i].getValue().compareTo(value)==0){
                return values[i].name();
            }
        }
        return "";
    }
    
}

StringValuedEnumType

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 java.lang.reflect.*;

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

import static StringValuedEnumReflect.*;

//Please notice the calls to getNameFromValue *************************
public class StringValuedEnumType <T extends Enum & StringValuedEnum> 
        implements EnhancedUserType, ParameterizedType{
    
    /**
     * Enum class for this particular user type.
     */
    private Class<T> enumClass;

    /**
     * Value to use if null.
     */
    private String defaultValue;
    
    /** Creates a new instance of ActiveStateEnumType */
    public StringValuedEnumType() {
    }
    
    public void setParameterValues(Properties parameters) {
        String enumClassName = parameters.getProperty("enum");
        try {
            enumClass = (Class<T>) Class.forName(enumClassName).asSubclass(Enum.class).
                    asSubclass(StringValuedEnum.class); //Validates the class but does not eliminate the cast
        } catch (ClassNotFoundException cnfe) {
            throw new HibernateException("Enum class not found", cnfe);
        }

        setDefaultValue(parameters.getProperty("defaultValue"));
    }

    public String getDefaultValue() {
        return defaultValue;
    }
    
    public void setDefaultValue(String defaultValue) {
        this.defaultValue = defaultValue;
    }
    
    /**
     * The class returned by <tt>nullSafeGet()</tt>.
     * @return Class
     */
    public Class returnedClass() {
        return enumClass;
    }

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

    /**
     * Retrieve an instance of the mapped class from a JDBC resultset. Implementors
     * should handle possibility of null values.
     *
     * @param rs a JDBC result set
     * @param names the column names
     * @param owner the containing entity
     * @return Object
     * @throws HibernateException
     * @throws SQLException
     */
    public Object nullSafeGet(ResultSet rs, String[] names, Object owner)
            throws HibernateException, SQLException {
        String value = rs.getString( names[0] );
        if (value==null) {
            value = getDefaultValue();
            if (value==null){ //no default value
                return null;
            } 
        }
        String name = getNameFromValue(enumClass, value);
        Object res = rs.wasNull() ? null : Enum.valueOf(enumClass, name);
        
        return res;
    }
 
    /**
     * Write an instance of the mapped class to a prepared statement. Implementors
     * should handle possibility of null values. A multi-column type should be written
     * to parameters starting from <tt>index</tt>.
     *
     * @param st a JDBC prepared statement
     * @param value the object to write
     * @param index statement parameter index
     * @throws HibernateException
     * @throws SQLException
     */   
    public void nullSafeSet(PreparedStatement st, Object value, int index)
    throws HibernateException, SQLException {
        if (value==null) {
            st.setNull(index, Types.VARCHAR);
        } else {
            st.setString( index, ((T) value).getValue() );
        }
    }
    
    public Object assemble(Serializable cached, Object owner)
            throws HibernateException {
        return cached;
    }
    
    public Serializable disassemble(Object value) throws HibernateException {
        return (Enum) value;
    }
        
    public Object deepCopy(Object value) throws HibernateException {
        return value;
    }

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

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

    public String objectToSQLString(Object value) {
        return '\'' + ((T) value).getValue() + '\'';
    }
    
    public String toXMLString(Object value) {
        return ((T) value).getValue();
    }

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

Example Enum

/**
 * Common mapping for Active/Inactive state fields. This class is only usefull
 * when the field respect the mapping defined here.
 */
public enum ActiveState implements StringValuedEnum{
    
    /** 
     * Active state.
     */
    ACTIVE("A"),
    
    /** 
     * Inactive state
     */
    INACTIVE("I");

    /** 
     * Internal storage of state field value, please see the Enum spec for clarification.
     */ 
    private final String state;
    
    /** 
     * Enum constructor for ActiveState.
     * @param state Value of state.
     */
    ActiveState(final String state){
        this.state = state;
    }

    /**
     * Current string value stored in the enum.
     * @return string value.
     */
    public String getValue() {
        return this.state;
    }
    
}

(2005-06-29 Added Default value and corrected nullSafeGet (was not safe for nulls)


  NEW COMMENT

Small correction 04 Jun 2006, 17:49 sebastian.fiorentini
Great solution. 
Add this to the last line of nullSafeGet method:

return res;

Regards,
Sebastian.
 
Error in default values implementation 06 Aug 2006, 01:43 MattFlower
This is a nice, simple implementation, but I think there is at least one
error in the implementation of default values.

As is, nullSafeGet won't use the default value because of the check on
rs.wasNull().  If we intend to use the default value there, I believe
that (name == null) would be a better check.

Another decent question would be whether nullSafeSet should be
explicitly setting the default value or the null value.  If it should be
setting the default value, there is no handling of such.

For now, I replaced my nullSafeGet as follows:

    public Object nullSafeGet(ResultSet rs, String[] names, Object owner)
            throws HibernateException, SQLException {
        String value = rs.getString( names[0] );
        if (value==null) {
            value = getDefaultValue();
            if (value==null){ //no default value
                return null;
            }
        }
        String name = getNameFromValue(enumClass, value);
        Object res = name == null ? null : Enum.valueOf(enumClass, name);

        return res;
    }
 
Re: Error in default values implementation 04 Sep 2006, 14:11 levmatta
On 06 Aug 2006 01:43, MattFlower wrote:

>This is a nice, simple implementation, but I think there is at least
>one error in the implementation of default values.

Thanks for the comments (to sebastian.fiorentini too), this code has
been very useful to me and I was already thinking that it was to me
alone. About the error comment:

>As is, nullSafeGet won't use the default value because of the check on
>rs.wasNull(). If we intend to use the default value there, I believe
>that (name == null) would be a better check..

I disagree, please forgive me if I did not understand your point. The
default value is returned when the field is NULL, so that if the
value is set there is no excuse to not finding the name (actually
throwing an exception).

>Another decent question would be whether nullSafeSet should be
>explicitly setting the default value or the null value.  If it should
>be setting the default value, there is no handling of such.

I can justify not to set the default value when it is
NULL, but these are ultimately thin and better advice would be to do
what your business needs:
- fewer changes to the database
- not messing with values that can have other meanings

In particular this last one means that the one application will be well
behaved considering other ones and considering evolutions. If someone
update the field meaning, allowing other values, you will fail and will
NOT change the database.


Finally, to me "defaultValue" if more a help to applications, so they do
not have to deal with null. Simply, it makes creating screens (html
pages) easier.

Chears to all.
 
the problem I see with this approach 29 Nov 2006, 10:32 vanyatka
Thanks for your solution!

The problem I see here, though, is that your business logic changes in a
way that all your enums now have to implement some 3rd party interface
and thus become tied to it. One of the benefits of Hibernate is adding
persistency support without ANY modifications to your business objects.
 
© Copyright 2006, Red Hat Middleware, LLC. All rights reserved. JBoss and Hibernate are registered trademarks and servicemarks of Red Hat, Inc. [Privacy Policy]