Skip to content

Commit 8a2ac27

Browse files
GH-2499 - Add support for AfterConvert events and @PostLoad annotated methods.
This closes #2499.
1 parent 5047546 commit 8a2ac27

File tree

13 files changed

+611
-52
lines changed

13 files changed

+611
-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: 136 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,16 @@
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.util.Arrays;
25+
import java.util.Collections;
2326
import java.util.HashMap;
27+
import java.util.HashSet;
28+
import java.util.LinkedHashSet;
2429
import java.util.List;
2530
import java.util.Locale;
2631
import java.util.Map;
@@ -40,8 +45,11 @@
4045
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
4146
import org.springframework.context.ApplicationContext;
4247
import org.springframework.core.GenericTypeResolver;
48+
import org.springframework.core.KotlinDetector;
49+
import org.springframework.core.annotation.AnnotationUtils;
4350
import org.springframework.data.mapping.MappingException;
4451
import org.springframework.data.mapping.PersistentEntity;
52+
import org.springframework.data.mapping.callback.EntityCallbacks;
4553
import org.springframework.data.mapping.context.AbstractMappingContext;
4654
import org.springframework.data.mapping.model.EntityInstantiator;
4755
import org.springframework.data.mapping.model.EntityInstantiators;
@@ -52,8 +60,10 @@
5260
import org.springframework.data.neo4j.core.convert.Neo4jConversions;
5361
import org.springframework.data.neo4j.core.convert.Neo4jPersistentPropertyConverter;
5462
import org.springframework.data.neo4j.core.convert.Neo4jPersistentPropertyConverterFactory;
63+
import org.springframework.data.neo4j.core.mapping.callback.EventSupport;
5564
import org.springframework.data.neo4j.core.schema.IdGenerator;
5665
import org.springframework.data.neo4j.core.schema.Node;
66+
import org.springframework.data.neo4j.core.schema.PostLoad;
5767
import org.springframework.data.util.TypeInformation;
5868
import org.springframework.lang.Nullable;
5969
import org.springframework.util.ReflectionUtils;
@@ -78,6 +88,8 @@ public final class Neo4jMappingContext extends AbstractMappingContext<Neo4jPersi
7888
*/
7989
private static final EntityInstantiators INSTANTIATORS = new EntityInstantiators();
8090

91+
private static final Set<Class<?>> VOID_TYPES = new HashSet<>(Arrays.asList(Void.class, void.class));
92+
8193
/**
8294
* A map of fallback id generators, that have not been added to the application context
8395
*/
@@ -95,6 +107,10 @@ public final class Neo4jMappingContext extends AbstractMappingContext<Neo4jPersi
95107

96108
private final Neo4jConversionService conversionService;
97109

110+
private final Map<Neo4jPersistentEntity, Set<MethodHolder>> postLoadMethods = new ConcurrentHashMap<>();
111+
112+
private EventSupport eventSupport;
113+
98114
private @Nullable AutowireCapableBeanFactory beanFactory;
99115

100116
private boolean strict = false;
@@ -133,12 +149,14 @@ public Neo4jMappingContext(Neo4jConversions neo4jConversions, @Nullable TypeSyst
133149

134150
this.conversionService = new DefaultNeo4jConversionService(neo4jConversions);
135151
this.typeSystem = typeSystem == null ? InternalTypeSystem.TYPE_SYSTEM : typeSystem;
152+
this.eventSupport = EventSupport.useExistingCallbacks(this, EntityCallbacks.create());
136153

137154
super.setSimpleTypeHolder(neo4jConversions.getSimpleTypeHolder());
138155
}
139156

140157
public Neo4jEntityConverter getEntityConverter() {
141-
return new DefaultNeo4jEntityConverter(INSTANTIATORS, conversionService, nodeDescriptionStore, typeSystem);
158+
return new DefaultNeo4jEntityConverter(INSTANTIATORS, nodeDescriptionStore, conversionService, eventSupport,
159+
typeSystem);
142160
}
143161

