Skip to content

GH-2640 - Introduce API for dynamic configuration of transient properties. #2645

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jan 5, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ final class DefaultNeo4jPersistentProperty extends AnnotationBasedPersistentProp

private final Lazy<Neo4jPersistentPropertyConverter<?>> customConversion;

private final @Nullable PersistentPropertyCharacteristics optionalCharacteristics;

/**
* Creates a new {@link AnnotationBasedPersistentProperty}.
*
Expand All @@ -69,7 +71,7 @@ final class DefaultNeo4jPersistentProperty extends AnnotationBasedPersistentProp
* @param simpleTypeHolder type holder
*/
DefaultNeo4jPersistentProperty(Property property, PersistentEntity<?, Neo4jPersistentProperty> owner,
Neo4jMappingContext mappingContext, SimpleTypeHolder simpleTypeHolder) {
Neo4jMappingContext mappingContext, SimpleTypeHolder simpleTypeHolder, @Nullable PersistentPropertyCharacteristics optionalCharacteristics) {

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

return this.mappingContext.getOptionalCustomConversionsFor(this);
});

this.optionalCharacteristics = optionalCharacteristics;
}

@Override
Expand Down Expand Up @@ -308,7 +312,17 @@ static String deriveRelationshipType(String name) {
@Override
public boolean isReadOnly() {

if (optionalCharacteristics != null && optionalCharacteristics.isReadOnly() != null) {
return Boolean.TRUE.equals(optionalCharacteristics.isReadOnly());
}

Class<org.springframework.data.neo4j.core.schema.Property> typeOfAnnotation = org.springframework.data.neo4j.core.schema.Property.class;
return isAnnotationPresent(ReadOnlyProperty.class) || (isAnnotationPresent(typeOfAnnotation) && getRequiredAnnotation(typeOfAnnotation).readOnly());
}

@Override
public boolean isTransient() {
return this.optionalCharacteristics == null || optionalCharacteristics.isTransient() == null ?
super.isTransient() : Boolean.TRUE.equals(optionalCharacteristics.isTransient());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
import org.springframework.data.neo4j.core.schema.IdGenerator;
import org.springframework.data.neo4j.core.schema.Node;
import org.springframework.data.neo4j.core.schema.PostLoad;
import org.springframework.data.util.Lazy;
import org.springframework.data.util.TypeInformation;
import org.springframework.lang.Nullable;
import org.springframework.util.ReflectionUtils;
Expand Down Expand Up @@ -117,26 +118,67 @@ public final class Neo4jMappingContext extends AbstractMappingContext<Neo4jPersi

private boolean strict = false;

public Neo4jMappingContext() {
private final Lazy<PersistentPropertyCharacteristicsProvider> propertyCharacteristicsProvider;

/**
* A builder for creating custom instances of a {@link Neo4jMappingContext}.
* @since 6.3.7
*/
public static class Builder {

private Neo4jConversions neo4jConversions;

@Nullable
private TypeSystem typeSystem;

this(new Neo4jConversions());
@Nullable
private PersistentPropertyCharacteristicsProvider persistentPropertyCharacteristicsProvider;

private Builder() {
this(new Neo4jConversions(), null, null);
}

private Builder(Neo4jConversions neo4jConversions, @Nullable TypeSystem typeSystem, @Nullable PersistentPropertyCharacteristicsProvider persistentPropertyCharacteristicsProvider) {
this.neo4jConversions = neo4jConversions;
this.typeSystem = typeSystem;
this.persistentPropertyCharacteristicsProvider = persistentPropertyCharacteristicsProvider;
}

@SuppressWarnings("HiddenField")
public Builder withNeo4jConversions(@Nullable Neo4jConversions neo4jConversions) {
this.neo4jConversions = neo4jConversions;
return this;
}

@SuppressWarnings("HiddenField")
public Builder withPersistentPropertyCharacteristicsProvider(@Nullable PersistentPropertyCharacteristicsProvider persistentPropertyCharacteristicsProvider) {
this.persistentPropertyCharacteristicsProvider = persistentPropertyCharacteristicsProvider;
return this;
}

@SuppressWarnings("HiddenField")
public Builder withTypeSystem(@Nullable TypeSystem typeSystem) {
this.typeSystem = typeSystem;
return this;
}

public Neo4jMappingContext build() {
return new Neo4jMappingContext(this);
}
}

public Neo4jMappingContext(Neo4jConversions neo4jConversions) {
public static Builder builder() {
return new Builder();
}

public Neo4jMappingContext() {

this(neo4jConversions, null);
this(new Builder());
}

/**
* We need to set the context to non-strict in case we must dynamically add parent classes. As there is no
* way to access the original value without reflection, we track its change.
*
* @param strict The new value for the strict setting.
*/
@Override
public void setStrict(boolean strict) {
super.setStrict(strict);
this.strict = strict;
public Neo4jMappingContext(Neo4jConversions neo4jConversions) {

this(new Builder(neo4jConversions, null, null));
}

/**
Expand All @@ -148,14 +190,35 @@ public void setStrict(boolean strict) {
*/
@API(status = API.Status.INTERNAL, since = "6.0")
public Neo4jMappingContext(Neo4jConversions neo4jConversions, @Nullable TypeSystem typeSystem) {
this(new Builder(neo4jConversions, typeSystem, null));
}

private Neo4jMappingContext(Builder builder) {

this.conversionService = new DefaultNeo4jConversionService(neo4jConversions);
this.typeSystem = typeSystem == null ? InternalTypeSystem.TYPE_SYSTEM : typeSystem;
this.conversionService = new DefaultNeo4jConversionService(builder.neo4jConversions);
this.typeSystem = builder.typeSystem == null ? InternalTypeSystem.TYPE_SYSTEM : builder.typeSystem;
this.eventSupport = EventSupport.useExistingCallbacks(this, EntityCallbacks.create());

super.setSimpleTypeHolder(neo4jConversions.getSimpleTypeHolder());
super.setSimpleTypeHolder(builder.neo4jConversions.getSimpleTypeHolder());

PersistentPropertyCharacteristicsProvider characteristicsProvider = builder.persistentPropertyCharacteristicsProvider;
this.propertyCharacteristicsProvider = Lazy.of(() -> characteristicsProvider != null || this.beanFactory == null ?
characteristicsProvider : this.beanFactory.getBeanProvider(PersistentPropertyCharacteristicsProvider.class).getIfUnique());
}

/**
* We need to set the context to non-strict in case we must dynamically add parent classes. As there is no
* way to access the original value without reflection, we track its change.
*
* @param strict The new value for the strict setting.
*/
@Override
public void setStrict(boolean strict) {
super.setStrict(strict);
this.strict = strict;
}


public Neo4jEntityConverter getEntityConverter() {
return new DefaultNeo4jEntityConverter(INSTANTIATORS, nodeDescriptionStore, conversionService, eventSupport,
typeSystem);
Expand Down Expand Up @@ -260,7 +323,12 @@ private static boolean isValidParentNode(@Nullable Class<?> parentClass) {
protected Neo4jPersistentProperty createPersistentProperty(Property property, Neo4jPersistentEntity<?> owner,
SimpleTypeHolder simpleTypeHolder) {

return new DefaultNeo4jPersistentProperty(property, owner, this, simpleTypeHolder);
PersistentPropertyCharacteristics optionalCharacteristics = this.propertyCharacteristicsProvider
.getOptional()
.flatMap(provider -> Optional.ofNullable(provider.apply(property, owner)))
.orElse(null);

return new DefaultNeo4jPersistentProperty(property, owner, this, simpleTypeHolder, optionalCharacteristics);
}

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

NodeDescription<?> existingDescription = this.doGetPersistentEntity(typeInformation);
Neo4jPersistentEntity<?> existingDescription = this.doGetPersistentEntity(typeInformation);
if (existingDescription != null) {
return (Neo4jPersistentEntity<?>) existingDescription;
return existingDescription;
}
return super.getPersistentEntity(typeInformation);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* Copyright 2011-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.neo4j.core.mapping;

import static org.apiguardian.api.API.Status.STABLE;

import org.apiguardian.api.API;
import org.springframework.lang.Nullable;

/**
* The characteristics of a {@link Neo4jPersistentProperty} can diverge from what is by default derived from the annotated
* classes. Diverging characteristics are requested by the {@link Neo4jMappingContext} prior to creating a persistent property.
* Additional providers of characteristics may be registered with the mapping context.
*
* @author Michael J. Simons
* @soundtrack Metallica - Kill 'Em All
* @since 6.3.7
*/
@API(status = STABLE, since = "6.3.7")
public interface PersistentPropertyCharacteristics {

/**
* @return {@literal null} to leave the defaults, {@literal true} or {@literal false} otherwise
*/
@Nullable
default Boolean isTransient() {
return null;
}

/**
* @return {@literal null} to leave the defaults, {@literal true} or {@literal false} otherwise
*/
@Nullable
default Boolean isReadOnly() {
return null;
}

/**
* @return Characteristics applying the defaults
*/
static PersistentPropertyCharacteristics useDefaults() {
return new PersistentPropertyCharacteristics() {
};
}

/**
* @return Characteristics to treat a property as transient
*/
static PersistentPropertyCharacteristics treatAsTransient() {
return new PersistentPropertyCharacteristics() {
@Override
public Boolean isTransient() {
return true;
}
};
}

/**
* @return Characteristics to treat a property as transient
*/
static PersistentPropertyCharacteristics treatAsReadOnly() {
return new PersistentPropertyCharacteristics() {
@Override
public Boolean isReadOnly() {
return true;
}
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright 2011-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.neo4j.core.mapping;

import static org.apiguardian.api.API.Status.STABLE;

import java.util.function.BiFunction;

import org.apiguardian.api.API;
import org.springframework.data.mapping.model.Property;

/**
* An instance of such a provider can be registered as a Spring bean and will be consulted by the {@link Neo4jMappingContext}
* prior to creating and populating {@link Neo4jPersistentProperty persistent properties}.
*
* @author Michael J. Simons
* @soundtrack Metallica - Kill 'Em All
* @since 6.3.7
*/
@API(status = STABLE, since = "6.3.7")
public interface PersistentPropertyCharacteristicsProvider extends BiFunction<Property, Neo4jPersistentEntity<?>, PersistentPropertyCharacteristics> {
}
Original file line number Diff line number Diff line change
Expand Up @@ -634,6 +634,37 @@ void hierarchyMustBeConsistentlyReportedWithIntermediateConcreteClasses() throws
assertThat(children).containsExactly("B2", "B2a", "B3", "B3a");
}

@Test
void characteristicsShouldBeApplied() {

Neo4jMappingContext neo4jMappingContext1 = Neo4jMappingContext.builder().withPersistentPropertyCharacteristicsProvider((property, owner) -> {
if (owner.getUnderlyingClass().equals(UserNode.class)) {
if (property.getName().equals("name")) {
return PersistentPropertyCharacteristics.treatAsTransient();
} else if (property.getName().equals("first_name")) {
return PersistentPropertyCharacteristics.treatAsReadOnly();
}
}
if (property.getType().equals(ConvertibleType.class)) {
return PersistentPropertyCharacteristics.treatAsTransient();
}

return PersistentPropertyCharacteristics.useDefaults();
}).build();
Neo4jMappingContext neo4jMappingContext2 = Neo4jMappingContext.builder().build();

Neo4jPersistentEntity<?> userEntity = neo4jMappingContext1.getPersistentEntity(UserNode.class);
assertThat(userEntity.getPersistentProperty("name")).isNull(); // Transient properties won't materialize
assertThat(userEntity.getRequiredPersistentProperty("first_name").isTransient()).isFalse();
assertThat(userEntity.getRequiredPersistentProperty("first_name").isReadOnly()).isTrue();

Neo4jPersistentEntity<?> entityWithConvertible = neo4jMappingContext1.getPersistentEntity(EntityWithConvertibleProperty.class);
assertThat(entityWithConvertible.getPersistentProperty("convertibleType")).isNull();

entityWithConvertible = neo4jMappingContext2.getPersistentEntity(EntityWithConvertibleProperty.class);
assertThat(entityWithConvertible.getPersistentProperty("convertibleType")).isNotNull();
}

@Test // GH-2574
void hierarchyMustBeConsistentlyReported() throws ClassNotFoundException {

Expand Down
Loading