Skip to content

Commit 5c7ab59

Browse files
GH-2640 - Introduce API for dynamic configuration of transient properties. (#2645)
This change introduces the concept of `PersistentPropertyCharacteristics` and `PersistentPropertyCharacteristicsProvider`. The latter can be registered implicitly as a `@Bean` with the mapping context or via a custom mapping context. It allows checking the properties and their owner for indicators like type and so on to treat them as transient or read only. Examples have been added to the `Neo4jMappingContextTest` and `PersistentPropertyCharacteristicsIT`. This closes #2640.
1 parent dc69615 commit 5c7ab59

File tree

8 files changed

+414
-24
lines changed

8 files changed

+414
-24
lines changed

src/main/java/org/springframework/data/neo4j/config/Neo4jCdiConfigurationSupport.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ public Neo4jClient neo4jClient(@SuppressWarnings("CdiInjectionPointsInspection")
8989
@Produces @Singleton
9090
public Neo4jMappingContext neo4jMappingContext(@SuppressWarnings("CdiInjectionPointsInspection") Driver driver, @Any Instance<Neo4jConversions> neo4JConversions) {
9191

92-
return new Neo4jMappingContext(resolve(neo4JConversions), driver.defaultTypeSystem());
92+
return Neo4jMappingContext.builder().withNeo4jConversions(resolve(neo4JConversions)).withTypeSystem(driver.defaultTypeSystem()).build();
9393
}
9494

9595
@Produces @Singleton

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

+15-1
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ final class DefaultNeo4jPersistentProperty extends AnnotationBasedPersistentProp
6060

6161
private final Lazy<Neo4jPersistentPropertyConverter<?>> customConversion;
6262

63+
private final @Nullable PersistentPropertyCharacteristics optionalCharacteristics;
64+
6365
/**
6466
* Creates a new {@link AnnotationBasedPersistentProperty}.
6567
*
@@ -68,7 +70,7 @@ final class DefaultNeo4jPersistentProperty extends AnnotationBasedPersistentProp
6870
* @param simpleTypeHolder type holder
6971
*/
7072
DefaultNeo4jPersistentProperty(Property property, PersistentEntity<?, Neo4jPersistentProperty> owner,
71-
Neo4jMappingContext mappingContext, SimpleTypeHolder simpleTypeHolder) {
73+
Neo4jMappingContext mappingContext, SimpleTypeHolder simpleTypeHolder, @Nullable PersistentPropertyCharacteristics optionalCharacteristics) {
7274

7375
super(property, owner, simpleTypeHolder);
7476
this.mappingContext = mappingContext;
@@ -100,6 +102,8 @@ final class DefaultNeo4jPersistentProperty extends AnnotationBasedPersistentProp
100102

101103
return this.mappingContext.getOptionalCustomConversionsFor(this);
102104
});
105+
106+
this.optionalCharacteristics = optionalCharacteristics;
103107
}
104108

105109
@Override
@@ -307,7 +311,17 @@ static String deriveRelationshipType(String name) {
307311
@Override
308312
public boolean isReadOnly() {
309313

314+
if (optionalCharacteristics != null && optionalCharacteristics.isReadOnly() != null) {
315+
return Boolean.TRUE.equals(optionalCharacteristics.isReadOnly());
316+
}
317+
310318
Class<org.springframework.data.neo4j.core.schema.Property> typeOfAnnotation = org.springframework.data.neo4j.core.schema.Property.class;
311319
return isAnnotationPresent(ReadOnlyProperty.class) || (isAnnotationPresent(typeOfAnnotation) && getRequiredAnnotation(typeOfAnnotation).readOnly());
312320
}
321+
322+
@Override
323+
public boolean isTransient() {
324+
return this.optionalCharacteristics == null || optionalCharacteristics.isTransient() == null ?
325+
super.isTransient() : Boolean.TRUE.equals(optionalCharacteristics.isTransient());
326+
}
313327
}

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

+90-21
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@
3838