144162
public Neo4jConversionService getConversionService() {
@@ -170,25 +188,33 @@ protected <T> Neo4jPersistentEntity<?> createPersistentEntity(TypeInformation<T>
170188
if (!newEntity.describesInterface()) {
171189
if (this.nodeDescriptionStore.containsKey(primaryLabel)) {
172190

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);
191+
Neo4jPersistentEntity existingEntity = (Neo4jPersistentEntity) this.nodeDescriptionStore.get(
192+
primaryLabel);
193+
if (!existingEntity.getTypeInformation().getRawTypeInformation()
194+
.equals(typeInformation.getRawTypeInformation())) {
195+
String message = String.format(Locale.ENGLISH,
196+
"The schema already contains a node description under the primary label %s", primaryLabel);
176197
throw new MappingException(message);
177198
}
178199
}
179200

180201
if (this.nodeDescriptionStore.containsValue(newEntity)) {
181-
Optional<String> label = this.nodeDescriptionStore.entrySet().stream().filter(e -> e.getValue().equals(newEntity)).map(Map.Entry::getKey).findFirst();
202+
Optional<String> label = this.nodeDescriptionStore.entrySet().stream()
203+
.filter(e -> e.getValue().equals(newEntity)).map(Map.Entry::getKey).findFirst();
182204

183-
String message = String.format(Locale.ENGLISH, "The schema already contains description %s under the primary label %s", newEntity, label.orElse("n/a"));
205+
String message = String.format(Locale.ENGLISH,
206+
"The schema already contains description %s under the primary label %s", newEntity,
207+
label.orElse("n/a"));
184208
throw new MappingException(message);
185209
}
186210

187211
NodeDescription<?> existingDescription = this.getNodeDescription(newEntity.getUnderlyingClass());
188212
if (existingDescription != null) {
189213

190214
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());
215+
String message = String.format(Locale.ENGLISH,
216+
"The schema already contains description with the underlying class %s under the primary label %s",
217+
newEntity.getUnderlyingClass().getName(), existingDescription.getPrimaryLabel());
192218
throw new MappingException(message);
193219
}
194220
}
@@ -221,7 +247,7 @@ private static boolean isValidParentNode(@Nullable Class<?> parentClass) {
221247

222248
// Either a concrete class explicitly annotated as Node or an abstract class
223249
return Modifier.isAbstract(parentClass.getModifiers()) ||
224-
parentClass.isAnnotationPresent(Node.class);
250+
parentClass.isAnnotationPresent(Node.class);
225251
}
226252

