Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve SERIALIZE_IDENTIFIER_FOR_LAZY_NOT_LOADED_OBJECTS feature #96

Merged
merged 4 commits into from
Aug 23, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
* <code>{"identifierName":"identifierValue"}</code> rather than <code>null</code>.
* <p>
* Default value is false.
* Default value is false.
* <p>
* Note that the name of the identifier property can only be determined if
* <ul>
* <li>the {@link Mapping} is provided to the Hibernate4Module, or </li>
* <li>the persistence context that loaded the proxy has not yet been closed, or</li>
* <li>the id property is mapped with property access (for instance because the {@code @Id}
* annotation is applied to a method rather than a field)</li>
* </ul>
* Otherwise, the entity name will be used instead.
*/
SERIALIZE_IDENTIFIER_FOR_LAZY_NOT_LOADED_OBJECTS(false),

Expand Down
Original file line number Diff line number Diff line change
@@ -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.*;
Expand All @@ -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}.
Expand Down Expand Up @@ -174,15 +178,18 @@ 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 {
final SessionImplementor session = init.getSession();
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();
Expand All @@ -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")) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably has to remain here; ideally BeanUtil from jackson-databind could be used but unfortunately okNameForIsGetter requires AnnotatedMethod, not base Method so it can not be used.

But there is Introspector.decapitalize():

http://docs.oracle.com/javase/7/docs/api/java/beans/Introspector.html#decapitalize(java.lang.String)

which is what you can call, so:

String name = Introspector.decapitalize(name.substring(3));

if name starts with "get". And the reason to call the method is that getURL() should result in URL, and NOT uRL.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good catch, thanks :-)

name = Introspector.decapitalize(name.substring(3));
}
return name;
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
* <code>{"identifierName":"identifierValue"}</code> rather than <code>null</code>.
* <p>
* Default value is false.
* Default value is false.
* <p>
* Note that the name of the identifier property can only be determined if
* <ul>
* <li>the {@link Mapping} is provided to the Hibernate5Module, or </li>
* <li>the persistence context that loaded the proxy has not yet been closed, or</li>
* <li>the id property is mapped with property access (for instance because the {@code @Id}
* annotation is applied to a method rather than a field)</li>
* </ul>
* Otherwise, the entity name will be used instead.
*/
SERIALIZE_IDENTIFIER_FOR_LAZY_NOT_LOADED_OBJECTS(false),

Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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}.
Expand Down Expand Up @@ -174,15 +178,18 @@ 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 {
final SessionImplementor session = init.getSession();
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();
Expand All @@ -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);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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();
}
}
}