From 08fb5c6a6f8711ccf140636da85519b799cfc8b3 Mon Sep 17 00:00:00 2001 From: Matt Feury Date: Fri, 23 Aug 2013 18:10:05 -0400 Subject: [PATCH 01/17] Add IdentityEnumArrayType.java This behaves like the IntegerArrayType except that it serializes to/fro Enum values on save/load. See class comments for more info --- .../PostgresqlExtensionsDialect.java | 2 + .../hibernate/usertype/AbstractArrayType.java | 1 + .../usertype/IdentityEnumArrayType.java | 153 ++++++++++++++++++ 3 files changed, 156 insertions(+) create mode 100644 src/java/net/kaleidos/hibernate/usertype/IdentityEnumArrayType.java diff --git a/src/java/net/kaleidos/hibernate/PostgresqlExtensionsDialect.java b/src/java/net/kaleidos/hibernate/PostgresqlExtensionsDialect.java index f06af49..483bc46 100644 --- a/src/java/net/kaleidos/hibernate/PostgresqlExtensionsDialect.java +++ b/src/java/net/kaleidos/hibernate/PostgresqlExtensionsDialect.java @@ -3,6 +3,7 @@ import net.kaleidos.hibernate.usertype.IntegerArrayType; import net.kaleidos.hibernate.usertype.LongArrayType; import net.kaleidos.hibernate.usertype.StringArrayType; +import net.kaleidos.hibernate.usertype.IdentityEnumArrayType; import org.hibernate.dialect.Dialect; import org.hibernate.dialect.PostgreSQLDialect; @@ -23,6 +24,7 @@ public PostgresqlExtensionsDialect() { registerColumnType(Types.ARRAY, "array"); registerColumnType(LongArrayType.SQLTYPE, "int8[]"); registerColumnType(IntegerArrayType.SQLTYPE, "int[]"); + registerColumnType(IdentityEnumArrayType.SQLTYPE, "int[]"); registerColumnType(StringArrayType.SQLTYPE, "varchar[]"); } diff --git a/src/java/net/kaleidos/hibernate/usertype/AbstractArrayType.java b/src/java/net/kaleidos/hibernate/usertype/AbstractArrayType.java index 9ef44a3..a6a7dea 100644 --- a/src/java/net/kaleidos/hibernate/usertype/AbstractArrayType.java +++ b/src/java/net/kaleidos/hibernate/usertype/AbstractArrayType.java @@ -9,6 +9,7 @@ public abstract class AbstractArrayType implements UserType { protected static final int INTEGER_ARRAY = 90001; protected static final int LONG_ARRAY = 90002; protected static final int STRING_ARRAY = 90003; + protected static final int ENUM_INTEGER_ARRAY = 90004; @Override public Object assemble(Serializable cached, Object owner) throws HibernateException { diff --git a/src/java/net/kaleidos/hibernate/usertype/IdentityEnumArrayType.java b/src/java/net/kaleidos/hibernate/usertype/IdentityEnumArrayType.java new file mode 100644 index 0000000..86f51eb --- /dev/null +++ b/src/java/net/kaleidos/hibernate/usertype/IdentityEnumArrayType.java @@ -0,0 +1,153 @@ +package net.kaleidos.hibernate.usertype; + +import java.sql.Array; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Types; + +import org.hibernate.HibernateException; + +import grails.util.GrailsWebUtil; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.hibernate.MappingException; +import org.hibernate.usertype.ParameterizedType; + +import java.io.Serializable; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.*; + +/** +* This class behaves almost identical to IntegerArrayType in that it +* stores and retrieves an array of ints. The difference, however, is that +* this is used with an Array of Enums, rather than Ints. The Enums are serialized +* to their ordinal value before persisted to the database. On retrieval, +* they are then converted back into their original Enum type. +* +* A lot of this code was pieced together using Grails' Hibernate's IdentityEnumType, as seen here: +* http://grepcode.com/file_/repo1.maven.org/maven2/org.grails/grails-hibernate/2.1.1/org/codehaus/groovy/grails/orm/hibernate/cfg/IdentityEnumType.java/ +* +* The BidiEnumMap was lifted 100% from that class. Although it is static, it is declared as Private +* so we are unable to access it without copying the code :(. +*/ +public class IdentityEnumArrayType extends IntegerArrayType implements ParameterizedType { + public static int SQLTYPE = AbstractArrayType.ENUM_INTEGER_ARRAY; + private static final Log LOG = LogFactory.getLog(IdentityEnumArrayType.class); + + private Class> enumClass; + public static final String ENUM_ID_ACCESSOR = "getId"; + public static final String PARAM_ENUM_CLASS = "enumClass"; + + @Override + public Class returnedClass() { + // This may not be 100%. + // Is there anyway to return the actual enumClass as an array and not cause issues? + return Enum[].class; + } + + @Override + public int[] sqlTypes() { + return new int[] { SQLTYPE }; + } + + public Object idToEnum(Object id) throws HibernateException, SQLException { + try { + BidiEnumMap bidiMap = new BidiEnumMap(enumClass); + return bidiMap.getEnumValue(id); + } catch (Exception e) { + throw new HibernateException("Uh oh. Unable to create bidirectional enum map for " + enumClass + " with nested exception: " + e.toString()); + } + } + + @Override + @SuppressWarnings({"rawtypes", "unchecked"}) + public Object nullSafeGet(ResultSet rs, String[] names, Object owner) throws HibernateException, SQLException { + Object result = super.nullSafeGet(rs, names, owner); + + if (result == null) { + return result; + } else { + Integer[] results = (Integer[]) result; + Object converted = java.lang.reflect.Array.newInstance(enumClass, results.length); + + for (int i = 0; i < results.length; i++) { + java.lang.reflect.Array.set(converted, i, idToEnum(results[i])); + } + + return converted; + } + } + + @Override + public void nullSafeSet(PreparedStatement st, Object value, int index) throws HibernateException, SQLException { + Object converted = value; + + if (value != null) { + Object[] o = (Object[])value; + for (int i = 0; i < o.length; i++) { + o[i] = ((Enum)o[i]).ordinal(); + } + converted = o; + } + + super.nullSafeSet(st, converted, index); + } + + public void setParameterValues(Properties properties) { + try { + enumClass = (Class>)properties.get(PARAM_ENUM_CLASS); + } catch (Exception e) { + throw new MappingException("Error mapping Enum Class using IdentityEnumArrayType", e); + } + } + + // This code was lifted from IdentityEnumType.java (see class comments for more info) + @SuppressWarnings({"rawtypes", "unchecked"}) + public static class BidiEnumMap implements Serializable { + + private static final long serialVersionUID = 3325751131102095834L; + private final Map enumToKey; + private final Map keytoEnum; + private Class keyType; + + private BidiEnumMap(Class enumClass) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException { + if (LOG.isDebugEnabled()) { + LOG.debug(String.format("Building Bidirectional Enum Map...")); + } + + @SuppressWarnings("hiding") + EnumMap enumToKey = new EnumMap(enumClass); + @SuppressWarnings("hiding") + HashMap keytoEnum = new HashMap(); + + Method idAccessor = enumClass.getMethod(ENUM_ID_ACCESSOR); + + keyType = idAccessor.getReturnType(); + + Method valuesAccessor = enumClass.getMethod("values"); + Object[] values = (Object[]) valuesAccessor.invoke(enumClass); + + for (Object value : values) { + Object id = idAccessor.invoke(value); + enumToKey.put((Enum) value, id); + if (keytoEnum.containsKey(id)) { + LOG.warn(String.format("Duplicate Enum ID '%s' detected for Enum %s!", id, enumClass.getName())); + } + keytoEnum.put(id, value); + } + + this.enumToKey = Collections.unmodifiableMap(enumToKey); + this.keytoEnum = Collections.unmodifiableMap(keytoEnum); + } + + public Object getEnumValue(Object id) { + return keytoEnum.get(id); + } + + public Object getKey(Object enumValue) { + return enumToKey.get(enumValue); + } + } +} From 21f0006f533c800fc58b6a51f57670a493e9be0e Mon Sep 17 00:00:00 2001 From: Matt Feury Date: Fri, 23 Aug 2013 19:52:19 -0400 Subject: [PATCH 02/17] Properly convert IdentityEnumArrayType values for CriteriaQueries --- .../criterion/array/PgArrayExpression.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/java/net/kaleidos/hibernate/criterion/array/PgArrayExpression.java b/src/java/net/kaleidos/hibernate/criterion/array/PgArrayExpression.java index aada951..61b9303 100644 --- a/src/java/net/kaleidos/hibernate/criterion/array/PgArrayExpression.java +++ b/src/java/net/kaleidos/hibernate/criterion/array/PgArrayExpression.java @@ -8,6 +8,9 @@ import org.hibernate.type.Type; import org.hibernate.util.StringHelper; +import java.lang.reflect.Array; +import java.util.List; + /** * Constrains a property in an array */ @@ -42,6 +45,19 @@ public TypedValue[] getTypedValues(Criteria criteria, CriteriaQuery criteriaQuer Object[] arrValue; if ("net.kaleidos.hibernate.usertype.IntegerArrayType".equals(propertyTypeName)) { arrValue = pgCriteriaUtils.getValueAsArrayOfType(value, Integer.class); + } else if ("net.kaleidos.hibernate.usertype.IdentityEnumArrayType".equals(propertyTypeName)) { + if (value instanceof List) { + List valueAsList = (List)value; + arrValue = (Object[]) Array.newInstance(Integer.class, valueAsList.size()); + + for(int i=0; i < valueAsList.size(); i++) { + arrValue[i] = ((Enum)(valueAsList.get(i))).ordinal(); + } + } else { + Object converted = ((Enum)value).ordinal(); + arrValue = pgCriteriaUtils.getValueAsArrayOfType(converted, Integer.class); + } + } else if ("net.kaleidos.hibernate.usertype.LongArrayType".equals(propertyTypeName)) { arrValue = pgCriteriaUtils.getValueAsArrayOfType(value, Long.class); } else if ("net.kaleidos.hibernate.usertype.StringArrayType".equals(propertyTypeName)) { From 59ab76cea348cb19b19395758d7c63026a785a43 Mon Sep 17 00:00:00 2001 From: Matt Feury Date: Fri, 23 Aug 2013 19:52:54 -0400 Subject: [PATCH 03/17] Don't attempt to convert Enums to Integers twice Seems like this function gets called when we're using a CriteriaQuery. However, in that case, we've already done the converting to Integer prior to that query, so these should already be in ordinal form --- .../kaleidos/hibernate/usertype/IdentityEnumArrayType.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/java/net/kaleidos/hibernate/usertype/IdentityEnumArrayType.java b/src/java/net/kaleidos/hibernate/usertype/IdentityEnumArrayType.java index 86f51eb..0889c75 100644 --- a/src/java/net/kaleidos/hibernate/usertype/IdentityEnumArrayType.java +++ b/src/java/net/kaleidos/hibernate/usertype/IdentityEnumArrayType.java @@ -4,7 +4,6 @@ import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; -import java.sql.Types; import org.hibernate.HibernateException; @@ -87,7 +86,9 @@ public void nullSafeSet(PreparedStatement st, Object value, int index) throws Hi if (value != null) { Object[] o = (Object[])value; for (int i = 0; i < o.length; i++) { - o[i] = ((Enum)o[i]).ordinal(); + if (! (o[i] instanceof Integer)) { + o[i] = ((Enum)o[i]).ordinal(); + } } converted = o; } From 8bf2573f6905e929bb7aecf73d3bd606ce00990f Mon Sep 17 00:00:00 2001 From: Matt Feury Date: Fri, 23 Aug 2013 20:11:01 -0400 Subject: [PATCH 04/17] Add readme and instructions for enum array type --- README.md | 41 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 94de580..23dace4 100644 --- a/README.md +++ b/README.md @@ -51,22 +51,44 @@ If you just only add the dialect, hibernate will create a new sequence for every ### Arrays -The plugin supports the definition of `Integer`, `Long` and `String` arrays in your domain classes. +The plugin supports the definition of `Integer`, `Long`, `String`, and `Enum` arrays in your domain classes. + +The EnumArrayType behaves almost identical to IntegerArrayType in that it stores and retrieves an array of ints. The difference, however, is that this is used with an Array of Enums, rather than Ints. The Enums are serialized to their ordinal value before persisted to the database. On retrieval, they are then converted back into their original Enum type. ```groovy import net.kaleidos.hibernate.usertype.IntegerArrayType import net.kaleidos.hibernate.usertype.LongArrayType import net.kaleidos.hibernate.usertype.StringArrayType +import net.kaleidos.hibernate.usertype.IdentityEnumArrayType class Like { Integer[] favoriteNumbers = [] Long[] favoriteLongNumbers = [] String[] favoriteMovies = [] + Juice[] favoriteJuices = [] + + static enum Juice { + ORANGE(0), + APPLE(1), + GRAPE(2) + + private final int value + Juice(int value) { this.value = value } + public int value() { return this.value } + public int getId() { return this.value } + + public String getName() { this.toString() } + + // Enum Constructors are not publicly visible + // so we have to loop through the values and find the desired one + public static withId(int value) { Juice.values().find { it.value() == value } } + } static mapping = { favoriteNumbers type:IntegerArrayType favoriteLongNumbers type:LongArrayType favoriteMovies type:StringArrayType + favoriteJuices type:IdentityEnumArrayType, params:[enumClass: Juice] } } ``` @@ -76,7 +98,8 @@ Now you can create domain objects using lists of integers, longs and strings and ```groovy def like1 = new Like(favoriteNumbers:[5, 17, 9, 6], favoriteLongNumbers:[123, 239, 3498239, 2344235], - favoriteMovies:["Spiderman", "Blade Runner", "Starwars"]) + favoriteMovies:["Spiderman", "Blade Runner", "Starwars"], + favoriteJuices:[Like.Juice.ORANGE, Like.Juice.GRAPE]) like1.save() ``` @@ -85,9 +108,9 @@ And now, with `psql`: ``` =# select * from like; - id | favorite_long_numbers | favorite_movies | favorite_numbers -----+-------------------------- +----------------------------------------+------------------ - 1 | {123,239,3498239,2344235} | {Spiderman,"Blade Runner",Starwars} | {5,17,9,6} + id | favorite_long_numbers | favorite_movies | favorite_numbers | favorite_juices +----+-------------------------- +----------------------------------------+------------------+---------------- + 1 | {123,239,3498239,2344235} | {Spiderman,"Blade Runner",Starwars} | {5,17,9,6} | {0,2} ``` #### Criterias @@ -111,6 +134,13 @@ def numbers = [5, 17] def result = Like.withCriteria { pgArrayContains 'favoriteNumbers', numbers } + +// If using enums, pass the enum right through +def juices = Like.Juice.ORANGE +def result = Like.withCriteria { + pgArrayContains 'favoriteJuices', juices +} + ``` #### Is contained @@ -172,6 +202,7 @@ You can send any questions to: - Iván López: lopez.ivan@gmail.com - Alonso Torres: alonso.javier.torres@gmail.com +- Matt Feury: mattfeury@gmail.com Collaborations are appreciated :-) From d01c06a63bba59bc35c4dc78631981923180ad7b Mon Sep 17 00:00:00 2001 From: Matt Feury Date: Sat, 24 Aug 2013 14:51:00 -0400 Subject: [PATCH 05/17] Allow a map function to be passed through when getting ValueAsArrayOfType This function will transform either the Object or the List of Objects into some other value --- .../criterion/array/PgArrayExpression.java | 19 ++++------ .../criterion/array/PgCriteriaUtils.java | 37 ++++++++++++++++--- 2 files changed, 40 insertions(+), 16 deletions(-) diff --git a/src/java/net/kaleidos/hibernate/criterion/array/PgArrayExpression.java b/src/java/net/kaleidos/hibernate/criterion/array/PgArrayExpression.java index 61b9303..4f05d73 100644 --- a/src/java/net/kaleidos/hibernate/criterion/array/PgArrayExpression.java +++ b/src/java/net/kaleidos/hibernate/criterion/array/PgArrayExpression.java @@ -46,18 +46,15 @@ public TypedValue[] getTypedValues(Criteria criteria, CriteriaQuery criteriaQuer if ("net.kaleidos.hibernate.usertype.IntegerArrayType".equals(propertyTypeName)) { arrValue = pgCriteriaUtils.getValueAsArrayOfType(value, Integer.class); } else if ("net.kaleidos.hibernate.usertype.IdentityEnumArrayType".equals(propertyTypeName)) { - if (value instanceof List) { - List valueAsList = (List)value; - arrValue = (Object[]) Array.newInstance(Integer.class, valueAsList.size()); - - for(int i=0; i < valueAsList.size(); i++) { - arrValue[i] = ((Enum)(valueAsList.get(i))).ordinal(); + arrValue = pgCriteriaUtils.getValueAsArrayOfType( + value, + Integer.class, + new PgCriteriaUtils.MapFunction() { + public Object map(Object o) { + return ((Enum)o).ordinal(); + } } - } else { - Object converted = ((Enum)value).ordinal(); - arrValue = pgCriteriaUtils.getValueAsArrayOfType(converted, Integer.class); - } - + ); } else if ("net.kaleidos.hibernate.usertype.LongArrayType".equals(propertyTypeName)) { arrValue = pgCriteriaUtils.getValueAsArrayOfType(value, Long.class); } else if ("net.kaleidos.hibernate.usertype.StringArrayType".equals(propertyTypeName)) { diff --git a/src/java/net/kaleidos/hibernate/criterion/array/PgCriteriaUtils.java b/src/java/net/kaleidos/hibernate/criterion/array/PgCriteriaUtils.java index 727a8c1..4b3b0d0 100644 --- a/src/java/net/kaleidos/hibernate/criterion/array/PgCriteriaUtils.java +++ b/src/java/net/kaleidos/hibernate/criterion/array/PgCriteriaUtils.java @@ -14,15 +14,13 @@ class PgCriteriaUtils { * will be the type passed as parameter * @param targetValue The value we want to wrap as an array * @param expectedType The expected type of the returned array + * @param mapFunction If non-null, it will transform each object in the array to a given object. * @return */ @SuppressWarnings("unchecked") - public Object[] getValueAsArrayOfType(Object targetValue, Class expectedType) { + public Object[] getValueAsArrayOfType(Object targetValue, Class expectedType, MapFunction mapFunction) { Object[] arrValue; - if (expectedType.isInstance(targetValue)) { - arrValue = (Object[]) Array.newInstance(expectedType, 1); - arrValue[0] = expectedType.cast(targetValue); - } else if (targetValue instanceof List) { + if (targetValue instanceof List) { List valueAsList = (List)targetValue; arrValue = (Object[]) Array.newInstance(expectedType, valueAsList.size()); @@ -30,15 +28,44 @@ public Object[] getValueAsArrayOfType(Object targetValue, Class expectedType) for(int i=0; i instead"); } } + } else if (expectedType.isInstance(targetValue) || mapFunction != null) { + arrValue = (Object[]) Array.newInstance(expectedType, 1); + + if (mapFunction != null) { + arrValue[0] = expectedType.cast(mapFunction.map(targetValue)); + } else { + arrValue[0] = expectedType.cast(targetValue); + } } else { throw new HibernateException("criteria doesn't support values of type: " + targetValue.getClass().getName() + ". Try: " + expectedType + " or List<" + expectedType + "> instead"); } return arrValue; } + + /** + * Overloaded version of getValueAsArrayOfType that doesn't use a mapFunction + */ + public Object[] getValueAsArrayOfType(Object targetValue, Class expectedType) { + return getValueAsArrayOfType(targetValue, expectedType, null); + } + + /** + * Simple class for passing a closure that takes an Object and transforms it into a new value. + */ + public static abstract class MapFunction { + /** + * Transforms an object into some new value. + * @param o the object we want to transform + * @return some transformed version of the object + */ + public abstract Object map(Object o); + } } From 444b9be76a80a8aec828f505c1ca1c1e8218fda5 Mon Sep 17 00:00:00 2001 From: Matt Feury Date: Sat, 24 Aug 2013 16:06:19 -0400 Subject: [PATCH 06/17] Prevent ArrayStoreExceptions when converting Enums to Int array We have to create another array so we guarantee there are no mixed types. --- .../kaleidos/hibernate/usertype/IdentityEnumArrayType.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/java/net/kaleidos/hibernate/usertype/IdentityEnumArrayType.java b/src/java/net/kaleidos/hibernate/usertype/IdentityEnumArrayType.java index 0889c75..493a011 100644 --- a/src/java/net/kaleidos/hibernate/usertype/IdentityEnumArrayType.java +++ b/src/java/net/kaleidos/hibernate/usertype/IdentityEnumArrayType.java @@ -85,12 +85,13 @@ public void nullSafeSet(PreparedStatement st, Object value, int index) throws Hi if (value != null) { Object[] o = (Object[])value; + Integer[] result = new Integer[o.length]; for (int i = 0; i < o.length; i++) { if (! (o[i] instanceof Integer)) { - o[i] = ((Enum)o[i]).ordinal(); + result[i] = ((Enum)o[i]).ordinal(); } } - converted = o; + converted = result; } super.nullSafeSet(st, converted, index); From 59522e2a3791d19b11b96076ba9fbd91ef036925 Mon Sep 17 00:00:00 2001 From: Matt Feury Date: Sat, 24 Aug 2013 17:48:25 -0400 Subject: [PATCH 07/17] Throw Hibernate exception if we can't cast to Enum --- .../hibernate/criterion/array/PgArrayExpression.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/java/net/kaleidos/hibernate/criterion/array/PgArrayExpression.java b/src/java/net/kaleidos/hibernate/criterion/array/PgArrayExpression.java index 4f05d73..94a9aa2 100644 --- a/src/java/net/kaleidos/hibernate/criterion/array/PgArrayExpression.java +++ b/src/java/net/kaleidos/hibernate/criterion/array/PgArrayExpression.java @@ -51,7 +51,11 @@ public TypedValue[] getTypedValues(Criteria criteria, CriteriaQuery criteriaQuer Integer.class, new PgCriteriaUtils.MapFunction() { public Object map(Object o) { - return ((Enum)o).ordinal(); + try { + return ((Enum)o).ordinal(); + } catch (ClassCastException e) { + throw new HibernateException("Unable to cast object " + o + " to Enum."); + } } } ); From ec9c2798dd182cff811b2628821f05efb287b4af Mon Sep 17 00:00:00 2001 From: Matt Feury Date: Sat, 24 Aug 2013 17:48:43 -0400 Subject: [PATCH 08/17] Properly pass already converted enums into nullSafeSet We switched to putting them in a new array, but we were only loading the new array when it wasn't already an integer. it'll already be an integer when it's used via the criteria operators --- .../net/kaleidos/hibernate/usertype/IdentityEnumArrayType.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/java/net/kaleidos/hibernate/usertype/IdentityEnumArrayType.java b/src/java/net/kaleidos/hibernate/usertype/IdentityEnumArrayType.java index 493a011..df2253c 100644 --- a/src/java/net/kaleidos/hibernate/usertype/IdentityEnumArrayType.java +++ b/src/java/net/kaleidos/hibernate/usertype/IdentityEnumArrayType.java @@ -89,6 +89,8 @@ public void nullSafeSet(PreparedStatement st, Object value, int index) throws Hi for (int i = 0; i < o.length; i++) { if (! (o[i] instanceof Integer)) { result[i] = ((Enum)o[i]).ordinal(); + } else { + result[i] = (Integer)o[i]; } } converted = result; From f899871e5b7421ef6b41f11855ff73a1118216fe Mon Sep 17 00:00:00 2001 From: Matt Feury Date: Sat, 24 Aug 2013 17:54:46 -0400 Subject: [PATCH 09/17] Add enum domain classes / fields for testing --- grails-app/domain/test/array/TestEnum.groovy | 25 +++++++++++++++++++ .../domain/test/criteria/array/Like.groovy | 20 ++++++++++++++- 2 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 grails-app/domain/test/array/TestEnum.groovy diff --git a/grails-app/domain/test/array/TestEnum.groovy b/grails-app/domain/test/array/TestEnum.groovy new file mode 100644 index 0000000..7541c07 --- /dev/null +++ b/grails-app/domain/test/array/TestEnum.groovy @@ -0,0 +1,25 @@ +package test.array + +import net.kaleidos.hibernate.usertype.IdentityEnumArrayType + +class TestEnum { + + static enum Day { + MONDAY(0), + TUESDAY(1), + WEDNESDAY(2), + THURSDAY(3), + FRIDAY(4), + SATURDAY(5), + SUNDAY(6) + + private final int value + Day(int value) { this.value = value } + } + + Day[] days + + static mapping = { + days type: IdentityEnumArrayType, params: [enumClass: Day] + } +} diff --git a/grails-app/domain/test/criteria/array/Like.groovy b/grails-app/domain/test/criteria/array/Like.groovy index 07cfa0c..eba5da5 100644 --- a/grails-app/domain/test/criteria/array/Like.groovy +++ b/grails-app/domain/test/criteria/array/Like.groovy @@ -1,5 +1,6 @@ package test.criteria.array +import net.kaleidos.hibernate.usertype.IdentityEnumArrayType import net.kaleidos.hibernate.usertype.IntegerArrayType import net.kaleidos.hibernate.usertype.LongArrayType import net.kaleidos.hibernate.usertype.StringArrayType @@ -11,6 +12,22 @@ class Like { Integer[] favoriteNumbers = [] Long[] favoriteLongNumbers = [] String[] favoriteMovies = [] + Juice[] favoriteJuices = [] + + static enum Juice { + ORANGE(0), + APPLE(1), + GRAPE(2), + PINEAPPLE(3), + TOMATO(4), + GRAPEFRUIT(5), + CRANBERRY(6), + CARROT(7), + LEMON(8) + + private final int value + Juice(int value) { this.value = value } + } static mapping = { table "pg_extensions_like" @@ -18,5 +35,6 @@ class Like { favoriteNumbers type:IntegerArrayType favoriteMovies type:StringArrayType favoriteLongNumbers type:LongArrayType + favoriteJuices type:IdentityEnumArrayType, params: [enumClass: Juice] } -} \ No newline at end of file +} From 5676f03f912020f70e28cece8b979a4ae0b179a3 Mon Sep 17 00:00:00 2001 From: Matt Feury Date: Sat, 24 Aug 2013 17:55:22 -0400 Subject: [PATCH 10/17] Add test for TestEnum creation --- ...stgresqlArraysDomainIntegrationSpec.groovy | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/test/integration/net/kaleidos/hibernate/array/PostgresqlArraysDomainIntegrationSpec.groovy b/test/integration/net/kaleidos/hibernate/array/PostgresqlArraysDomainIntegrationSpec.groovy index 3c3432d..dd6cef6 100644 --- a/test/integration/net/kaleidos/hibernate/array/PostgresqlArraysDomainIntegrationSpec.groovy +++ b/test/integration/net/kaleidos/hibernate/array/PostgresqlArraysDomainIntegrationSpec.groovy @@ -3,6 +3,7 @@ package net.kaleidos.hibernate.array import grails.plugin.spock.* import spock.lang.* +import test.array.TestEnum import test.array.TestInteger import test.array.TestLong import test.array.TestString @@ -57,4 +58,20 @@ class PostgresqlArraysDomainIntegrationSpec extends IntegrationSpec { strings << [ [], ["string 1"], ["string 1", "string 2"], ["string 1", "string 2", "string 3"] ] } -} \ No newline at end of file + @Unroll + void 'save a domain class with an enum array value'() { + setup: + def testEnum = new TestEnum(days: days) + + when: + testEnum.save() + + then: + testEnum.hasErrors() == false + testEnum.days.length == days.size() + + where: + days << [ [], [TestEnum.Day.MONDAY], [TestEnum.Day.SUNDAY, TestEnum.Day.SATURDAY], [TestEnum.Day.WEDNESDAY, TestEnum.Day.THURSDAY, TestEnum.Day.TUESDAY] ] + } + +} From 1c3cc88f5aa4a6415a660f3900adff15f8e80084 Mon Sep 17 00:00:00 2001 From: Matt Feury Date: Sat, 24 Aug 2013 17:57:05 -0400 Subject: [PATCH 11/17] Add pgArrayContains test for EnumArrays --- .../PgContainsCriteriaTestService.groovy | 23 ++++++++++ ...sCriteriaTestServiceIntegrationSpec.groovy | 46 ++++++++++++++++++- 2 files changed, 68 insertions(+), 1 deletion(-) diff --git a/grails-app/services/test/criteria/array/PgContainsCriteriaTestService.groovy b/grails-app/services/test/criteria/array/PgContainsCriteriaTestService.groovy index 9e9e711..eaa41b7 100644 --- a/grails-app/services/test/criteria/array/PgContainsCriteriaTestService.groovy +++ b/grails-app/services/test/criteria/array/PgContainsCriteriaTestService.groovy @@ -36,6 +36,18 @@ class PgContainsCriteriaTestService { return result } + /** + * Search "likes" with enum in array + */ + public List searchWithCriteriaEnumArray(Like.Juice juice) { + def result = Like.withCriteria { + pgArrayContains 'favoriteJuices', juice + } + + return result + } + + /** * Search "likes" with n integers in array */ @@ -69,6 +81,17 @@ class PgContainsCriteriaTestService { return result } + /** + * Search "likes" with n enums in array + */ + public List searchWithCriteriaEnumArray(List juice) { + def result = Like.withCriteria { + pgArrayContains 'favoriteJuices', juice + } + + return result + } + /** * Search with a join */ diff --git a/test/integration/net/kaleidos/hibernate/array/PgContainsCriteriaTestServiceIntegrationSpec.groovy b/test/integration/net/kaleidos/hibernate/array/PgContainsCriteriaTestServiceIntegrationSpec.groovy index 3d6dc92..32c10ae 100644 --- a/test/integration/net/kaleidos/hibernate/array/PgContainsCriteriaTestServiceIntegrationSpec.groovy +++ b/test/integration/net/kaleidos/hibernate/array/PgContainsCriteriaTestServiceIntegrationSpec.groovy @@ -91,6 +91,38 @@ class PgContainsCriteriaTestServiceIntegrationSpec extends IntegrationSpec { [] | 4 } + @Unroll + void 'search #juice in an array of enums'() { + setup: + new Like(favoriteJuices:[Like.Juice.ORANGE, Like.Juice.GRAPE]).save() + new Like(favoriteJuices:[Like.Juice.PINEAPPLE, Like.Juice.GRAPE, Like.Juice.CARROT, Like.Juice.CRANBERRY]).save() + new Like(favoriteJuices:[Like.Juice.APPLE, Like.Juice.TOMATO, Like.Juice.CARROT]).save() + new Like(favoriteJuices:[Like.Juice.ORANGE, Like.Juice.TOMATO, Like.Juice.CARROT]).save() + + when: + def result = pgContainsCriteriaTestService.searchWithCriteriaEnumArray(juice) + + then: + result.size() == resultSize + + where: + juice | resultSize + Like.Juice.CRANBERRY | 1 + Like.Juice.ORANGE | 2 + Like.Juice.LEMON | 0 + Like.Juice.APPLE | 1 + Like.Juice.GRAPE | 2 + Like.Juice.PINEAPPLE | 1 + Like.Juice.TOMATO | 2 + Like.Juice.CARROT | 3 + Like.Juice.GRAPEFRUIT | 0 + [Like.Juice.ORANGE, Like.Juice.GRAPE] | 1 + [Like.Juice.GRAPE, Like.Juice.PINEAPPLE] | 1 + [Like.Juice.CARROT] | 3 + [Like.Juice.CARROT, Like.Juice.TOMATO] | 2 + [] | 4 + } + void 'search in an array of strings with join with another domain class'() { setup: def user1 = new User(name:'John', like: new Like(favoriteMovies:["The Matrix", "The Lord of the Rings"])).save() @@ -167,4 +199,16 @@ class PgContainsCriteriaTestServiceIntegrationSpec extends IntegrationSpec { movie << [[1], ["Test", 1], [1L], ["Test", 1L]] } -} \ No newline at end of file + @Unroll + void 'search an invalid list inside the array of enum'() { + when: + def result = pgContainsCriteriaTestService.searchWithCriteriaEnumArray(juice) + + then: + thrown(HibernateException) + + where: + juice << [["Test"], [Like.Juice.ORANGE, "Test"], [1L], [Like.Juice.APPLE, 1L]] + } + +} From faf86f397a14299a56fcd7d633692c620c4f6981 Mon Sep 17 00:00:00 2001 From: Matt Feury Date: Sat, 24 Aug 2013 18:13:33 -0400 Subject: [PATCH 12/17] Add tests for pgArrayIsContainedBy with enum arrays --- .../PgIsContainedByCriteriaTestService.groovy | 22 +++++++++ ...yCriteriaTestServiceIntegrationSpec.groovy | 46 ++++++++++++++++++- 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/grails-app/services/test/criteria/array/PgIsContainedByCriteriaTestService.groovy b/grails-app/services/test/criteria/array/PgIsContainedByCriteriaTestService.groovy index 75fdc19..f3b4354 100644 --- a/grails-app/services/test/criteria/array/PgIsContainedByCriteriaTestService.groovy +++ b/grails-app/services/test/criteria/array/PgIsContainedByCriteriaTestService.groovy @@ -71,6 +71,28 @@ class PgIsContainedByCriteriaTestService { return result } + /** + * Search for "likes" which elements are equals to the parameter + */ + public List searchIsContainedByEnum(Like.Juice juice) { + def result = Like.withCriteria { + pgArrayIsContainedBy 'favoriteJuices', juice + } + + return result + } + + /** + * Search for "likes" contained by the parameter + */ + public List searchIsContainedByEnum(List juices) { + def result = Like.withCriteria { + pgArrayIsContainedBy 'favoriteJuices', juices + } + + return result + } + /** * Search with a join */ diff --git a/test/integration/net/kaleidos/hibernate/array/PgIsContainedByCriteriaTestServiceIntegrationSpec.groovy b/test/integration/net/kaleidos/hibernate/array/PgIsContainedByCriteriaTestServiceIntegrationSpec.groovy index 2c22c9c..ad70108 100644 --- a/test/integration/net/kaleidos/hibernate/array/PgIsContainedByCriteriaTestServiceIntegrationSpec.groovy +++ b/test/integration/net/kaleidos/hibernate/array/PgIsContainedByCriteriaTestServiceIntegrationSpec.groovy @@ -88,6 +88,38 @@ class PgIsContainedByCriteriaTestServiceIntegrationSpec extends IntegrationSpec [] | 0 } + @Unroll + void 'search #juice in an array of enums'() { + setup: + new Like(favoriteJuices:[Like.Juice.ORANGE]).save() + new Like(favoriteJuices:[Like.Juice.PINEAPPLE, Like.Juice.GRAPE, Like.Juice.CARROT, Like.Juice.CRANBERRY]).save() + new Like(favoriteJuices:[Like.Juice.APPLE]).save() + new Like(favoriteJuices:[Like.Juice.ORANGE, Like.Juice.CARROT]).save() + + when: + def result = pgIsContainedByCriteriaTestService.searchIsContainedByEnum(juice) + + then: + result.size() == resultSize + + where: + juice | resultSize + Like.Juice.CRANBERRY | 0 + Like.Juice.ORANGE | 1 + Like.Juice.LEMON | 0 + Like.Juice.APPLE | 1 + Like.Juice.GRAPE | 0 + Like.Juice.PINEAPPLE | 0 + Like.Juice.TOMATO | 0 + Like.Juice.CARROT | 0 + Like.Juice.GRAPEFRUIT | 0 + [Like.Juice.ORANGE, Like.Juice.GRAPE] | 1 + [Like.Juice.GRAPE, Like.Juice.PINEAPPLE, + Like.Juice.ORANGE, Like.Juice.CARROT] | 2 + [Like.Juice.APPLE] | 1 + [Like.Juice.CARROT, Like.Juice.ORANGE] | 2 + [] | 0 + } void 'search in an array of strings with join with another domain class'() { setup: @@ -165,4 +197,16 @@ class PgIsContainedByCriteriaTestServiceIntegrationSpec extends IntegrationSpec movie << [[1], ["Test", 1], [1L], ["Test", 1L]] } -} \ No newline at end of file + @Unroll + void 'search an invalid list #juice inside the array of enum'() { + when: + def result = pgIsContainedByCriteriaTestService.searchIsContainedByEnum(juice) + + then: + thrown(HibernateException) + + where: + juice << [["Test"], ["Test", Like.Juice.ORANGE], [1L], ["Test", 1L]] + } + +} From a4bd13c74b17937c721e546f5ff2a01229149b70 Mon Sep 17 00:00:00 2001 From: Matt Feury Date: Sat, 24 Aug 2013 18:15:51 -0400 Subject: [PATCH 13/17] Add tests for pgArrayIsEmpty with enum arrays --- .../array/PgIsEmptyCriteriaTestService.groovy | 11 +++++++++++ ...IsEmptyCriteriaTestServiceIntegrationSpec.groovy | 13 +++++++++++++ 2 files changed, 24 insertions(+) diff --git a/grails-app/services/test/criteria/array/PgIsEmptyCriteriaTestService.groovy b/grails-app/services/test/criteria/array/PgIsEmptyCriteriaTestService.groovy index f7e2e77..9558346 100644 --- a/grails-app/services/test/criteria/array/PgIsEmptyCriteriaTestService.groovy +++ b/grails-app/services/test/criteria/array/PgIsEmptyCriteriaTestService.groovy @@ -36,6 +36,17 @@ class PgIsEmptyCriteriaTestService { return result } + /** + * Search "empty" enum arrays + */ + public List searchEmptyEnumArray() { + def result = Like.withCriteria { + pgArrayIsEmpty 'favoriteJuices' + } + + return result + } + /** * Search "empty" arrays with a join */ diff --git a/test/integration/net/kaleidos/hibernate/array/PgIsEmptyCriteriaTestServiceIntegrationSpec.groovy b/test/integration/net/kaleidos/hibernate/array/PgIsEmptyCriteriaTestServiceIntegrationSpec.groovy index b37f525..0a49bed 100644 --- a/test/integration/net/kaleidos/hibernate/array/PgIsEmptyCriteriaTestServiceIntegrationSpec.groovy +++ b/test/integration/net/kaleidos/hibernate/array/PgIsEmptyCriteriaTestServiceIntegrationSpec.groovy @@ -51,6 +51,19 @@ class PgIsEmptyCriteriaTestServiceIntegrationSpec extends IntegrationSpec { result.size() == 2 } + void 'search for empty enum arrays'() { + setup: + new Like(favoriteJuices:[Like.Juice.APPLE, Like.Juice.TOMATO]).save() + new Like(favoriteJuices:[]).save() + new Like(favoriteJuices:[]).save() + + when: + def result = pgIsEmptyCriteriaTestService.searchEmptyEnumArray() + + then: + result.size() == 2 + } + void 'search in an array of strings with join with another domain class'() { setup: def user1 = new User(name:'John', like: new Like(favoriteMovies:["The Matrix", "The Lord of the Rings"])).save() From 93481554a0fbf8aae1a96a8d935f04505f54755c Mon Sep 17 00:00:00 2001 From: Matt Feury Date: Sat, 24 Aug 2013 18:19:08 -0400 Subject: [PATCH 14/17] Add test for pgArrayIsNotEmpty on enum arrays --- .../array/PgIsNotEmptyCriteriaTestService.groovy | 11 +++++++++++ ...otEmptyCriteriaTestServiceIntegrationSpec.groovy | 13 +++++++++++++ 2 files changed, 24 insertions(+) diff --git a/grails-app/services/test/criteria/array/PgIsNotEmptyCriteriaTestService.groovy b/grails-app/services/test/criteria/array/PgIsNotEmptyCriteriaTestService.groovy index 149f0bf..e33a0b9 100644 --- a/grails-app/services/test/criteria/array/PgIsNotEmptyCriteriaTestService.groovy +++ b/grails-app/services/test/criteria/array/PgIsNotEmptyCriteriaTestService.groovy @@ -36,6 +36,17 @@ class PgIsNotEmptyCriteriaTestService { return result } + /** + * Search "non empty" enum arrays + */ + public List searchNonEmptyEnumArray() { + def result = Like.withCriteria { + pgArrayIsNotEmpty 'favoriteJuices' + } + + return result + } + /** * Search "non empty" arrays with a join */ diff --git a/test/integration/net/kaleidos/hibernate/array/PgIsNotEmptyCriteriaTestServiceIntegrationSpec.groovy b/test/integration/net/kaleidos/hibernate/array/PgIsNotEmptyCriteriaTestServiceIntegrationSpec.groovy index 6382d02..9516408 100644 --- a/test/integration/net/kaleidos/hibernate/array/PgIsNotEmptyCriteriaTestServiceIntegrationSpec.groovy +++ b/test/integration/net/kaleidos/hibernate/array/PgIsNotEmptyCriteriaTestServiceIntegrationSpec.groovy @@ -51,6 +51,19 @@ class PgIsNotEmptyCriteriaTestServiceIntegrationSpec extends IntegrationSpec { result.size() == 1 } + void 'search for empty enum arrays'() { + setup: + new Like(favoriteJuices:[Like.Juice.APPLE, Like.Juice.TOMATO]).save() + new Like(favoriteJuices:[]).save() + new Like(favoriteJuices:[]).save() + + when: + def result = pgIsNotEmptyCriteriaTestService.searchNonEmptyEnumArray() + + then: + result.size() == 1 + } + void 'search in an array of strings with join with another domain class'() { setup: def user1 = new User(name:'John', like: new Like(favoriteMovies:["The Matrix", "The Lord of the Rings"])).save() From 0fdb03bc69bb3f3a859864d307dd4b03a87ea97d Mon Sep 17 00:00:00 2001 From: Matt Feury Date: Sat, 24 Aug 2013 18:25:45 -0400 Subject: [PATCH 15/17] Add tests for pgArrayOverlaps of enum arrays --- .../PgOverlapsCriteriaTestService.groovy | 22 +++++++++ ...sCriteriaTestServiceIntegrationSpec.groovy | 45 ++++++++++++++++++- 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/grails-app/services/test/criteria/array/PgOverlapsCriteriaTestService.groovy b/grails-app/services/test/criteria/array/PgOverlapsCriteriaTestService.groovy index af524a7..2e57b6b 100644 --- a/grails-app/services/test/criteria/array/PgOverlapsCriteriaTestService.groovy +++ b/grails-app/services/test/criteria/array/PgOverlapsCriteriaTestService.groovy @@ -69,6 +69,28 @@ class PgOverlapsCriteriaTestService { return result } + /** + * Search overlaps "likes" with enum in array + */ + public List overlapsEnumArray(Like.Juice juice) { + def result = Like.withCriteria { + pgArrayOverlaps 'favoriteJuices', juice + } + + return result + } + + /** + * Search overlaps "likes" with n enums in array + */ + public List overlapsEnumArray(List juices) { + def result = Like.withCriteria { + pgArrayOverlaps 'favoriteJuices', juices + } + + return result + } + /** * Search overlaps with a join */ diff --git a/test/integration/net/kaleidos/hibernate/array/PgOverlapsCriteriaTestServiceIntegrationSpec.groovy b/test/integration/net/kaleidos/hibernate/array/PgOverlapsCriteriaTestServiceIntegrationSpec.groovy index 87de3eb..b4cfeb5 100644 --- a/test/integration/net/kaleidos/hibernate/array/PgOverlapsCriteriaTestServiceIntegrationSpec.groovy +++ b/test/integration/net/kaleidos/hibernate/array/PgOverlapsCriteriaTestServiceIntegrationSpec.groovy @@ -91,6 +91,38 @@ class PgOverlapsCriteriaTestServiceIntegrationSpec extends IntegrationSpec { [] | 0 } + @Unroll + void 'search #juice in an array of enums'() { + setup: + new Like(favoriteJuices:[Like.Juice.ORANGE, Like.Juice.GRAPE]).save() + new Like(favoriteJuices:[Like.Juice.PINEAPPLE, Like.Juice.GRAPE, Like.Juice.CARROT, Like.Juice.CRANBERRY]).save() + new Like(favoriteJuices:[Like.Juice.APPLE, Like.Juice.TOMATO, Like.Juice.CARROT]).save() + new Like(favoriteJuices:[Like.Juice.ORANGE, Like.Juice.TOMATO, Like.Juice.CARROT]).save() + + when: + def result = pgOverlapsCriteriaTestService.overlapsEnumArray(juice) + + then: + result.size() == resultSize + + where: + juice | resultSize + Like.Juice.CRANBERRY | 1 + Like.Juice.ORANGE | 2 + Like.Juice.LEMON | 0 + Like.Juice.APPLE | 1 + Like.Juice.GRAPE | 2 + Like.Juice.PINEAPPLE | 1 + Like.Juice.TOMATO | 2 + Like.Juice.CARROT | 3 + Like.Juice.GRAPEFRUIT | 0 + [Like.Juice.ORANGE, Like.Juice.GRAPE] | 3 + [Like.Juice.GRAPE, Like.Juice.PINEAPPLE] | 2 + [Like.Juice.CARROT] | 3 + [Like.Juice.CARROT, Like.Juice.TOMATO] | 3 + [] | 0 + } + void 'search in an array of strings with join with another domain class'() { setup: def user1 = new User(name:'John', like: new Like(favoriteMovies:["The Matrix", "The Lord of the Rings"])).save() @@ -167,4 +199,15 @@ class PgOverlapsCriteriaTestServiceIntegrationSpec extends IntegrationSpec { movie << [[1], ["Test", 1], [1L], ["Test", 1L]] } -} \ No newline at end of file + @Unroll + void 'search an invalid list inside the array of enum'() { + when: + def result = pgOverlapsCriteriaTestService.overlapsEnumArray(juice) + + then: + thrown(HibernateException) + + where: + juice << [["Test"], [Like.Juice.ORANGE, "Test"], [1L], [Like.Juice.APPLE, 1L]] + } +} From bfa77df42c5e6ebde5a570b895dd66217f1f915b Mon Sep 17 00:00:00 2001 From: Matt Feury Date: Sat, 24 Aug 2013 18:29:57 -0400 Subject: [PATCH 16/17] Remove unnecessary methods from readme's enum --- README.md | 8 -------- 1 file changed, 8 deletions(-) diff --git a/README.md b/README.md index 23dace4..efbd67f 100644 --- a/README.md +++ b/README.md @@ -74,14 +74,6 @@ class Like { private final int value Juice(int value) { this.value = value } - public int value() { return this.value } - public int getId() { return this.value } - - public String getName() { this.toString() } - - // Enum Constructors are not publicly visible - // so we have to loop through the values and find the desired one - public static withId(int value) { Juice.values().find { it.value() == value } } } static mapping = { From 41df11dd5176ed596fdd9c9b508ddc8553832ffb Mon Sep 17 00:00:00 2001 From: Matt Feury Date: Sun, 25 Aug 2013 02:36:27 -0400 Subject: [PATCH 17/17] Cache bidirectional enum mapping. Not sure if this'll gain us much, but it's probably best to cache as best as possible at the db level. --- .../kaleidos/hibernate/usertype/IdentityEnumArrayType.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/java/net/kaleidos/hibernate/usertype/IdentityEnumArrayType.java b/src/java/net/kaleidos/hibernate/usertype/IdentityEnumArrayType.java index df2253c..fe7145a 100644 --- a/src/java/net/kaleidos/hibernate/usertype/IdentityEnumArrayType.java +++ b/src/java/net/kaleidos/hibernate/usertype/IdentityEnumArrayType.java @@ -36,6 +36,7 @@ public class IdentityEnumArrayType extends IntegerArrayType implements Parameter private static final Log LOG = LogFactory.getLog(IdentityEnumArrayType.class); private Class> enumClass; + private BidiEnumMap bidiMap; public static final String ENUM_ID_ACCESSOR = "getId"; public static final String PARAM_ENUM_CLASS = "enumClass"; @@ -53,7 +54,10 @@ public int[] sqlTypes() { public Object idToEnum(Object id) throws HibernateException, SQLException { try { - BidiEnumMap bidiMap = new BidiEnumMap(enumClass); + if (bidiMap == null) { + bidiMap = new BidiEnumMap(enumClass); + } + return bidiMap.getEnumValue(id); } catch (Exception e) { throw new HibernateException("Uh oh. Unable to create bidirectional enum map for " + enumClass + " with nested exception: " + e.toString());