227253
/*
@@ -339,18 +365,21 @@ Constructor<?> findConstructor(Class<?> clazz, Class<?>... parameterTypes) {
339365
}
340366
}
341367

342-
private <T extends Neo4jPersistentPropertyConverterFactory> T getOrCreateConverterFactoryOfType(Class<T> converterFactoryType) {
368+
private <T extends Neo4jPersistentPropertyConverterFactory> T getOrCreateConverterFactoryOfType(
369+
Class<T> converterFactoryType) {
343370

344371
return converterFactoryType.cast(this.converterFactories.computeIfAbsent(converterFactoryType, t -> {
345372
Constructor<?> optionalConstructor;
346373
optionalConstructor = findConstructor(t, BeanFactory.class, Neo4jConversionService.class);
347374
if (optionalConstructor != null) {
348-
return t.cast(BeanUtils.instantiateClass(optionalConstructor, this.beanFactory, this.conversionService));
375+
return t.cast(
376+
BeanUtils.instantiateClass(optionalConstructor, this.beanFactory, this.conversionService));
349377
}
350378

351379
optionalConstructor = findConstructor(t, Neo4jConversionService.class, BeanFactory.class);
352380
if (optionalConstructor != null) {
353-
return t.cast(BeanUtils.instantiateClass(optionalConstructor, this.beanFactory, this.conversionService));
381+
return t.cast(
382+
BeanUtils.instantiateClass(optionalConstructor, this.beanFactory, this.conversionService));
354383
}
355384

356385
optionalConstructor = findConstructor(t, BeanFactory.class);
@@ -379,8 +408,10 @@ Neo4jPersistentPropertyConverter<?> getOptionalCustomConversionsFor(Neo4jPersist
379408
}
380409

381410
ConvertWith convertWith = persistentProperty.getRequiredAnnotation(ConvertWith.class);
382-
Neo4jPersistentPropertyConverterFactory persistentPropertyConverterFactory = this.getOrCreateConverterFactoryOfType(convertWith.converterFactory());
383-
Neo4jPersistentPropertyConverter<?> customConverter = persistentPropertyConverterFactory.getPropertyConverterFor(persistentProperty);
411+
Neo4jPersistentPropertyConverterFactory persistentPropertyConverterFactory = this.getOrCreateConverterFactoryOfType(
412+
convertWith.converterFactory());
413+
Neo4jPersistentPropertyConverter<?> customConverter = persistentPropertyConverterFactory.getPropertyConverterFor(
414+
persistentProperty);
384415

385416
boolean forCollection = false;
386417
if (persistentProperty.isCollectionLike()) {
@@ -406,20 +437,22 @@ Neo4jPersistentPropertyConverter<?> getOptionalCustomConversionsFor(Neo4jPersist
406437
persistentProperty.getType().equals(((ParameterizedType) propertyType).getRawType());
407438
}
408439

409-
return new NullSafeNeo4jPersistentPropertyConverter<>(customConverter, persistentProperty.isComposite(), forCollection);
440+
return new NullSafeNeo4jPersistentPropertyConverter<>(customConverter, persistentProperty.isComposite(),
441+
forCollection);
410442
}
411443

412444
@Override
413445
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
414446
super.setApplicationContext(applicationContext);
415447

416448
this.beanFactory = applicationContext.getAutowireCapableBeanFactory();
449+
this.eventSupport = EventSupport.discoverCallbacks(this, this.beanFactory);
417450
}
418451

419452
public CreateRelationshipStatementHolder createStatement(Neo4jPersistentEntity<?> neo4jPersistentEntity,
420-
NestedRelationshipContext relationshipContext,
421-
Object relatedValue,
422-
boolean isNewRelationship) {
453+
NestedRelationshipContext relationshipContext,
454+
Object relatedValue,
455+
boolean isNewRelationship) {
423456

424457
if (relationshipContext.hasRelationshipWithProperties()) {
425458
MappingSupport.RelationshipPropertiesWithEntityHolder relatedValueEntityHolder =
@@ -436,25 +469,30 @@ public CreateRelationshipStatementHolder createStatement(Neo4jPersistentEntity<?
436469

437470
String dynamicRelationshipType = null;
438471
if (relationshipContext.getRelationship().isDynamic()) {
439-
TypeInformation<?> keyType = relationshipContext.getInverse().getTypeInformation().getRequiredComponentType();
472+
TypeInformation<?> keyType = relationshipContext.getInverse().getTypeInformation()
473+
.getRequiredComponentType();
440474
Object key = ((Map.Entry) relatedValue).getKey();
441-
dynamicRelationshipType = conversionService.writeValue(key, keyType, relationshipContext.getInverse().getOptionalConverter()).asString();
475+
dynamicRelationshipType = conversionService.writeValue(key, keyType,
476+
relationshipContext.getInverse().getOptionalConverter()).asString();
442477
}
443478
return createStatementForRelationShipWithProperties(
444479
neo4jPersistentEntity, relationshipContext,
445480
dynamicRelationshipType, relatedValueEntityHolder, isNewRelationship
446481
);
447482
} else {
448-
return createStatementForRelationshipWithoutProperties(neo4jPersistentEntity, relationshipContext, relatedValue);
483+
return createStatementForRelationshipWithoutProperties(neo4jPersistentEntity, relationshipContext,
484+
relatedValue);
449485
}
450486
}
451487

452-
private CreateRelationshipStatementHolder createStatementForRelationShipWithProperties(Neo4jPersistentEntity<?> neo4jPersistentEntity,
488+
private CreateRelationshipStatementHolder createStatementForRelationShipWithProperties(
489+
Neo4jPersistentEntity<?> neo4jPersistentEntity,
453490
NestedRelationshipContext relationshipContext, @Nullable String dynamicRelationshipType,
454-
MappingSupport.RelationshipPropertiesWithEntityHolder relatedValue, boolean isNewRelationship) {
491+
MappingSupport.RelationshipPropertiesWithEntityHolder relatedValue, boolean isNewRelationship) {
455492

456493
Statement relationshipCreationQuery = CypherGenerator.INSTANCE.prepareSaveOfRelationshipWithProperties(
457-
neo4jPersistentEntity, relationshipContext.getRelationship(), isNewRelationship, dynamicRelationshipType);
494+
neo4jPersistentEntity, relationshipContext.getRelationship(), isNewRelationship,
495+
dynamicRelationshipType);
458496

459497
Map<String, Object> propMap = new HashMap<>();
460498
// write relationship properties
@@ -481,4 +519,79 @@ private CreateRelationshipStatementHolder createStatementForRelationshipWithoutP
481519
neo4jPersistentEntity, relationshipContext.getRelationship(), relationshipType);
482520
return new CreateRelationshipStatementHolder(relationshipCreationQuery);
483521
}
522+
523+
/**
524+
* Executes all post load methods of the given instance.
525+
*
526+
* @param entity The entity definition
527+
* @param instance The instance whose post load methods should be executed
528+
* @param <T> Type of the entity
529+
* @return The instance
530+
*/
531+
public <T> T invokePostLoad(Neo4jPersistentEntity<T> entity, T instance) {
532+
533+
getPostLoadMethods(entity).forEach(methodHolder -> methodHolder.invoke(instance));
534+
return instance;
535+
}
536+
537+
Set<MethodHolder> getPostLoadMethods(Neo4jPersistentEntity<?> entity) {
538+
return this.postLoadMethods.computeIfAbsent(entity, Neo4jMappingContext::computePostLoadMethods);
539+
}
540+
541+
private static Set<MethodHolder> computePostLoadMethods(Neo4jPersistentEntity<?> entity) {
542+
543+
Set<MethodHolder> postLoadMethods = new LinkedHashSet<>();
544+
ReflectionUtils.MethodFilter isValidPostLoad = method -> {
545+
int modifiers = method.getModifiers();
546+
return !Modifier.isStatic(modifiers) && method.getParameterCount() == 0 && VOID_TYPES.contains(
547+
method.getReturnType()) && AnnotationUtils.findAnnotation(method, PostLoad.class) != null;
548+
};
549+
Class<?> underlyingClass = entity.getUnderlyingClass();
550+
ReflectionUtils.doWithMethods(underlyingClass, method -> postLoadMethods.add(new MethodHolder(method, null)),
551+
isValidPostLoad);
552+
if (KotlinDetector.isKotlinType(underlyingClass)) {
553+
ReflectionUtils.doWithFields(underlyingClass, field -> {
554+
ReflectionUtils.doWithMethods(field.getType(),
555+
method -> postLoadMethods.add(new MethodHolder(method, field)), isValidPostLoad);
556+
}, field -> field.isSynthetic() && field.getName().startsWith("$$delegate_"));
557+
}
558+
559+
return Collections.unmodifiableSet(postLoadMethods);
560+
}
561+
562+
static class MethodHolder {
563+
564+
private final Method method;
565+
566+
@Nullable
567+
private final Field delegate;
568+
569+
MethodHolder(Method method, @Nullable Field delegate) {
570+
this.method = method;
571+
this.delegate = delegate;
572+
}
573+
574+
String getName() {
575+
return method.getName();
576+
}
577+
578+
void invoke(Object instance) {
579+
580+
method.setAccessible(true);
581+
ReflectionUtils.invokeMethod(method, getInstanceOrDelegate(instance, delegate));
582+
}
583+
584+
static Object getInstanceOrDelegate(Object instance, @Nullable Field delegateHolder) {
585+
if (delegateHolder == null) {
586+
return instance;
587+
} else {
588+
try {
589+
delegateHolder.setAccessible(true);
590+
return delegateHolder.get(instance);
591+
} catch (IllegalAccessException e) {
592+
throw new RuntimeException(e);
593+
}
594+
}
595+
}
596+
}
484597
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* Copyright 2011-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.neo4j.core.mapping.callback;
17+
18+
import static org.apiguardian.api.API.Status.STABLE;
19+
20+
import org.apiguardian.api.API;
21+
import org.neo4j.driver.types.MapAccessor;
22+
import org.springframework.data.mapping.callback.EntityCallback;
23+
import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity;
24+
25+
/**
26+
* A callback that can be used to modify an instance of a {@link Neo4jPersistentEntity} after it has been converted:
27+
* That is, when a Neo4j record has been fully processed and the entity and all its associations have been processed.
28+
* <p>
29+
* There is no reactive variant for this callback. It is safe to use this one for both reactive and imperative workloads.
30+
*
31+
* @author Michael J. Simons
32+
* @param <T> The type of the entity.
33+
* @since 6.3.0
34+
*/
35+
@FunctionalInterface
36+
@API(status = STABLE, since = "6.3.0")
37+
public interface AfterConvertCallback<T> extends EntityCallback<T> {
38+
39+
/**
40+
* Invoked after converting a Neo4j record (aka after hydrating an entity).
41+
*
42+
* @param instance The instance as hydrated by the {@link org.springframework.data.neo4j.core.mapping.Neo4jEntityConverter}.
43+
* @param entity The entity definition
44+
* @param source The Neo4j record that was used to hydrate the instance.
45+
* @return the domain object used further
46+
*/
47+
T onAfterConvert(T instance, Neo4jPersistentEntity<T> entity, MapAccessor source);
48+
}

0 commit comments

Comments
 (0)