Skip to content

Commit 74e6bfb

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 6ddbba8 commit 74e6bfb

File tree

8 files changed

+414
-23
lines changed

8 files changed

+414
-23
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
@@ -61,6 +61,8 @@ final class DefaultNeo4jPersistentProperty extends AnnotationBasedPersistentProp
6161

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

64+
private final @Nullable PersistentPropertyCharacteristics optionalCharacteristics;
65+
6466
/**
6567
* Creates a new {@link AnnotationBasedPersistentProperty}.
6668
*
@@ -69,7 +71,7 @@ final class DefaultNeo4jPersistentProperty extends AnnotationBasedPersistentProp
6971
* @param simpleTypeHolder type holder
7072
*/
7173
DefaultNeo4jPersistentProperty(Property property, PersistentEntity<?, Neo4jPersistentProperty> owner,
72-
Neo4jMappingContext mappingContext, SimpleTypeHolder simpleTypeHolder) {
74+
Neo4jMappingContext mappingContext, SimpleTypeHolder simpleTypeHolder, @Nullable PersistentPropertyCharacteristics optionalCharacteristics) {
7375

7476
super(property, owner, simpleTypeHolder);
7577
this.mappingContext = mappingContext;
@@ -101,6 +103,8 @@ final class DefaultNeo4jPersistentProperty extends AnnotationBasedPersistentProp
101103

102104
return this.mappingContext.getOptionalCustomConversionsFor(this);
103105
});
106+
107+
this.optionalCharacteristics = optionalCharacteristics;
104108
}
105109

106110
@Override
@@ -308,7 +312,17 @@ static String deriveRelationshipType(String name) {
308312
@Override
309313
public boolean isReadOnly() {
310314

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

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

+90-20
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
import org.springframework.data.neo4j.core.schema.IdGenerator;
6767
import org.springframework.data.neo4j.core.schema.Node;
6868
import org.springframework.data.neo4j.core.schema.PostLoad;
69+
import org.springframework.data.util.Lazy;
6970
import org.springframework.data.util.TypeInformation;
7071
import org.springframework.lang.Nullable;
7172
import org.springframework.util.ReflectionUtils;
@@ -117,26 +118,67 @@ public final class Neo4jMappingContext extends AbstractMappingContext<Neo4jPersi
117118

118119
private boolean strict = false;
119120

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

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

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

127-
this(neo4jConversions, null);
176+
this(new Builder());
128177
}
129178

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;
179+
public Neo4jMappingContext(Neo4jConversions neo4jConversions) {
180+
181+
this(new Builder(neo4jConversions, null, null));
140182
}
141183

142184
/**
@@ -145,17 +187,40 @@ public void setStrict(boolean strict) {
145187
*
146188
* @param neo4jConversions The conversions to be used
147189
* @param typeSystem The current drivers type system. If this is null, we use the default one without accessing the driver.
190+
* @deprecated Use {@link Neo4jMappingContext#builder()}
148191
*/
149192
@API(status = API.Status.INTERNAL, since = "6.0")
193+
@Deprecated
150194
public Neo4jMappingContext(Neo4jConversions neo4jConversions, @Nullable TypeSystem typeSystem) {
195+
this(new Builder(neo4jConversions, typeSystem, null));
196+
}
197+
198+
private Neo4jMappingContext(Builder builder) {
151199

152-
this.conversionService = new DefaultNeo4jConversionService(neo4jConversions);
153-
this.typeSystem = typeSystem == null ? InternalTypeSystem.TYPE_SYSTEM : typeSystem;
200+
this.conversionService = new DefaultNeo4jConversionService(builder.neo4jConversions);
201+
this.typeSystem = builder.typeSystem == null ? InternalTypeSystem.TYPE_SYSTEM : builder.typeSystem;
154202
this.eventSupport = EventSupport.useExistingCallbacks(this, EntityCallbacks.create());
155203

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

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

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

266336
@Override
@@ -278,9 +348,9 @@ public NodeDescription<?> getNodeDescription(Class<?> underlyingClass) {
278348
@Nullable
279349
public Neo4jPersistentEntity<?> getPersistentEntity(TypeInformation<?> typeInformation) {
280350

281-
NodeDescription<?> existingDescription = this.doGetPersistentEntity(typeInformation);
351+
Neo4jPersistentEntity<?> existingDescription = this.doGetPersistentEntity(typeInformation);
282352
if (existingDescription != null) {
283-
return (Neo4jPersistentEntity<?>) existingDescription;
353+
return existingDescription;
284354
}
285355
return super.getPersistentEntity(typeInformation);
286356
}
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)