-
Notifications
You must be signed in to change notification settings - Fork 239
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
CB-16465 Lazy load safe toString() methods
Added tests for Entity classes to make sure they implement toString() correctly.
- Loading branch information
Showing
94 changed files
with
1,183 additions
and
26 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
7 changes: 7 additions & 0 deletions
7
autoscale/src/test/java/com/sequenceiq/periscope/PeriscopeEntityToStringTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
package com.sequenceiq.periscope; | ||
|
||
import com.sequenceiq.cloudbreak.AbstractEntityToStringTest; | ||
|
||
class PeriscopeEntityToStringTest extends AbstractEntityToStringTest { | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
155 changes: 155 additions & 0 deletions
155
common/src/test/java/com/sequenceiq/cloudbreak/AbstractEntityToStringTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,155 @@ | ||
package com.sequenceiq.cloudbreak; | ||
|
||
import static org.assertj.core.api.SoftAssertions.assertSoftly; | ||
import static org.mockito.Mockito.mock; | ||
import static org.mockito.Mockito.when; | ||
import static org.mockito.Mockito.withSettings; | ||
|
||
import java.lang.reflect.Field; | ||
import java.lang.reflect.Method; | ||
import java.lang.reflect.Modifier; | ||
import java.util.Map; | ||
import java.util.Set; | ||
import java.util.function.Function; | ||
import java.util.stream.Collectors; | ||
import java.util.stream.Stream; | ||
|
||
import javax.persistence.Entity; | ||
import javax.persistence.FetchType; | ||
import javax.persistence.ManyToOne; | ||
import javax.persistence.OneToMany; | ||
import javax.persistence.OneToOne; | ||
|
||
import org.hibernate.proxy.HibernateProxy; | ||
import org.hibernate.proxy.LazyInitializer; | ||
import org.junit.jupiter.api.BeforeEach; | ||
import org.junit.jupiter.api.Test; | ||
import org.reflections.Reflections; | ||
import org.reflections.scanners.SubTypesScanner; | ||
import org.reflections.scanners.TypeAnnotationsScanner; | ||
import org.springframework.util.ReflectionUtils; | ||
|
||
public abstract class AbstractEntityToStringTest { | ||
|
||
private static Set<Class<?>> ENTITY_CLASSES; | ||
|
||
@BeforeEach | ||
void setUp() { | ||
if (ENTITY_CLASSES == null) { | ||
ENTITY_CLASSES = getEntityClasses(); | ||
} | ||
} | ||
|
||
Set<Class<?>> getEntityClasses() { | ||
return new Reflections(this.getClass().getPackageName(), new TypeAnnotationsScanner(), new SubTypesScanner()) | ||
.getTypesAnnotatedWith(Entity.class, true) | ||
.stream() | ||
.filter(clazz -> !Modifier.isAbstract(clazz.getModifiers())) | ||
.collect(Collectors.toSet()); | ||
} | ||
|
||
@Test | ||
void testImplementedToString() { | ||
assertSoftly(softly -> | ||
ENTITY_CLASSES.forEach(entityClass -> { | ||
Method toString = ReflectionUtils.findMethod(entityClass, "toString"); | ||
if (Object.class.equals(toString.getDeclaringClass())) { | ||
softly.fail("Class %s does not implement toString() %s", entityClass.getName(), toString.getDeclaringClass()); | ||
} | ||
})); | ||
} | ||
|
||
@Test | ||
void testLazyLoadSafeToString() { | ||
assertSoftly(softly -> | ||
ENTITY_CLASSES.forEach(entityClass -> { | ||
Object entityInstance = createEntityInstance(entityClass); | ||
setLazyLoadedFields(entityClass, entityInstance); | ||
try { | ||
entityInstance.toString(); | ||
} catch (UninitializedToString e) { | ||
softly.fail("Lazy loaded field %s may throw UninitializedException, please wrap in DatabaseUtil.lazyLoadSafeToString()", e.getField()); | ||
} | ||
})); | ||
} | ||
|
||
/** | ||
* Set circular entity references, so if they are referenced in each other's toString() methods, the test will fail with a {@link StackOverflowError} | ||
*/ | ||
@Test | ||
void testCircularEntityReferencesInToString() { | ||
Map<? extends Class<?>, Object> entityInstancesByClass = ENTITY_CLASSES.stream() | ||
.map(this::createEntityInstance) | ||
.collect(Collectors.toMap(Object::getClass, Function.identity())); | ||
|
||
entityInstancesByClass.values().forEach(entity -> Stream.of(entity.getClass().getDeclaredFields()) | ||
.filter(field -> entityInstancesByClass.containsKey(field.getType())) | ||
.forEach(field -> { | ||
ReflectionUtils.makeAccessible(field); | ||
ReflectionUtils.setField(field, entity, entityInstancesByClass.get(field.getType())); | ||
})); | ||
|
||
assertSoftly(softly -> entityInstancesByClass.values() | ||
.forEach(entityInstance -> softly.assertThatCode(entityInstance::toString).doesNotThrowAnyException())); | ||
} | ||
|
||
private Object createEntityInstance(Class<?> entityClass) { | ||
try { | ||
Object instance = entityClass.getDeclaredConstructor().newInstance(); | ||
Class<?> clazz = entityClass; | ||
do { | ||
try { | ||
for (Field field : clazz.getDeclaredFields()) { | ||
if (!Modifier.isFinal(field.getType().getModifiers()) && !Modifier.isStatic(field.getModifiers())) { | ||
field.setAccessible(true); | ||
field.set(instance, mock(field.getType())); | ||
} | ||
} | ||
clazz = clazz.getSuperclass(); | ||
} catch (Exception e) { | ||
throw new RuntimeException("Failed to set fields to mock values", e); | ||
} | ||
} while (!clazz.equals(Object.class)); | ||
return instance; | ||
} catch (Exception e) { | ||
throw new RuntimeException("Failed to verify toString() implementation of entity " + entityClass.getName(), e); | ||
} | ||
} | ||
|
||
private void setLazyLoadedFields(Class<?> entityClass, Object newInstance) { | ||
Set<Field> lazyLoadedFields = Stream.of(entityClass.getDeclaredFields()) | ||
.filter(this::isLazyLoaded) | ||
.collect(Collectors.toSet()); | ||
lazyLoadedFields.forEach(field -> { | ||
ReflectionUtils.makeAccessible(field); | ||
ReflectionUtils.setField(field, newInstance, createProxyObject(field.getType(), field)); | ||
}); | ||
} | ||
|
||
private boolean isLazyLoaded(Field field) { | ||
return (field.isAnnotationPresent(OneToMany.class) && FetchType.LAZY.equals(field.getAnnotation(OneToMany.class).fetch())) | ||
|| (field.isAnnotationPresent(OneToOne.class) && FetchType.LAZY.equals(field.getAnnotation(OneToOne.class).fetch())) | ||
|| (field.isAnnotationPresent(ManyToOne.class) && FetchType.LAZY.equals(field.getAnnotation(ManyToOne.class).fetch())); | ||
} | ||
|
||
private <T> T createProxyObject(Class<T> type, Field field) { | ||
LazyInitializer lazyInitializer = mock(LazyInitializer.class); | ||
when(lazyInitializer.isUninitialized()).thenReturn(true); | ||
T hibernateProxy = mock(type, withSettings().extraInterfaces(HibernateProxy.class)); | ||
when(((HibernateProxy) hibernateProxy).getHibernateLazyInitializer()).thenReturn(lazyInitializer); | ||
when(hibernateProxy.toString()).thenThrow(new UninitializedToString(field)); | ||
return hibernateProxy; | ||
} | ||
|
||
static class UninitializedToString extends RuntimeException { | ||
private final Field field; | ||
|
||
UninitializedToString(Field field) { | ||
this.field = field; | ||
} | ||
|
||
public Field getField() { | ||
return field; | ||
} | ||
} | ||
} |
Oops, something went wrong.