From a7098ce453b13a37e486a8786c6a7c4390d7ccc1 Mon Sep 17 00:00:00 2001 From: Adrian Moos Date: Wed, 27 Jul 2016 21:42:38 +0200 Subject: [PATCH 1/4] improve SERIALIZE_IDENTIFIER_FOR_LAZY_NOT_LOADED_OBJECTS feature - don't require session or mapping for ids mapped with property access - add a testcase - improve documentation --- .../datatype/hibernate4/Hibernate4Module.java | 15 ++++-- .../hibernate4/HibernateProxySerializer.java | 46 ++++++++++++++++++- .../datatype/hibernate4/LazyLoadingTest.java | 23 ++++++++++ 3 files changed, 79 insertions(+), 5 deletions(-) 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..5d9ff72f 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,8 @@ package com.fasterxml.jackson.datatype.hibernate4; 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 +20,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 +177,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 +185,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 +200,40 @@ protected Object findProxied(HibernateProxy proxy) } return init.getImplementation(); } + + // Alas, hibernate offers no public api to access this information, so we must resort to ugly hacks ... + protected static class ProxyReader { + + // static final so the JVM can inline the lookup + private static final Field getIdentifierMethod; + + static { + try { + getIdentifierMethod = BasicLazyInitializer.class.getDeclaredField("getIdentifierMethod"); + getIdentifierMethod.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) getIdentifierMethod.get(init); + if (idGetter == null) { + return null; + } + String name = idGetter.getName(); + if (name.startsWith("get")) { + name = Character.toLowerCase(name.charAt(3)) + name.substring(4); + } + return name; + } catch (Exception 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(); + } + } } From ba677a94c5df7ddacbf6bda48d80a7a02e8cf303 Mon Sep 17 00:00:00 2001 From: Adrian Moos Date: Tue, 2 Aug 2016 14:53:19 +0200 Subject: [PATCH 2/4] more robust application of the JavaBean naming convention --- .../datatype/hibernate4/HibernateProxySerializer.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 5d9ff72f..22fb616f 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,5 +1,6 @@ package com.fasterxml.jackson.datatype.hibernate4; +import java.beans.Introspector; import java.io.IOException; import java.lang.reflect.Field; import java.lang.reflect.Method; @@ -228,10 +229,10 @@ static String getIdentifierPropertyName(LazyInitializer init) { } String name = idGetter.getName(); if (name.startsWith("get")) { - name = Character.toLowerCase(name.charAt(3)) + name.substring(4); + name = Introspector.decapitalize(name.substring(3)); } return name; - } catch (Exception e) { + } catch (IllegalAccessException e) { throw new RuntimeException(e); } } From c119de40d62ae80922e63dcb7664f440f7112683 Mon Sep 17 00:00:00 2001 From: Adrian Moos Date: Tue, 9 Aug 2016 15:40:37 +0200 Subject: [PATCH 3/4] communicate intent more clearly --- .../hibernate4/HibernateProxySerializer.java | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) 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 22fb616f..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 @@ -202,16 +202,21 @@ protected Object findProxied(HibernateProxy proxy) return init.getImplementation(); } - // Alas, hibernate offers no public api to access this information, so we must resort to ugly hacks ... + /** + * 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 getIdentifierMethod; + private static final Field getIdentifierMethodField; static { try { - getIdentifierMethod = BasicLazyInitializer.class.getDeclaredField("getIdentifierMethod"); - getIdentifierMethod.setAccessible(true); + 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); @@ -223,7 +228,7 @@ protected static class ProxyReader { */ static String getIdentifierPropertyName(LazyInitializer init) { try { - Method idGetter = (Method) getIdentifierMethod.get(init); + Method idGetter = (Method) getIdentifierMethodField.get(init); if (idGetter == null) { return null; } From a33127007537099bfcc869a056ad9de500753406 Mon Sep 17 00:00:00 2001 From: Adrian Moos Date: Tue, 9 Aug 2016 15:51:09 +0200 Subject: [PATCH 4/4] improve SERIALIZE_IDENTIFIER_FOR_LAZY_NOT_LOADED_OBJECTS feature - don't require session or mapping for ids mapped with property access - add a testcase - improve documentation --- .../datatype/hibernate5/Hibernate5Module.java | 15 ++++-- .../hibernate5/HibernateProxySerializer.java | 52 ++++++++++++++++++- .../datatype/hibernate5/LazyLoadingTest.java | 23 ++++++++ 3 files changed, 85 insertions(+), 5 deletions(-) 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 + *

    + *
  • the {@link Mapping} is provided to the Hibernate5Module, or
  • + *
  • the persistence context that loaded the proxy has not yet been closed, or
  • + *
  • the id property is mapped with property access (for instance because the {@code @Id} + * annotation is applied to a method rather than a field)
  • + *
+ * 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(); + } + } }