UserType for persisting Typesafe Enumerations with a single classThe replacement solution for the now deprecated PersistentEnum given in UserType for persisting a Typesafe Enumeration with a VARCHAR column required that every enum implementation had a companion UserType implementation. Properties of the enum type had to be typed with the UserType implementation. This was bad news for users of the hbm2java POJO generator since setters/getters of such properties required the UserType implementation.
Single class solutionThe solution presented here only requires one class per enum -- the enum itself. In adittion, the persisted representation of the enum can be either a char, an integer, a string, or the enum string name itself (more types can easily be added). There is one difference between real typesafe enums and the persistent type safe enums presented here -- they all require a no-arg constructor implementation (preferrably no-op). This is so that Hibernate will be able to create an instance while restoring objects from the database (the enum will still be resolved to an already created static enum instance in the nullSafeGet method). This is analogous to the requirement for Serialzable classes to have a public/protected no-arg constructor.
How to use it?To create an enum persisted as char, simply provide an enum implementation that extend PersistentCharacterEnum.
package com.foo;
public final class Gender extends PersistentCharacterEnum {
public static final Gender MALE = new Gender("male", 'M');
public static final Gender FEMALE = new Gender("female", 'F');
public static final Gender UNDETERMINED = new Gender("undetermined", 'U');
public Gender() {}
private Gender(String name, char persistentValue) {
super(name, persistentValue);
}
}
Now this enumeration can be used on properties in your mapping file as:
<property name="gender" type="com.foo.Gender">
Base class implementationAll that is required to use this enum scheme is to import the following four classes into your project. Enjoy!
- Base class for enums persisted as characters:
package com.foo;
import net.sf.hibernate.Hibernate;
import net.sf.hibernate.type.NullableType;
/**
* Provides a base class for persistable, type-safe, comparable,
* and serializable enums persisted as characters.
*
* <p>Create a subclass of this class implementing the enumeration:
* <pre>package com.foo;
*
* public final class Gender extends PersistentCharacterEnum {
* public static final Gender MALE = new Gender("male", 'M');
* public static final Gender FEMALE = new Gender("female", 'F');
* public static final Gender UNDETERMINED = new Gender("undetermined", 'U');
*
* public Gender() {}
*
* private Gender(String name, char persistentValue) {
* super(name, persistentValue);
* }
* }
* </pre>
* Note that a no-op default constructor must be provided.</p>
*
* <p>Use this enumeration in your mapping file as:
* <pre><property name="gender" type="com.foo.Gender"></pre></p>
*
* <p><code>
* $Id: PersistentCharacterEnum.java,v 1.1 2004/05/31 06:52:28 austvold Exp $
* </pre></p>
*
* @version $Revision: 1.1 $
* @author Ørjan Nygaard Austvold
*/
public abstract class PersistentCharacterEnum extends PersistentEnum {
/**
* Default constructor. Hibernate need the default constructor
* to retrieve an instance of the enum from a JDBC resultset.
* The instance will be converted to the correct enum instance
* in {@link #nullSafeGet(java.sql.ResultSet, java.lang.String[], java.lang.Object)}.
*/
protected PersistentCharacterEnum() {
// no-op -- instance will be tossed away once the equivalent enum is found.
}
/**
* Constructs an enum with the given name and persistent representation.
*
* @param name name of enum.
* @param persistentCharacter persistent representation of the enum.
*/
protected PersistentCharacterEnum(String name, char persistentCharacter) {
super(name, new Character(persistentCharacter));
}
/**
* @see java.lang.Comparable#compareTo(java.lang.Object)
*/
public int compareTo(Object other) {
if (other == this) {
return 0;
}
return ((Character) getEnumCode()).compareTo(((PersistentEnum) other).getEnumCode());
}
/**
* @see PersistentEnum#getNullableType()
*/
protected NullableType getNullableType() {
return Hibernate.CHARACTER;
}
}
- Base class for enums persisted as integers:
package com.foo;
import net.sf.hibernate.Hibernate;
import net.sf.hibernate.type.NullableType;
/**
* Provides a base class for persistable, type-safe, comparable,
* and serializable enums persisted as integers.
*
* <p>Create a subclass of this class implementing the enumeration:
* <pre>package com.foo;
*
* public final class Gender extends PersistentCharacterEnum {
* public static final Gender MALE = new Gender("male", 0);
* public static final Gender FEMALE = new Gender("female", 1);
* public static final Gender UNDETERMINED = new Gender("undetermined", 2);
*
* public Gender() {}
*
* private Gender(String name, int persistentValue) {
* super(name, persistentValue);
* }
* }
* </pre>
* Note that a no-op default constructor must be provided.</p>
*
* <p>Use this enumeration in your mapping file as:
* <pre><property name="gender" type="com.foo.Gender"></pre></p>
*
* <p><code>
* $Id: PersistentIntegerEnum.java,v 1.1 2004/05/31 06:52:28 austvold Exp $
* </pre></p>
*
* @version $Revision: 1.1 $
* @author Ørjan Nygaard Austvold
*/
public abstract class PersistentIntegerEnum extends PersistentEnum {
/**
* Default constructor. Hibernate need the default constructor
* to retrieve an instance of the enum from a JDBC resultset.
* The instance will be converted to the correct enum instance
* in {@link #nullSafeGet(java.sql.ResultSet, java.lang.String[], java.lang.Object)}.
*/
protected PersistentIntegerEnum() {
// no-op -- instance will be tossed away once the equivalent enum is found.
}
/**
* Constructs an enum with the given name and persistent representation.
*
* @param name name of enum.
* @param persistentInteger persistent representation of the enum.
*/
protected PersistentIntegerEnum(String name, int persistentInteger) {
super(name, new Integer(persistentInteger));
}
/**
* @see java.lang.Comparable#compareTo(java.lang.Object)
*/
public int compareTo(Object other) {
if (other == this) {
return 0;
}
return ((Integer) getEnumCode()).compareTo(((PersistentEnum) other).getEnumCode());
}
/**
* @see PersistentEnum#getNullableType()
*/
protected NullableType getNullableType() {
return Hibernate.INTEGER;
}
}
- Base class for enums persisted as strings:
package com.foo;
import net.sf.hibernate.Hibernate;
import net.sf.hibernate.type.NullableType;
/**
* Provides a base class for persistable, type-safe, comparable,
* and serializable enums persisted as strings.
*
* <p>Create a subclass of this class implementing the enumeration:
* <pre>package com.foo;
*
* public final class Gender extends PersistentCharacterEnum {
* public static final Gender MALE = new Gender("male");
* public static final Gender FEMALE = new Gender("female");
* public static final Gender UNDETERMINED = new Gender("undetermined");
*
* public Gender() {}
*
* private Gender(String name) {
* super(name);
* }
* }
* </pre>
* Note that a no-op default constructor must be provided.</p>
*
* <p>Use this enumeration in your mapping file as:
* <pre><property name="gender" type="com.foo.Gender"></pre></p>
*
* <p><code>
* $Id: PersistentStringEnum.java,v 1.1 2004/05/31 06:52:28 austvold Exp $
* </pre></p>
*
* @version $Revision: 1.1 $
* @author Ørjan Nygaard Austvold
*/
public abstract class PersistentStringEnum extends PersistentEnum {
/**
* Default constructor. Hibernate need the default constructor
* to retrieve an instance of the enum from a JDBC resultset.
* The instance will be resolved to the correct enum instance
* in {@link #nullSafeGet(java.sql.ResultSet, java.lang.String[], java.lang.Object)}.
*/
protected PersistentStringEnum() {
// no-op -- instance will be tossed away once the equivalent enum is found.
}
/**
* Constructs an enum with name as the persistent representation.
*
* @param name name of the enum.
*/
protected PersistentStringEnum(String name) {
super(name, name);
}
/**
* Constructs an enum with the given name and persistent representation.
*
* @param name name of enum.
* @param persistentString persistent representation of the enum.
*/
protected PersistentStringEnum(String name, String persistentString) {
super(name, persistentString);
}
/**
* @see java.lang.Comparable#compareTo(java.lang.Object)
*/
public int compareTo(Object other) {
if (other == this) {
return 0;
}
return ((String) getEnumCode()).compareTo(((PersistentEnum) other).getEnumCode());
}
/**
* @see PersistentEnum#getNullableType()
*/
protected NullableType getNullableType() {
return Hibernate.STRING;
}
}
- Base class for all of the above enum base class implementations:
package com.foo;
import net.sf.hibernate.HibernateException;
import net.sf.hibernate.UserType;
import net.sf.hibernate.type.NullableType;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
/**
* Provides a base class for implementations of persistable, type-safe,
* comparable and serializable enums with a custom persisted representation.
*
* <p>The subclass must provide a compareTo(Object) and getNullableType()
* implementation.</p>
*
* <p><code>
* $Id: PersistentEnum.java,v 1.1 2004/05/31 06:52:28 austvold Exp $
* </pre></p>
*
* @version $Revision: 1.1 $
* @author Ørjan Nygaard Austvold
*/
abstract class PersistentEnum implements Comparable, Serializable, UserType {
/**
* <code>Map</code> where key is of class name, value is of <code>Map</code>.
* where key is of enumCode and value is of enum instance.
*/
private static final Map enumClasses = new HashMap();
/**
* The identifying enum code.
*/
protected Serializable enumCode;
/**
* The name of the enumeration. Used as toString result.
*/
protected String name;
/**
* The hashcode representation of the enum.
*/
protected transient int hashCode;
/**
* Default constructor. Hibernate need the default constructor
* to retrieve an instance of the enum from a JDBC resultset.
* The instance will be converted to the correct enum instance
* in {@link #nullSafeGet(java.sql.ResultSet, java.lang.String[], java.lang.Object)}.
*/
protected PersistentEnum() {
// no-op -- instance will be tossed away once the equivalent enum is found.
}
/**
* Constructs a new enumeration instance with the given name and persisted
* representation of enumCode.
*
* @param name name of the enum instance.
* @param enumCode persisted enum representation.
*/
protected PersistentEnum(String name, Serializable enumCode) {
this.name = name;
this.enumCode = enumCode;
hashCode = 7 + returnedClass().hashCode() + 3 * enumCode.hashCode();
Class enumClass = returnedClass();
Map entries = (Map) enumClasses.get(enumClass);
if (entries == null) {
entries = new HashMap();
enumClasses.put(enumClass, entries);
}
if (entries.containsKey(enumCode)) {
throw new IllegalArgumentException("The enum code must be unique, '"
+ enumCode + "' has already been added");
}
entries.put(enumCode, this);
}
/**
* @see java.lang.Object#hashCode()
*/
public final int hashCode() {
return hashCode;
}
/**
* @see java.lang.Object#equals(java.lang.Object)
*/
public final boolean equals(Object other) {
if (other == this) {
return true;
} else if (other == null) {
return false;
} else if (((PersistentEnum) other).returnedClass().getName().equals(returnedClass().getName())) {
// different classloaders
try {
// try to avoid reflection
return enumCode.equals(((PersistentEnum) other).enumCode);
} catch (ClassCastException ex) {
// use reflection
try {
Method mth = other.getClass().getMethod("getEnumCode", null);
Serializable enumCode = (Serializable) mth.invoke(other, null);
return this.enumCode.equals(enumCode);
} catch (Exception ignore) { // NoSuchMethod-, IllegalAccess-, InvocationTargetException
}
return false;
}
} else {
return false;
}
}
/**
* Gets the persistable enum code of this enum.
*
* @return the enum code.
*/
public final Serializable getEnumCode() {
return enumCode;
}
/**
* Resolves this enumeration into a already staticly instantiated enum.
*
* @return the type-safe enum equivalent to this enumeration.
*/
protected Object readResolve() {
Map entries = (Map) enumClasses.get(returnedClass());
return (entries != null) ? entries.get(enumCode) : null;
}
/**
* Gets the collection of enumeration instances of a given
* enumeration class.
*
* @param enumClass enumeration class type.
* @return collection of enumerations of the given class.
*/
protected static Collection getEnumCollection(Class enumClass) {
Map entries = (Map) enumClasses.get(enumClass);
return (entries != null)
? Collections.unmodifiableCollection(entries.values())
: Collections.EMPTY_LIST;
}
/**
* @see Comparable#compareTo(Object)
*/
public abstract int compareTo(Object other);
/**
* Gets the Hibernate type of the persisted representation of the enum.
*
* @return the Nullable Hibernate type.
*/
protected abstract NullableType getNullableType();
/**
* @see net.sf.hibernate.UserType#sqlTypes()
*/
public int[] sqlTypes() {
return new int[]{getNullableType().sqlType()};
}
/**
* Simply return the enums name.
*
* @return the string representation of this enum.
*/
public String toString() {
return name;
}
/**
* @see net.sf.hibernate.UserType#deepCopy(java.lang.Object)
*/
public Object deepCopy(Object value) throws HibernateException {
// Enums are immutable - nothing to be done to deeply clone it
return value;
}
/**
* @see net.sf.hibernate.UserType#isMutable()
*/
public boolean isMutable() {
// Enums are immutable
return false;
}
/**
* @see net.sf.hibernate.UserType#returnedClass()
* @return
*/
public Class returnedClass() {
return this.getClass();
}
/**
* @see net.sf.hibernate.UserType#equals(java.lang.Object, java.lang.Object)
*/
public boolean equals(Object x, Object y) throws HibernateException {
if (x == y) {
return true;
} else if (x == null || y == null) {
return false;
} else {
return getNullableType().equals(x, y);
}
}
/**
* @see net.sf.hibernate.UserType#nullSafeGet(java.sql.ResultSet, java.lang.String[], java.lang.Object)
*/
public Object nullSafeGet(ResultSet rs, String[] names, Object owner) throws HibernateException, SQLException {
Serializable enumCode = (Serializable) getNullableType().nullSafeGet(rs, names[0]);
Map entries = (Map) enumClasses.get(returnedClass());
return (PersistentEnum) ((entries != null)
? entries.get(enumCode)
: null);
}
/**
* @see net.sf.hibernate.UserType#nullSafeSet(java.sql.PreparedStatement, java.lang.Object, int)
*/
public void nullSafeSet(PreparedStatement st, Object value, int index) throws HibernateException, SQLException {
// make sure the received value is of the right type
if ((value != null) && !returnedClass().isAssignableFrom(value.getClass())) {
throw new IllegalArgumentException("Received value is not a [" +
returnedClass().getName() + "] but [" + value.getClass() + "]");
}
// convert the enum into its persistence format
Serializable enumCode = ((PersistentEnum) value).getEnumCode();
// set the value into the resultset
st.setObject(index, enumCode, getNullableType().sqlType());
}
}
|