Skip to content

Commit ebf8b36

Browse files
GH-2640 - Introduce API for dynamic configuration of transient properties.
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 ebf8b36

File tree

6 files changed

+410
-21
lines changed

6 files changed

+410
-21
lines changed

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

+88-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
/**
@@ -148,14 +190,35 @@ public void setStrict(boolean strict) {
148190
*/
149191
@API(status = API.Status.INTERNAL, since = "6.0")
150192
public Neo4jMappingContext(Neo4jConversions neo4jConversions, @Nullable TypeSystem typeSystem) {
193+
this(new Builder(neo4jConversions, typeSystem, null));
194+
}
195+
196+
private Neo4jMappingContext(Builder builder) {
151197

152-
this.conversionService = new DefaultNeo4jConversionService(neo4jConversions);
153-
this.typeSystem = typeSystem == null ? InternalTypeSystem.TYPE_SYSTEM : typeSystem;
198+
this.conversionService = new DefaultNeo4jConversionService(builder.neo4jConversions);
199+
this.typeSystem = builder.typeSystem == null ? InternalTypeSystem.TYPE_SYSTEM : builder.typeSystem;
154200
this.eventSupport = EventSupport.useExistingCallbacks(this, EntityCallbacks.create());
155201

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

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

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

266334
@Override
@@ -278,9 +346,9 @@ public NodeDescription<?> getNodeDescription(Class<?> underlyingClass) {
278346
@Nullable
279347
public Neo4jPersistentEntity<?> getPersistentEntity(TypeInformation<?> typeInformation) {
280348

281-
NodeDescription<?> existingDescription = this.doGetPersistentEntity(typeInformation);
349+
Neo4jPersistentEntity<?> existingDescription = this.doGetPersistentEntity(typeInformation);
282350
if (existingDescription != null) {
283-
return (Neo4jPersistentEntity<?>) existingDescription;
351+
return existingDescription;
284352
}
285353
return super.getPersistentEntity(typeInformation);
286354
}
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)