Skip to content

Commit ffb08be

Browse files
GH-2499 - Add support for AfterConvert events and @PostLoad annotated methods.
This closes #2499.
1 parent 80fbbb6 commit ffb08be

File tree

13 files changed

+623
-52
lines changed

13 files changed

+623
-52
lines changed

src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jEntityConverter.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
import org.springframework.data.mapping.model.EntityInstantiators;
5353
import org.springframework.data.mapping.model.ParameterValueProvider;
5454
import org.springframework.data.neo4j.core.convert.Neo4jConversionService;
55+
import org.springframework.data.neo4j.core.mapping.callback.EventSupport;
5556
import org.springframework.data.neo4j.core.schema.TargetNode;
5657
import org.springframework.data.util.ReflectionUtils;
5758
import org.springframework.data.util.TypeInformation;
@@ -72,6 +73,8 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter {
7273
private final NodeDescriptionStore nodeDescriptionStore;
7374
private final Neo4jConversionService conversionService;
7475

76+
private final EventSupport eventSupport;
77+
7578
private final KnownObjects knownObjects = new KnownObjects();
7679

7780
private final Type nodeType;
@@ -80,8 +83,8 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter {
8083
private final Type listType;
8184
private final Map<String, Collection<Node>> labelNodeCache = new HashMap<>();
8285

83-
DefaultNeo4jEntityConverter(EntityInstantiators entityInstantiators, Neo4jConversionService conversionService,
84-
NodeDescriptionStore nodeDescriptionStore, TypeSystem typeSystem) {
86+
DefaultNeo4jEntityConverter(EntityInstantiators entityInstantiators, NodeDescriptionStore nodeDescriptionStore,
87+
Neo4jConversionService conversionService, EventSupport eventSupport, TypeSystem typeSystem) {
8588

8689
Assert.notNull(entityInstantiators, "EntityInstantiators must not be null!");
8790
Assert.notNull(conversionService, "Neo4jConversionService must not be null!");
@@ -91,6 +94,7 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter {
9194
this.entityInstantiators = entityInstantiators;
9295
this.conversionService = conversionService;
9396
this.nodeDescriptionStore = nodeDescriptionStore;
97+
this.eventSupport = eventSupport;
9498

9599
this.nodeType = typeSystem.NODE();
96100
this.relationshipType = typeSystem.RELATIONSHIP();
@@ -287,6 +291,7 @@ private <ET> ET map(MapAccessor queryResult, Neo4jPersistentEntity<ET> nodeDescr
287291

288292
PersistentPropertyAccessor<ET> propertyAccessor = concreteNodeDescription.getPropertyAccessor(instance);
289293
ET bean = propertyAccessor.getBean();
294+
bean = eventSupport.maybeCallAfterConvert(bean, concreteNodeDescription, queryResult);
290295

291296
// save final state of the bean
292297
knownObjects.storeObject(internalId, bean);

src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContext.java

Lines changed: 148 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,18 @@
1616
package org.springframework.data.neo4j.core.mapping;
1717

1818
import java.lang.reflect.Constructor;
19+
import java.lang.reflect.Field;
1920
import java.lang.reflect.Method;
2021
import java.lang.reflect.Modifier;
2122
import java.lang.reflect.ParameterizedType;
2223
import java.lang.reflect.Type;
24+
import java.security.AccessController;
25+
import java.security.PrivilegedAction;
26+
import java.util.Arrays;
27+
import java.util.Collections;
2328
import java.util.HashMap;
29+
import java.util.HashSet;
30+
import java.util.LinkedHashSet;
2431
import java.util.List;
2532
import java.util.Locale;
2633
import java.util.Map;
@@ -40,8 +47,11 @@
4047
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
4148
import org.springframework.context.ApplicationContext;
4249
import org.springframework.core.GenericTypeResolver;
50+
import org.springframework.core.KotlinDetector;
51+
import org.springframework.core.annotation.AnnotationUtils;
4352
import org.springframework.data.mapping.MappingException;
4453
import org.springframework.data.mapping.PersistentEntity;
54+
import org.springframework.data.mapping.callback.EntityCallbacks;
4555
import org.springframework.data.mapping.context.AbstractMappingContext;
4656
import org.springframework.data.mapping.model.EntityInstantiator;
4757
import org.springframework.data.mapping.model.EntityInstantiators;
@@ -52,8 +62,10 @@
5262
import org.springframework.data.neo4j.core.convert.Neo4jConversions;
5363
import org.springframework.data.neo4j.core.convert.Neo4jPersistentPropertyConverter;
5464
import org.springframework.data.neo4j.core.convert.Neo4jPersistentPropertyConverterFactory;
65+
import org.springframework.data.neo4j.core.mapping.callback.EventSupport;
5566
import org.springframework.data.neo4j.core.schema.IdGenerator;
5667
import org.springframework.data.neo4j.core.schema.Node;
68+
import org.springframework.data.neo4j.core.schema.PostLoad;
5769
import org.springframework.data.util.TypeInformation;
5870
import org.springframework.lang.Nullable;
5971
import org.springframework.util.ReflectionUtils;
@@ -78,6 +90,8 @@ public final class Neo4jMappingContext extends AbstractMappingContext<Neo4jPersi
7890
*/
7991
private static final EntityInstantiators INSTANTIATORS = new EntityInstantiators();
8092

93+
private static final Set<Class<?>> VOID_TYPES = new HashSet<>(Arrays.asList(Void.class, void.class));
94+
8195
/**
8296
* A map of fallback id generators, that have not been added to the application context
8397
*/
@@ -95,6 +109,10 @@ public final class Neo4jMappingContext extends AbstractMappingContext<Neo4jPersi
95109

96110
private final Neo4jConversionService conversionService;
97111

112+
private final Map<Neo4jPersistentEntity, Set<MethodHolder>> postLoadMethods = new ConcurrentHashMap<>();
113+
114+
private EventSupport eventSupport;
115+
98116
private @Nullable AutowireCapableBeanFactory beanFactory;
99117

100118
private boolean strict = false;
@@ -133,12 +151,14 @@ public Neo4jMappingContext(Neo4jConversions neo4jConversions, @Nullable TypeSyst
133151

134152
this.conversionService = new DefaultNeo4jConversionService(neo4jConversions);
135153
this.typeSystem = typeSystem == null ? InternalTypeSystem.TYPE_SYSTEM : typeSystem;
154+
this.eventSupport = EventSupport.useExistingCallbacks(this, EntityCallbacks.create());
136155

137156
super.setSimpleTypeHolder(neo4jConversions.getSimpleTypeHolder());
138157
}
139158

140159
public Neo4jEntityConverter getEntityConverter() {
141-
return new DefaultNeo4jEntityConverter(INSTANTIATORS, conversionService, nodeDescriptionStore, typeSystem);
160+
return new DefaultNeo4jEntityConverter(INSTANTIATORS, nodeDescriptionStore, conversionService, eventSupport,
161+
typeSystem);
142162
}
143163

144164
public Neo4jConversionService getConversionService() {
@@ -170,25 +190,33 @@ protected <T> Neo4jPersistentEntity<?> createPersistentEntity(TypeInformation<T>
170190
if (!newEntity.describesInterface()) {
171191
if (this.nodeDescriptionStore.containsKey(primaryLabel)) {
172192

173-
Neo4jPersistentEntity existingEntity = (Neo4jPersistentEntity) this.nodeDescriptionStore.get(primaryLabel);
174-
if (!existingEntity.getTypeInformation().getRawTypeInformation().equals(typeInformation.getRawTypeInformation())) {
175-
String message = String.format(Locale.ENGLISH, "The schema already contains a node description under the primary label %s", primaryLabel);
193+
Neo4jPersistentEntity existingEntity = (Neo4jPersistentEntity) this.nodeDescriptionStore.get(
194+
primaryLabel);
195+
if (!existingEntity.getTypeInformation().getRawTypeInformation()
196+
.equals(typeInformation.getRawTypeInformation())) {
197+
String message = String.format(Locale.ENGLISH,
198+
"The schema already contains a node description under the primary label %s", primaryLabel);
176199
throw new MappingException(message);
177200
}
178201
}
179202

180203
if (this.nodeDescriptionStore.containsValue(newEntity)) {
181-
Optional<String> label = this.nodeDescriptionStore.entrySet().stream().filter(e -> e.getValue().equals(newEntity)).map(Map.Entry::getKey).findFirst();
204+
Optional<String> label = this.nodeDescriptionStore.entrySet().stream()
205+
.filter(e -> e.getValue().equals(newEntity)).map(Map.Entry::getKey).findFirst();
182206

183-
String message = String.format(Locale.ENGLISH, "The schema already contains description %s under the primary label %s", newEntity, label.orElse("n/a"));
207+
String message = String.format(Locale.ENGLISH,
208+
"The schema already contains description %s under the primary label %s", newEntity,
209+
label.orElse("n/a"));
184210
throw new MappingException(message);
185211
}
186212

187213
NodeDescription<?> existingDescription = this.getNodeDescription(newEntity.getUnderlyingClass());
188214
if (existingDescription != null) {
189215

190216
if (!existingDescription.getPrimaryLabel().equals(newEntity.getPrimaryLabel())) {
191-
String message = String.format(Locale.ENGLISH, "The schema already contains description with the underlying class %s under the primary label %s", newEntity.getUnderlyingClass().getName(), existingDescription.getPrimaryLabel());
217+
String message = String.format(Locale.ENGLISH,
218+
"The schema already contains description with the underlying class %s under the primary label %s",
219+
newEntity.getUnderlyingClass().getName(), existingDescription.getPrimaryLabel());
192220
throw new MappingException(message);
193221
}
194222
}
@@ -221,7 +249,7 @@ private static boolean isValidParentNode(@Nullable Class<?> parentClass) {
221249

222250
// Either a concrete class explicitly annotated as Node or an abstract class
223251
return Modifier.isAbstract(parentClass.getModifiers()) ||
224-
parentClass.isAnnotationPresent(Node.class);
252+
parentClass.isAnnotationPresent(Node.class);
225253
}
226254

227255
/*
@@ -339,18 +367,21 @@ Constructor<?> findConstructor(Class<?> clazz, Class<?>... parameterTypes) {
339367
}
340368
}
341369

342-
private <T extends Neo4jPersistentPropertyConverterFactory> T getOrCreateConverterFactoryOfType(Class<T> converterFactoryType) {
370+
private <T extends Neo4jPersistentPropertyConverterFactory> T getOrCreateConverterFactoryOfType(
371+
Class<T> converterFactoryType) {
343372

344373
return converterFactoryType.cast(this.converterFactories.computeIfAbsent(converterFactoryType, t -> {
345374
Constructor<?> optionalConstructor;
346375
optionalConstructor = findConstructor(t, BeanFactory.class, Neo4jConversionService.class);
347376
if (optionalConstructor != null) {
348-
return t.cast(BeanUtils.instantiateClass(optionalConstructor, this.beanFactory, this.conversionService));
377+
return t.cast(
378+
BeanUtils.instantiateClass(optionalConstructor, this.beanFactory, this.conversionService));
349379
}
350380

351381
optionalConstructor = findConstructor(t, Neo4jConversionService.class, BeanFactory.class);
352382
if (optionalConstructor != null) {
353-
return t.cast(BeanUtils.instantiateClass(optionalConstructor, this.beanFactory, this.conversionService));
383+
return t.cast(
384+
BeanUtils.instantiateClass(optionalConstructor, this.beanFactory, this.conversionService));
354385
}
355386

356387
optionalConstructor = findConstructor(t, BeanFactory.class);
@@ -379,8 +410,10 @@ Neo4jPersistentPropertyConverter<?> getOptionalCustomConversionsFor(Neo4jPersist
379410
}
380411

381412
ConvertWith convertWith = persistentProperty.getRequiredAnnotation(ConvertWith.class);
382-
Neo4jPersistentPropertyConverterFactory persistentPropertyConverterFactory = this.getOrCreateConverterFactoryOfType(convertWith.converterFactory());
383-
Neo4jPersistentPropertyConverter<?> customConverter = persistentPropertyConverterFactory.getPropertyConverterFor(persistentProperty);
413+
Neo4jPersistentPropertyConverterFactory persistentPropertyConverterFactory = this.getOrCreateConverterFactoryOfType(
414+
convertWith.converterFactory());
415+
Neo4jPersistentPropertyConverter<?> customConverter = persistentPropertyConverterFactory.getPropertyConverterFor(
416+
persistentProperty);
384417

385418
boolean forCollection = false;
386419
if (persistentProperty.isCollectionLike()) {
@@ -406,20 +439,22 @@ Neo4jPersistentPropertyConverter<?> getOptionalCustomConversionsFor(Neo4jPersist
406439
persistentProperty.getType().equals(((ParameterizedType) propertyType).getRawType());
407440
}
408441

409-
return new NullSafeNeo4jPersistentPropertyConverter<>(customConverter, persistentProperty.isComposite(), forCollection);
442+
return new NullSafeNeo4jPersistentPropertyConverter<>(customConverter, persistentProperty.isComposite(),
443+
forCollection);
410444
}
411445

412446
@Override
413447
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
414448
super.setApplicationContext(applicationContext);
415449

416450
this.beanFactory = applicationContext.getAutowireCapableBeanFactory();
451+
this.eventSupport = EventSupport.discoverCallbacks(this, this.beanFactory);
417452
}
418453

419454
public CreateRelationshipStatementHolder createStatement(Neo4jPersistentEntity<?> neo4jPersistentEntity,
420-
NestedRelationshipContext relationshipContext,
421-
Object relatedValue,
422-
boolean isNewRelationship) {
455+
NestedRelationshipContext relationshipContext,
456+
Object relatedValue,
457+
boolean isNewRelationship) {
423458

424459
if (relationshipContext.hasRelationshipWithProperties()) {
425460
MappingSupport.RelationshipPropertiesWithEntityHolder relatedValueEntityHolder =
@@ -436,25 +471,30 @@ public CreateRelationshipStatementHolder createStatement(Neo4jPersistentEntity<?
436471

437472
String dynamicRelationshipType = null;
438473
if (relationshipContext.getRelationship().isDynamic()) {
439-
TypeInformation<?> keyType = relationshipContext.getInverse().getTypeInformation().getRequiredComponentType();
474+
TypeInformation<?> keyType = relationshipContext.getInverse().getTypeInformation()
475+
.getRequiredComponentType();
440476
Object key = ((Map.Entry) relatedValue).getKey();
441-
dynamicRelationshipType = conversionService.writeValue(key, keyType, relationshipContext.getInverse().getOptionalConverter()).asString();
477+
dynamicRelationshipType = conversionService.writeValue(key, keyType,
478+
relationshipContext.getInverse().getOptionalConverter()).asString();
442479
}
443480
return createStatementForRelationShipWithProperties(
444481
neo4jPersistentEntity, relationshipContext,
445482
dynamicRelationshipType, relatedValueEntityHolder, isNewRelationship
446483
);
447484
} else {
448-
return createStatementForRelationshipWithoutProperties(neo4jPersistentEntity, relationshipContext, relatedValue);
485+
return createStatementForRelationshipWithoutProperties(neo4jPersistentEntity, relationshipContext,
486+
relatedValue);
449487
}
450488
}
451489

452-
private CreateRelationshipStatementHolder createStatementForRelationShipWithProperties(Neo4jPersistentEntity<?> neo4jPersistentEntity,
490+
private CreateRelationshipStatementHolder createStatementForRelationShipWithProperties(
491+
Neo4jPersistentEntity<?> neo4jPersistentEntity,
453492
NestedRelationshipContext relationshipContext, @Nullable String dynamicRelationshipType,
454-
MappingSupport.RelationshipPropertiesWithEntityHolder relatedValue, boolean isNewRelationship) {
493+
MappingSupport.RelationshipPropertiesWithEntityHolder relatedValue, boolean isNewRelationship) {
455494

456495
Statement relationshipCreationQuery = CypherGenerator.INSTANCE.prepareSaveOfRelationshipWithProperties(
457-
neo4jPersistentEntity, relationshipContext.getRelationship(), isNewRelationship, dynamicRelationshipType);
496+
neo4jPersistentEntity, relationshipContext.getRelationship(), isNewRelationship,
497+
dynamicRelationshipType);
458498

459499
Map<String, Object> propMap = new HashMap<>();
460500
// write relationship properties
@@ -481,4 +521,89 @@ private CreateRelationshipStatementHolder createStatementForRelationshipWithoutP
481521
neo4jPersistentEntity, relationshipContext.getRelationship(), relationshipType);
482522
return new CreateRelationshipStatementHolder(relationshipCreationQuery);
483523
}
524+
525+
/**
526+
* Executes all post load methods of the given instance.
527+
*
528+
* @param entity The entity definition
529+
* @param instance The instance whose post load methods should be executed
530+
* @param <T> Type of the entity
531+
* @return The instance
532+
*/
533+
public <T> T invokePostLoad(Neo4jPersistentEntity<T> entity, T instance) {
534+
535+
getPostLoadMethods(entity).forEach(methodHolder -> methodHolder.invoke(instance));
536+
return instance;
537+
}
538+
539+
Set<MethodHolder> getPostLoadMethods(Neo4jPersistentEntity<?> entity) {
540+
return this.postLoadMethods.computeIfAbsent(entity, Neo4jMappingContext::computePostLoadMethods);
541+
}
542+
543+
private static Set<MethodHolder> computePostLoadMethods(Neo4jPersistentEntity<?> entity) {
544+
545+
Set<MethodHolder> postLoadMethods = new LinkedHashSet<>();
546+
ReflectionUtils.MethodFilter isValidPostLoad = method -> {
547+
int modifiers = method.getModifiers();
548+
return !Modifier.isStatic(modifiers) && method.getParameterCount() == 0 && VOID_TYPES.contains(
549+
method.getReturnType()) && AnnotationUtils.findAnnotation(method, PostLoad.class) != null;
550+
};
551+
Class<?> underlyingClass = entity.getUnderlyingClass();
552+
ReflectionUtils.doWithMethods(underlyingClass, method -> postLoadMethods.add(new MethodHolder(method, null)),
553+
isValidPostLoad);
554+
if (KotlinDetector.isKotlinType(underlyingClass)) {
555+
ReflectionUtils.doWithFields(underlyingClass, field -> {
556+
ReflectionUtils.doWithMethods(field.getType(),
557+
method -> postLoadMethods.add(new MethodHolder(method, field)), isValidPostLoad);
558+
}, field -> field.isSynthetic() && field.getName().startsWith("$$delegate_"));
559+
}
560+
561+
return Collections.unmodifiableSet(postLoadMethods);
562+
}
563+
564+
static class MethodHolder {
565+
566+
private final Method method;
567+
568+
@Nullable
569+
private final Field delegate;
570+
571+
MethodHolder(Method method, @Nullable Field delegate) {
572+
this.method = method;
573+
this.delegate = delegate;
574+
}
575+
576+
Method getMethod() {
577+
return method;
578+
}
579+
580+
String getName() {
581+
return method.getName();
582+
}
583+
584+
void invoke(Object instance) {
585+
Method methodToInvoke = AccessController.doPrivileged((PrivilegedAction<Method>) () -> {
586+
if (!method.isAccessible()) {
587+
method.setAccessible(true);
588+
}
589+
return method;
590+
});
591+
ReflectionUtils.invokeMethod(methodToInvoke, getInstanceOrDelegate(instance, delegate));
592+
}
593+
594+
static Object getInstanceOrDelegate(Object instance, @Nullable Field delegateHolder) {
595+
if (delegateHolder == null) {
596+
return instance;
597+
} else {
598+
return AccessController.doPrivileged((PrivilegedAction<Object>) () -> {
599+
try {
600+
delegateHolder.setAccessible(true);
601+
return delegateHolder.get(instance);
602+
} catch (IllegalAccessException e) {
603+
throw new RuntimeException(e);
604+
}
605+
});
606+
}
607+
}
608+
}
484609
}

0 commit comments

Comments
 (0)