3939
import org.apiguardian.api.API;
4040
import org.neo4j.cypherdsl.core.Statement;
41-
import org.neo4j.driver.internal.types.InternalTypeSystem;
4241
import org.neo4j.driver.types.TypeSystem;
4342
import org.springframework.beans.BeanUtils;
4443
import org.springframework.beans.BeansException;
@@ -66,6 +65,7 @@
6665
import org.springframework.data.neo4j.core.schema.IdGenerator;
6766
import org.springframework.data.neo4j.core.schema.Node;
6867
import org.springframework.data.neo4j.core.schema.PostLoad;
68+
import org.springframework.data.util.Lazy;
6969
import org.springframework.data.util.TypeInformation;
7070
import org.springframework.lang.Nullable;
7171
import org.springframework.util.ReflectionUtils;
@@ -117,26 +117,67 @@ public final class Neo4jMappingContext extends AbstractMappingContext<Neo4jPersi
117117

118118
private boolean strict = false;
119119

120-
public Neo4jMappingContext() {
120+
private final Lazy<PersistentPropertyCharacteristicsProvider> propertyCharacteristicsProvider;
121+
122+
/**
123+
* A builder for creating custom instances of a {@link Neo4jMappingContext}.
124+
* @since 6.3.7
125+
*/
126+
public static class Builder {
127+
128+
private Neo4jConversions neo4jConversions;
129+
130+
@Nullable
131+
private TypeSystem typeSystem;
121132

122-
this(new Neo4jConversions());
133+
@Nullable
134+
private PersistentPropertyCharacteristicsProvider persistentPropertyCharacteristicsProvider;
135+
136+
private Builder() {
137+
this(new Neo4jConversions(), null, null);
138+
}
139+
140+
private Builder(Neo4jConversions neo4jConversions, @Nullable TypeSystem typeSystem, @Nullable PersistentPropertyCharacteristicsProvider persistentPropertyCharacteristicsProvider) {
141+
this.neo4jConversions = neo4jConversions;
142+
this.typeSystem = typeSystem;
143+
this.persistentPropertyCharacteristicsProvider = persistentPropertyCharacteristicsProvider;
144+
}
145+
146+
@SuppressWarnings("HiddenField")
147+
public Builder withNeo4jConversions(@Nullable Neo4jConversions neo4jConversions) {
148+
this.neo4jConversions = neo4jConversions;
149+
return this;
150+
}
151+
152+
@SuppressWarnings("HiddenField")
153+
public Builder withPersistentPropertyCharacteristicsProvider(@Nullable PersistentPropertyCharacteristicsProvider persistentPropertyCharacteristicsProvider) {
154+
this.persistentPropertyCharacteristicsProvider = persistentPropertyCharacteristicsProvider;
155+
return this;
156+
}
157+
158+
@SuppressWarnings("HiddenField")
159+
public Builder withTypeSystem(@Nullable TypeSystem typeSystem) {
160+
this.typeSystem = typeSystem;
161+
return this;
162+
}
163+
164+
public Neo4jMappingContext build() {
165+
return new Neo4jMappingContext(this);
166+
}
123167
}
124168

125-
public Neo4jMappingContext(Neo4jConversions neo4jConversions) {
169+
public static Builder builder() {
170+
return new Builder();
171+
}
172+
173+
public Neo4jMappingContext() {
126174

127-
this(neo4jConversions, null);
175+
this(new Builder());
128176
}
129177

130-
/**
131-
* We need to set the context to non-strict in case we must dynamically add parent classes. As there is no
132-
* way to access the original value without reflection, we track its change.
133-
*
134-
* @param strict The new value for the strict setting.
135-
*/
136-
@Override
137-
public void setStrict(boolean strict) {
138-
super.setStrict(strict);
139-
this.strict = strict;
178+
public Neo4jMappingContext(Neo4jConversions neo4jConversions) {
179+
180+
this(new Builder(neo4jConversions, null, null));
140181
}
141182

142183
/**
@@ -145,17 +186,40 @@ public void setStrict(boolean strict) {
145186
*
146187
* @param neo4jConversions The conversions to be used
147188
* @param typeSystem The current drivers type system. If this is null, we use the default one without accessing the driver.
189+
* @deprecated Use {@link Neo4jMappingContext#builder()}
148190
*/
149191
@API(status = API.Status.INTERNAL, since = "6.0")
192+
@Deprecated(since = "6.3.7", forRemoval = true)
150193
public Neo4jMappingContext(Neo4jConversions neo4jConversions, @Nullable TypeSystem typeSystem) {
194+
this(new Builder(neo4jConversions, typeSystem, null));
195+
}
196+
197+
private Neo4jMappingContext(Builder builder) {
151198

152-
this.conversionService = new DefaultNeo4jConversionService(neo4jConversions);
153-
this.typeSystem = typeSystem == null ? InternalTypeSystem.TYPE_SYSTEM : typeSystem;
199+
this.conversionService = new DefaultNeo4jConversionService(builder.neo4jConversions);
200+
this.typeSystem = builder.typeSystem == null ? TypeSystem.getDefault() : builder.typeSystem;
154201
this.eventSupport = EventSupport.useExistingCallbacks(this, EntityCallbacks.create());
155202

156-
super.setSimpleTypeHolder(neo4jConversions.getSimpleTypeHolder());
203+
super.setSimpleTypeHolder(builder.neo4jConversions.getSimpleTypeHolder());
204+
205+
PersistentPropertyCharacteristicsProvider characteristicsProvider = builder.persistentPropertyCharacteristicsProvider;
206+
this.propertyCharacteristicsProvider = Lazy.of(() -> characteristicsProvider != null || this.beanFactory == null ?
207+
characteristicsProvider : this.beanFactory.getBeanProvider(PersistentPropertyCharacteristicsProvider.class).getIfUnique());
157208
}
158209

210+
/**
211+
* We need to set the context to non-strict in case we must dynamically add parent classes. As there is no
212+
* way to access the original value without reflection, we track its change.
213+
*
214+
* @param strict The new value for the strict setting.
215+
*/
216+
@Override
217+
public void setStrict(boolean strict) {
218+
super.setStrict(strict);
219+
this.strict = strict;
220+
}
221+
222+
159223
public Neo4jEntityConverter getEntityConverter() {
160224
return new DefaultNeo4jEntityConverter(INSTANTIATORS, nodeDescriptionStore, conversionService, eventSupport,
161225
typeSystem);
@@ -260,7 +324,12 @@ private static boolean isValidParentNode(@Nullable Class<?> parentClass) {
260324
protected Neo4jPersistentProperty createPersistentProperty(Property property, Neo4jPersistentEntity<?> owner,
261325
SimpleTypeHolder simpleTypeHolder) {
262326

263-
return new DefaultNeo4jPersistentProperty(property, owner, this, simpleTypeHolder);
327+
PersistentPropertyCharacteristics optionalCharacteristics = this.propertyCharacteristicsProvider
328+
.getOptional()
329+
.flatMap(provider -> Optional.ofNullable(provider.apply(property, owner)))
330+
.orElse(null);
331+
332+
return new DefaultNeo4jPersistentProperty(property, owner, this, simpleTypeHolder, optionalCharacteristics);
264333
}
265334

266335
@Override
@@ -278,9 +347,9 @@ public NodeDescription<?> getNodeDescription(Class<?> underlyingClass) {
278347
@Nullable
279348
public Neo4jPersistentEntity<?> getPersistentEntity(TypeInformation<?> typeInformation) {
280349

281-
NodeDescription<?> existingDescription = this.doGetPersistentEntity(typeInformation);
350+
Neo4jPersistentEntity<?> existingDescription = this.doGetPersistentEntity(typeInformation);
282351
if (existingDescription != null) {
283-
return (Neo4jPersistentEntity<?>) existingDescription;
352+
return existingDescription;
284353
}
285354
return super.getPersistentEntity(typeInformation);
286355
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*
2+
* Copyright 2011-2023 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;
17+
18+
import static org.apiguardian.api.API.Status.STABLE;
19+
20+
import org.apiguardian.api.API;
21+
import org.springframework.lang.Nullable;
22+
23+
/**
24+
* The characteristics of a {@link Neo4jPersistentProperty} can diverge from what is by default derived from the annotated
25+
* classes. Diverging characteristics are requested by the {@link Neo4jMappingContext} prior to creating a persistent property.
26+
* Additional providers of characteristics may be registered with the mapping context.
27+
*
28+
* @author Michael J. Simons
29+
* @soundtrack Metallica - Kill 'Em All
30+
* @since 6.3.7
31+
*/
32+
@API(status = STABLE, since = "6.3.7")
33+
public interface PersistentPropertyCharacteristics {
34+
35+
/**
36+
* @return {@literal null} to leave the defaults, {@literal true} or {@literal false} otherwise
37+
*/
38+
@Nullable
39+
default Boolean isTransient() {
40+
return null;
41+
}
42+
43+
/**
44+
* @return {@literal null} to leave the defaults, {@literal true} or {@literal false} otherwise
45+
*/
46+
@Nullable
47+
default Boolean isReadOnly() {
48+
return null;
49+
}
50+
51+
/**
52+
* @return Characteristics applying the defaults
53+
*/
54+
static PersistentPropertyCharacteristics useDefaults() {
55+
return new PersistentPropertyCharacteristics() {
56+
};
57+
}
58+
59+
/**
60+
* @return Characteristics to treat a property as transient
61+
*/
62+
static PersistentPropertyCharacteristics treatAsTransient() {
63+
return new PersistentPropertyCharacteristics() {
64+
@Override
65+
public Boolean isTransient() {
66+
return true;
67+
}
68+
};
69+
}
70+
71+
/**
72+
* @return Characteristics to treat a property as transient
73+
*/
74+
static PersistentPropertyCharacteristics treatAsReadOnly() {
75+
return new PersistentPropertyCharacteristics() {
76+
@Override
77+
public Boolean isReadOnly() {
78+
return true;
79+
}
80+
};
81+
}
82+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Copyright 2011-2023 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;
17+
18+
import static org.apiguardian.api.API.Status.STABLE;
19+
20+
import java.util.function.BiFunction;
21+
22+
import org.apiguardian.api.API;
23+
import org.springframework.data.mapping.model.Property;
24+
25+
/**
26+
* An instance of such a provider can be registered as a Spring bean and will be consulted by the {@link Neo4jMappingContext}
27+
* prior to creating and populating {@link Neo4jPersistentProperty persistent properties}.
28+
*
29+
* @author Michael J. Simons
30+
* @soundtrack Metallica - Kill 'Em All
31+
* @since 6.3.7
32+
*/
33+
@API(status = STABLE, since = "6.3.7")
34+
public interface PersistentPropertyCharacteristicsProvider extends BiFunction<Property, Neo4jPersistentEntity<?>, PersistentPropertyCharacteristics> {
35+
}

src/test/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContextTest.java

+31
Original file line numberDiff line numberDiff line change
@@ -634,6 +634,37 @@ void hierarchyMustBeConsistentlyReportedWithIntermediateConcreteClasses() throws
634634
assertThat(children).containsExactly("B2", "B2a", "B3", "B3a");
635635
}
636636

637+
@Test
638+
void characteristicsShouldBeApplied() {
639+
640+
Neo4jMappingContext neo4jMappingContext1 = Neo4jMappingContext.builder().withPersistentPropertyCharacteristicsProvider((property, owner) -> {
641+
if (owner.getUnderlyingClass().equals(UserNode.class)) {
642+
if (property.getName().equals("name")) {
643+
return PersistentPropertyCharacteristics.treatAsTransient();
644+
} else if (property.getName().equals("first_name")) {
645+
return PersistentPropertyCharacteristics.treatAsReadOnly();
646+
}
647+
}
648+
if (property.getType().equals(ConvertibleType.class)) {
649+
return PersistentPropertyCharacteristics.treatAsTransient();
650+
}
651+
652+
return PersistentPropertyCharacteristics.useDefaults();
653+
}).build();
654+
Neo4jMappingContext neo4jMappingContext2 = Neo4jMappingContext.builder().build();
655+
656+
Neo4jPersistentEntity<?> userEntity = neo4jMappingContext1.getPersistentEntity(UserNode.class);
657+
assertThat(userEntity.getPersistentProperty("name")).isNull(); // Transient properties won't materialize
658+
assertThat(userEntity.getRequiredPersistentProperty("first_name").isTransient()).isFalse();
659+
assertThat(userEntity.getRequiredPersistentProperty("first_name").isReadOnly()).isTrue();
660+
661+
Neo4jPersistentEntity<?> entityWithConvertible = neo4jMappingContext1.getPersistentEntity(EntityWithConvertibleProperty.class);
662+
assertThat(entityWithConvertible.getPersistentProperty("convertibleType")).isNull();
663+
664+
entityWithConvertible = neo4jMappingContext2.getPersistentEntity(EntityWithConvertibleProperty.class);
665+
assertThat(entityWithConvertible.getPersistentProperty("convertibleType")).isNotNull();
666+
}
667+
637668
@Test // GH-2574
638669
void hierarchyMustBeConsistentlyReported() throws ClassNotFoundException {
639670

0 commit comments

Comments
 (0)