diff --git a/hibernate4/src/main/java/com/fasterxml/jackson/datatype/hibernate4/Hibernate4Module.java b/hibernate4/src/main/java/com/fasterxml/jackson/datatype/hibernate4/Hibernate4Module.java index 2b43d88e..fc935dfb 100644 --- a/hibernate4/src/main/java/com/fasterxml/jackson/datatype/hibernate4/Hibernate4Module.java +++ b/hibernate4/src/main/java/com/fasterxml/jackson/datatype/hibernate4/Hibernate4Module.java @@ -31,10 +31,19 @@ public enum Feature { USE_TRANSIENT_ANNOTATION(true), /** - * If FORCE_LAZY_LOADING is false lazy-loaded object should be serialized as map IdentifierName=>IdentifierValue - * instead of null (true); or serialized as nulls (false) + * If FORCE_LAZY_LOADING is false, this feature serializes uninitialized lazy loading proxies as + * {"identifierName":"identifierValue"} rather than null. *

- * Default value is false. + * Default value is false. + *

+ * Note that the name of the identifier property can only be determined if + *

+ * Otherwise, the entity name will be used instead. */ SERIALIZE_IDENTIFIER_FOR_LAZY_NOT_LOADED_OBJECTS(false), diff --git a/hibernate4/src/main/java/com/fasterxml/jackson/datatype/hibernate4/HibernateProxySerializer.java b/hibernate4/src/main/java/com/fasterxml/jackson/datatype/hibernate4/HibernateProxySerializer.java index 5b850beb..229aec19 100644 --- a/hibernate4/src/main/java/com/fasterxml/jackson/datatype/hibernate4/HibernateProxySerializer.java +++ b/hibernate4/src/main/java/com/fasterxml/jackson/datatype/hibernate4/HibernateProxySerializer.java @@ -1,6 +1,9 @@ package com.fasterxml.jackson.datatype.hibernate4; +import java.beans.Introspector; import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.Method; import java.util.HashMap; import com.fasterxml.jackson.core.*; @@ -18,6 +21,7 @@ import org.hibernate.engine.spi.SessionImplementor; import org.hibernate.proxy.HibernateProxy; import org.hibernate.proxy.LazyInitializer; +import org.hibernate.proxy.pojo.BasicLazyInitializer; /** * Serializer to use for values proxied using {@link org.hibernate.proxy.HibernateProxy}. @@ -174,7 +178,7 @@ protected Object findProxied(HibernateProxy proxy) LazyInitializer init = proxy.getHibernateLazyInitializer(); if (!_forceLazyLoading && init.isUninitialized()) { if (_serializeIdentifier) { - final String idName; + String idName; if (_mapping != null) { idName = _mapping.getIdentifierPropertyName(init.getEntityName()); } else { @@ -182,7 +186,10 @@ protected Object findProxied(HibernateProxy proxy) if (session != null) { idName = session.getFactory().getIdentifierPropertyName(init.getEntityName()); } else { - idName = init.getEntityName(); + idName = ProxyReader.getIdentifierPropertyName(init); + if (idName == null) { + idName = init.getEntityName(); + } } } final Object idValue = init.getIdentifier(); @@ -194,4 +201,45 @@ protected Object findProxied(HibernateProxy proxy) } return init.getImplementation(); } + + /** + * Inspects a Hibernate proxy to try and determine the name of the identifier property + * (Hibernate proxies know the getter of the identifier property because it receives special + * treatment in the invocation handler). Alas, the field storing the method reference is + * private and has no getter, so we must resort to ugly reflection hacks to read its value ... + */ + protected static class ProxyReader { + + // static final so the JVM can inline the lookup + private static final Field getIdentifierMethodField; + + static { + try { + getIdentifierMethodField = BasicLazyInitializer.class.getDeclaredField("getIdentifierMethod"); + getIdentifierMethodField.setAccessible(true); + } catch (Exception e) { + // should never happen: the field exists in all versions of hibernate 4 and 5 + throw new RuntimeException(e); + } + } + + /** + * @return the name of the identifier property, or null if the name could not be determined + */ + static String getIdentifierPropertyName(LazyInitializer init) { + try { + Method idGetter = (Method) getIdentifierMethodField.get(init); + if (idGetter == null) { + return null; + } + String name = idGetter.getName(); + if (name.startsWith("get")) { + name = Introspector.decapitalize(name.substring(3)); + } + return name; + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + } } diff --git a/hibernate4/src/test/java/com/fasterxml/jackson/datatype/hibernate4/LazyLoadingTest.java b/hibernate4/src/test/java/com/fasterxml/jackson/datatype/hibernate4/LazyLoadingTest.java index b45a1f5a..0a638dbf 100644 --- a/hibernate4/src/test/java/com/fasterxml/jackson/datatype/hibernate4/LazyLoadingTest.java +++ b/hibernate4/src/test/java/com/fasterxml/jackson/datatype/hibernate4/LazyLoadingTest.java @@ -6,7 +6,9 @@ import javax.persistence.EntityManagerFactory; import javax.persistence.Persistence; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.hibernate4.Hibernate4Module.Feature; import com.fasterxml.jackson.datatype.hibernate4.data.Customer; import com.fasterxml.jackson.datatype.hibernate4.data.Payment; @@ -54,4 +56,25 @@ public void testGetCustomerJson() throws Exception emf.close(); } } + + @Test + public void testSerializeIdentifierFeature() throws JsonProcessingException { + Hibernate4Module module = new Hibernate4Module(); + module.enable(Feature.SERIALIZE_IDENTIFIER_FOR_LAZY_NOT_LOADED_OBJECTS); + ObjectMapper objectMapper = new ObjectMapper().registerModule(module); + + EntityManagerFactory emf = Persistence.createEntityManagerFactory("persistenceUnit"); + try { + EntityManager em = emf.createEntityManager(); + Customer customerRef = em.getReference(Customer.class, 103); + em.close(); + assertFalse(Hibernate.isInitialized(customerRef)); + + String json = objectMapper.writeValueAsString(customerRef); + assertFalse(Hibernate.isInitialized(customerRef)); + assertEquals("{\"customerNumber\":103}", json); + } finally { + emf.close(); + } + } } diff --git a/hibernate5/src/main/java/com/fasterxml/jackson/datatype/hibernate5/Hibernate5Module.java b/hibernate5/src/main/java/com/fasterxml/jackson/datatype/hibernate5/Hibernate5Module.java index 0d0b1798..f4d727c8 100644 --- a/hibernate5/src/main/java/com/fasterxml/jackson/datatype/hibernate5/Hibernate5Module.java +++ b/hibernate5/src/main/java/com/fasterxml/jackson/datatype/hibernate5/Hibernate5Module.java @@ -31,10 +31,19 @@ public enum Feature { USE_TRANSIENT_ANNOTATION(true), /** - * If FORCE_LAZY_LOADING is false lazy-loaded object should be serialized as map IdentifierName=>IdentifierValue - * instead of null (true); or serialized as nulls (false) + * If FORCE_LAZY_LOADING is false, this feature serializes uninitialized lazy loading proxies as + * {"identifierName":"identifierValue"} rather than null. *

- * Default value is false. + * Default value is false. + *

+ * Note that the name of the identifier property can only be determined if + *

+ * Otherwise, the entity name will be used instead. */ SERIALIZE_IDENTIFIER_FOR_LAZY_NOT_LOADED_OBJECTS(false), diff --git a/hibernate5/src/main/java/com/fasterxml/jackson/datatype/hibernate5/HibernateProxySerializer.java b/hibernate5/src/main/java/com/fasterxml/jackson/datatype/hibernate5/HibernateProxySerializer.java index b25086f8..d12f5931 100644 --- a/hibernate5/src/main/java/com/fasterxml/jackson/datatype/hibernate5/HibernateProxySerializer.java +++ b/hibernate5/src/main/java/com/fasterxml/jackson/datatype/hibernate5/HibernateProxySerializer.java @@ -1,6 +1,9 @@ package com.fasterxml.jackson.datatype.hibernate5; +import java.beans.Introspector; import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.Method; import java.util.HashMap; import com.fasterxml.jackson.core.JsonGenerator; @@ -18,6 +21,7 @@ import org.hibernate.engine.spi.SessionImplementor; import org.hibernate.proxy.HibernateProxy; import org.hibernate.proxy.LazyInitializer; +import org.hibernate.proxy.pojo.BasicLazyInitializer; /** * Serializer to use for values proxied using {@link org.hibernate.proxy.HibernateProxy}. @@ -174,7 +178,7 @@ protected Object findProxied(HibernateProxy proxy) LazyInitializer init = proxy.getHibernateLazyInitializer(); if (!_forceLazyLoading && init.isUninitialized()) { if (_serializeIdentifier) { - final String idName; + String idName; if (_mapping != null) { idName = _mapping.getIdentifierPropertyName(init.getEntityName()); } else { @@ -182,7 +186,10 @@ protected Object findProxied(HibernateProxy proxy) if (session != null) { idName = session.getFactory().getIdentifierPropertyName(init.getEntityName()); } else { - idName = init.getEntityName(); + idName = ProxyReader.getIdentifierPropertyName(init); + if (idName == null) { + idName = init.getEntityName(); + } } } final Object idValue = init.getIdentifier(); @@ -194,4 +201,45 @@ protected Object findProxied(HibernateProxy proxy) } return init.getImplementation(); } + + /** + * Inspects a Hibernate proxy to try and determine the name of the identifier property + * (Hibernate proxies know the getter of the identifier property because it receives special + * treatment in the invocation handler). Alas, the field storing the method reference is + * private and has no getter, so we must resort to ugly reflection hacks to read its value ... + */ + protected static class ProxyReader { + + // static final so the JVM can inline the lookup + private static final Field getIdentifierMethodField; + + static { + try { + getIdentifierMethodField = BasicLazyInitializer.class.getDeclaredField("getIdentifierMethod"); + getIdentifierMethodField.setAccessible(true); + } catch (Exception e) { + // should never happen: the field exists in all versions of hibernate 4 and 5 + throw new RuntimeException(e); + } + } + + /** + * @return the name of the identifier property, or null if the name could not be determined + */ + static String getIdentifierPropertyName(LazyInitializer init) { + try { + Method idGetter = (Method) getIdentifierMethodField.get(init); + if (idGetter == null) { + return null; + } + String name = idGetter.getName(); + if (name.startsWith("get")) { + name = Introspector.decapitalize(name.substring(3)); + } + return name; + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + } } diff --git a/hibernate5/src/test/java/com/fasterxml/jackson/datatype/hibernate5/LazyLoadingTest.java b/hibernate5/src/test/java/com/fasterxml/jackson/datatype/hibernate5/LazyLoadingTest.java index 6a930a6a..0f8d1b0a 100644 --- a/hibernate5/src/test/java/com/fasterxml/jackson/datatype/hibernate5/LazyLoadingTest.java +++ b/hibernate5/src/test/java/com/fasterxml/jackson/datatype/hibernate5/LazyLoadingTest.java @@ -6,7 +6,9 @@ import javax.persistence.EntityManagerFactory; import javax.persistence.Persistence; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.hibernate5.Hibernate5Module.Feature; import com.fasterxml.jackson.datatype.hibernate5.data.Customer; import com.fasterxml.jackson.datatype.hibernate5.data.Payment; @@ -57,4 +59,25 @@ public void testGetCustomerJson() throws Exception emf.close(); } } + + @Test + public void testSerializeIdentifierFeature() throws JsonProcessingException { + Hibernate5Module module = new Hibernate5Module(); + module.enable(Feature.SERIALIZE_IDENTIFIER_FOR_LAZY_NOT_LOADED_OBJECTS); + ObjectMapper objectMapper = new ObjectMapper().registerModule(module); + + EntityManagerFactory emf = Persistence.createEntityManagerFactory("persistenceUnit"); + try { + EntityManager em = emf.createEntityManager(); + Customer customerRef = em.getReference(Customer.class, 103); + em.close(); + assertFalse(Hibernate.isInitialized(customerRef)); + + String json = objectMapper.writeValueAsString(customerRef); + assertFalse(Hibernate.isInitialized(customerRef)); + assertEquals("{\"customerNumber\":103}", json); + } finally { + emf.close(); + } + } }