From 7fcacb79911630e07f43963e644e1b1aec815907 Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Tue, 3 Jun 2025 06:57:22 +0200 Subject: [PATCH] refactor: Migrate to JSpecify annotations for nullability constraints. (#3016) Replaces all deprecated Spring annotations with JSpecify annotations, and includes a full refactor so that the parent build profile `-Pnullaway` compiles and works proper. Closes #2987 Signed-off-by: Michael Simons --- pom.xml | 104 +++--- ...agedTypesBeanRegistrationAotProcessor.java | 6 +- .../data/neo4j/aot/Neo4jRuntimeHints.java | 2 +- .../data/neo4j/aot/package-info.java | 4 + .../neo4j/config/AbstractNeo4jConfig.java | 15 +- .../config/AbstractReactiveNeo4jConfig.java | 2 +- .../config/Neo4jConfigurationSupport.java | 14 - .../data/neo4j/config/Neo4jEntityScanner.java | 8 +- .../data/neo4j/config/package-info.java | 4 +- .../data/neo4j/core/DatabaseSelection.java | 5 +- .../data/neo4j/core/DefaultNeo4jClient.java | 23 +- .../core/DefaultReactiveNeo4jClient.java | 24 +- .../data/neo4j/core/DynamicLabels.java | 6 +- .../data/neo4j/core/FluentFindOperation.java | 6 +- .../neo4j/core/FluentOperationSupport.java | 13 +- .../data/neo4j/core/NamedParameters.java | 4 +- .../data/neo4j/core/Neo4jClient.java | 16 +- .../data/neo4j/core/Neo4jOperations.java | 4 +- .../Neo4jPersistenceExceptionTranslator.java | 2 + .../data/neo4j/core/Neo4jTemplate.java | 313 +++++++++--------- .../data/neo4j/core/PreparedQuery.java | 17 +- .../neo4j/core/PropertyFilterSupport.java | 31 +- .../core/ReactiveFluentFindOperation.java | 5 +- .../core/ReactiveFluentOperationSupport.java | 10 +- .../data/neo4j/core/ReactiveNeo4jClient.java | 12 +- .../neo4j/core/ReactiveNeo4jOperations.java | 5 +- .../neo4j/core/ReactiveNeo4jTemplate.java | 272 +++++++-------- .../data/neo4j/core/RelationshipHandler.java | 26 +- .../data/neo4j/core/ResultSummaries.java | 2 +- .../core/SingleValueMappingFunction.java | 6 +- .../data/neo4j/core/TemplateSupport.java | 49 ++- .../data/neo4j/core/UserSelection.java | 5 +- .../neo4j/core/convert/AdditionalTypes.java | 19 +- .../data/neo4j/core/convert/ConvertWith.java | 7 +- ...o4jPersistentPropertyConverterFactory.java | 4 +- .../core/convert/Neo4jConversionService.java | 13 +- .../Neo4jPersistentPropertyConverter.java | 4 +- ...Neo4jPersistentPropertyToMapConverter.java | 2 +- .../core/convert/TemporalAmountConverter.java | 6 +- .../data/neo4j/core/convert/package-info.java | 4 +- .../CreateRelationshipStatementHolder.java | 3 +- .../neo4j/core/mapping/CypherGenerator.java | 101 +++--- .../DefaultNeo4jConversionService.java | 12 +- .../mapping/DefaultNeo4jEntityConverter.java | 207 ++++++------ .../mapping/DefaultNeo4jIsNewStrategy.java | 10 +- .../mapping/DefaultNeo4jPersistentEntity.java | 43 ++- .../DefaultNeo4jPersistentProperty.java | 55 +-- .../DefaultRelationshipDescription.java | 11 +- .../mapping/DtoInstantiatingConverter.java | 18 +- .../EntityFromDtoInstantiatingConverter.java | 13 +- .../neo4j/core/mapping/IdDescription.java | 23 +- .../neo4j/core/mapping/IdentitySupport.java | 12 +- .../neo4j/core/mapping/MappingSupport.java | 6 +- .../core/mapping/Neo4jMappingContext.java | 51 ++- .../core/mapping/Neo4jPersistentEntity.java | 14 + .../core/mapping/Neo4jPersistentProperty.java | 5 +- .../mapping/NestedRelationshipContext.java | 20 +- ...tedRelationshipProcessingStateMachine.java | 105 +++--- .../mapping/NoRootNodeMappingException.java | 24 +- .../neo4j/core/mapping/NodeDescription.java | 8 +- .../core/mapping/NodeDescriptionStore.java | 14 +- ...lSafeNeo4jPersistentPropertyConverter.java | 5 +- .../PersistentPropertyCharacteristics.java | 2 +- .../neo4j/core/mapping/PropertyFilter.java | 6 +- .../neo4j/core/mapping/PropertyTraverser.java | 2 +- .../core/mapping/RelationshipDescription.java | 15 +- .../data/neo4j/core/mapping/Schema.java | 10 +- .../core/mapping/UnknownEntityException.java | 4 + .../core/mapping/callback/EventSupport.java | 7 +- .../core/mapping/callback/package-info.java | 3 - .../data/neo4j/core/mapping/package-info.java | 6 +- .../data/neo4j/core/package-info.java | 4 +- .../neo4j/core/schema/CompositeProperty.java | 26 +- .../data/neo4j/core/schema/package-info.java | 4 +- .../support/BookmarkManagerReference.java | 12 +- .../data/neo4j/core/support/DateLong.java | 11 +- .../data/neo4j/core/support/DateString.java | 11 +- .../data/neo4j/core/support/UserAgent.java | 2 +- .../data/neo4j/core/support/package-info.java | 4 +- .../transaction/DefaultBookmarkManager.java | 2 +- .../transaction/Neo4jBookmarkManager.java | 6 +- .../Neo4jBookmarksUpdatedEvent.java | 9 +- .../transaction/Neo4jTransactionHolder.java | 2 +- .../transaction/Neo4jTransactionManager.java | 60 ++-- .../transaction/Neo4jTransactionUtils.java | 4 +- .../ReactiveDefaultBookmarkManager.java | 2 +- .../ReactiveNeo4jTransactionHolder.java | 2 +- .../ReactiveNeo4jTransactionManager.java | 46 ++- .../neo4j/core/transaction/package-info.java | 4 +- .../neo4j/repository/NoResultException.java | 6 +- ...Neo4jRepositoryConfigurationExtension.java | 2 +- ...Neo4jRepositoryConfigurationExtension.java | 2 +- .../neo4j/repository/config/package-info.java | 4 +- .../data/neo4j/repository/package-info.java | 4 +- .../repository/query/AbstractNeo4jQuery.java | 15 +- .../query/AbstractReactiveNeo4jQuery.java | 16 +- .../repository/query/CypherAdapterUtils.java | 24 +- .../repository/query/CypherQueryCreator.java | 16 +- .../repository/query/CypherdslBasedQuery.java | 5 +- .../query/FetchableFluentQueryByExample.java | 20 +- .../FetchableFluentQueryByPredicate.java | 4 +- .../repository/query/FluentQuerySupport.java | 6 +- .../query/Neo4jNestedMapEntityWriter.java | 12 +- .../repository/query/Neo4jQueryMethod.java | 15 +- .../repository/query/Neo4jQuerySupport.java | 15 +- .../repository/query/Neo4jSpelSupport.java | 17 +- .../query/OptionalUnwrappingConverter.java | 2 + .../repository/query/PartTreeNeo4jQuery.java | 4 +- .../neo4j/repository/query/Predicate.java | 31 +- .../repository/query/PropertyPathWrapper.java | 2 +- .../repository/query/QueryFragments.java | 44 ++- .../query/QueryFragmentsAndParameters.java | 114 ++++--- .../query/ReactiveCypherdslBasedQuery.java | 8 +- .../query/ReactiveFluentQueryByExample.java | 24 +- .../query/ReactiveFluentQueryByPredicate.java | 2 +- .../query/ReactiveNeo4jQueryMethod.java | 2 + .../query/ReactivePartTreeNeo4jQuery.java | 4 +- ...eactiveQuerydslNeo4jPredicateExecutor.java | 3 +- .../query/ReactiveStringBasedNeo4jQuery.java | 25 +- .../query/StringBasedNeo4jQuery.java | 2 +- .../neo4j/repository/query/package-info.java | 4 +- ...aphPropertyAccessingMethodInterceptor.java | 7 +- .../Neo4jEvaluationContextExtension.java | 8 +- .../support/Neo4jRepositoryFactory.java | 3 +- .../support/Neo4jRepositoryFactoryBean.java | 22 +- .../ReactiveNeo4jRepositoryFactory.java | 3 +- .../ReactiveNeo4jRepositoryFactoryBean.java | 18 +- ...stenceExceptionTranslationInterceptor.java | 3 + ...enceExceptionTranslationPostProcessor.java | 14 +- .../support/SimpleNeo4jRepository.java | 5 +- .../SimpleReactiveNeo4jRepository.java | 6 +- .../repository/support/package-info.java | 4 +- .../data/neo4j/types/CartesianPoint3d.java | 4 +- .../data/neo4j/types/Coordinate.java | 7 +- .../data/neo4j/types/GeographicPoint3d.java | 4 +- .../data/neo4j/types/package-info.java | 4 +- .../data/neo4j/core/Neo4jClientExtensions.kt | 2 +- .../core/ReactiveNeo4jClientExtensions.kt | 2 +- .../data/neo4j/core/Neo4jClientTest.java | 5 +- .../neo4j/core/ReactiveNeo4jClientTest.java | 5 +- .../compose_as_ids/CompositeValue.java | 5 +- .../SimpleDisplayNameGeneratorWithTags.java | 3 +- ...nrelatedObjectPropertyConverterAsBean.java | 8 +- .../integration/issues/gh2500/Group.java | 5 +- .../ImperativeElementIdIT.java | 5 +- .../pure_element_id/ReactiveElementIdIT.java | 5 +- .../ThingWithCompositeProperties.java | 6 +- .../data/neo4j/test/Neo4jExtension.java | 3 +- .../imperative/ImmutableRelationshipsIT.kt | 4 +- .../imperative/KotlinInheritanceIT.kt | 4 +- .../imperative/KotlinProjectionIT.kt | 4 +- .../imperative/Neo4jListContainsTest.kt | 4 +- .../neo4j/integration/k/KotlinIssuesIT.kt | 4 +- 153 files changed, 1461 insertions(+), 1279 deletions(-) create mode 100644 src/main/java/org/springframework/data/neo4j/aot/package-info.java diff --git a/pom.xml b/pom.xml index 66d67801a..07b91bff0 100644 --- a/pom.xml +++ b/pom.xml @@ -84,7 +84,11 @@ spring.data.neo4j 17 2.3.1 + 5.8.0 + 1.0.0 + 3.0.2 2021.0.1 + 2.2.0 3.1.4 3.7.1 ${java.version} @@ -92,6 +96,7 @@ 2.17.3 4.4.41 3.0.1 + 6.0.0 ${project.build.directory}/docs UTF-8 Arabba-SR11 @@ -102,22 +107,14 @@ ${skipTests} 4.0.0-SNAPSHOT - 2.2.0 - - org.neo4j - neo4j-cypher-dsl-bom - ${cypher-dsl.version} - pom - import - com.google.code.findbugs jsr305 - 3.0.2 + ${jsr305.version} com.tngtech.archunit @@ -131,9 +128,9 @@ test - org.junit-pioneer - junit-pioneer - ${junit-pioneer.version} + io.projectreactor.tools + blockhound + ${blockhound.version} io.r2dbc @@ -161,18 +158,35 @@ net.java.dev.jna jna - 5.8.0 + ${jna.version} org.apiguardian apiguardian-api ${apiguardian.version} + + org.jspecify + jspecify + ${jspecify.version} + + + org.junit-pioneer + junit-pioneer + ${junit-pioneer.version} + org.neo4j neo4j ${neo4j.version} + + org.neo4j + neo4j-cypher-dsl-bom + ${cypher-dsl.version} + pom + import + org.neo4j.driver neo4j-java-driver @@ -191,7 +205,7 @@ org.osgi org.osgi.core - 6.0.0 + ${org.osgi.core.version} org.slf4j @@ -210,11 +224,6 @@ pom import - - io.projectreactor.tools - blockhound - ${blockhound.version} - @@ -279,6 +288,11 @@ test true + + io.projectreactor.tools + blockhound + test + io.r2dbc r2dbc-h2 @@ -341,6 +355,11 @@ kotlinx-coroutines-reactor true + + org.junit-pioneer + junit-pioneer + test + org.neo4j neo4j-cypher-dsl @@ -415,28 +434,18 @@ - - io.projectreactor.tools - blockhound - test - - - org.junit-pioneer - junit-pioneer - test - - spring-snapshot - https://repo.spring.io/snapshot - - true - false + + true + + spring-snapshot + https://repo.spring.io/snapshot spring-milestone @@ -643,31 +652,6 @@ - - org.apache.maven.plugins - maven-compiler-plugin - - - java-compile - - - -Xlint:all,-options,-path - - - - - - java-test-compile - - true - - - - org.apache.maven.plugins maven-resources-plugin @@ -689,8 +673,8 @@ - src/main/antora/resources/antora-resources true + src/main/antora/resources/antora-resources diff --git a/src/main/java/org/springframework/data/neo4j/aot/Neo4jManagedTypesBeanRegistrationAotProcessor.java b/src/main/java/org/springframework/data/neo4j/aot/Neo4jManagedTypesBeanRegistrationAotProcessor.java index 76008745a..115931e7f 100644 --- a/src/main/java/org/springframework/data/neo4j/aot/Neo4jManagedTypesBeanRegistrationAotProcessor.java +++ b/src/main/java/org/springframework/data/neo4j/aot/Neo4jManagedTypesBeanRegistrationAotProcessor.java @@ -15,17 +15,17 @@ */ package org.springframework.data.neo4j.aot; +import org.jspecify.annotations.Nullable; import org.springframework.aot.generate.GenerationContext; import org.springframework.core.ResolvableType; import org.springframework.data.aot.ManagedTypesBeanRegistrationAotProcessor; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; /** * @author Gerrit Meier * @since 7.0.0 */ -public class Neo4jManagedTypesBeanRegistrationAotProcessor extends ManagedTypesBeanRegistrationAotProcessor { +public final class Neo4jManagedTypesBeanRegistrationAotProcessor extends ManagedTypesBeanRegistrationAotProcessor { public Neo4jManagedTypesBeanRegistrationAotProcessor() { setModuleIdentifier("neo4j"); @@ -36,7 +36,7 @@ public class Neo4jManagedTypesBeanRegistrationAotProcessor extends ManagedTypesB return isNeo4jManagedTypes(beanType) || super.isMatch(beanType, beanName); } - protected boolean isNeo4jManagedTypes(@Nullable Class beanType) { + boolean isNeo4jManagedTypes(@Nullable Class beanType) { return beanType != null && ClassUtils.isAssignable(Neo4jManagedTypes.class, beanType); } diff --git a/src/main/java/org/springframework/data/neo4j/aot/Neo4jRuntimeHints.java b/src/main/java/org/springframework/data/neo4j/aot/Neo4jRuntimeHints.java index 12dd3c0bf..10f03ba12 100644 --- a/src/main/java/org/springframework/data/neo4j/aot/Neo4jRuntimeHints.java +++ b/src/main/java/org/springframework/data/neo4j/aot/Neo4jRuntimeHints.java @@ -15,6 +15,7 @@ */ package org.springframework.data.neo4j.aot; +import org.jspecify.annotations.Nullable; import org.springframework.aot.hint.MemberCategory; import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.RuntimeHintsRegistrar; @@ -34,7 +35,6 @@ import org.springframework.data.neo4j.repository.support.SimpleNeo4jRepository; import org.springframework.data.neo4j.repository.support.SimpleReactiveNeo4jRepository; import org.springframework.data.querydsl.QuerydslUtils; import org.springframework.data.util.ReactiveWrappers; -import org.springframework.lang.Nullable; import java.util.Arrays; diff --git a/src/main/java/org/springframework/data/neo4j/aot/package-info.java b/src/main/java/org/springframework/data/neo4j/aot/package-info.java new file mode 100644 index 000000000..d7bad8269 --- /dev/null +++ b/src/main/java/org/springframework/data/neo4j/aot/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package org.springframework.data.neo4j.aot; + +import org.jspecify.annotations.NullMarked; diff --git a/src/main/java/org/springframework/data/neo4j/config/AbstractNeo4jConfig.java b/src/main/java/org/springframework/data/neo4j/config/AbstractNeo4jConfig.java index 717ec65f9..568cd36ec 100644 --- a/src/main/java/org/springframework/data/neo4j/config/AbstractNeo4jConfig.java +++ b/src/main/java/org/springframework/data/neo4j/config/AbstractNeo4jConfig.java @@ -16,6 +16,7 @@ package org.springframework.data.neo4j.config; import org.apiguardian.api.API; +import org.jspecify.annotations.Nullable; import org.neo4j.driver.Driver; import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Autowired; @@ -30,7 +31,6 @@ import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager; import org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager; import org.springframework.data.neo4j.repository.config.Neo4jRepositoryConfigurationExtension; -import org.springframework.lang.Nullable; import org.springframework.transaction.PlatformTransactionManager; /** @@ -65,11 +65,11 @@ public abstract class AbstractNeo4jConfig extends Neo4jConfigurationSupport { * @return A imperative Neo4j client. */ @Bean(Neo4jRepositoryConfigurationExtension.DEFAULT_NEO4J_CLIENT_BEAN_NAME) - public Neo4jClient neo4jClient(Driver driver, DatabaseSelectionProvider databaseSelectionProvider) { + public Neo4jClient neo4jClient(Driver driver, @Nullable DatabaseSelectionProvider databaseSelectionProvider) { return Neo4jClient.with(driver) .withDatabaseSelectionProvider(databaseSelectionProvider) - .withUserSelectionProvider(getUserSelectionProvider()) + .withUserSelectionProvider(this.userSelectionProviders.getIfUnique()) .withNeo4jBookmarkManager(getBootBookmarkManager()) .build(); } @@ -78,11 +78,6 @@ public abstract class AbstractNeo4jConfig extends Neo4jConfigurationSupport { return this.bookmarkManagerProviders.getIfAvailable(Neo4jBookmarkManager::create); } - @Nullable - private UserSelectionProvider getUserSelectionProvider() { - return this.userSelectionProviders == null ? null : this.userSelectionProviders.getIfUnique(); - } - @Bean(Neo4jRepositoryConfigurationExtension.DEFAULT_NEO4J_TEMPLATE_BEAN_NAME) public Neo4jOperations neo4jTemplate(final Neo4jClient neo4jClient, final Neo4jMappingContext mappingContext) { @@ -97,12 +92,12 @@ public abstract class AbstractNeo4jConfig extends Neo4jConfigurationSupport { * @return A platform transaction manager */ @Bean(Neo4jRepositoryConfigurationExtension.DEFAULT_TRANSACTION_MANAGER_BEAN_NAME) - public PlatformTransactionManager transactionManager(Driver driver, DatabaseSelectionProvider databaseSelectionProvider) { + public PlatformTransactionManager transactionManager(Driver driver, @Nullable DatabaseSelectionProvider databaseSelectionProvider) { return Neo4jTransactionManager .with(driver) .withDatabaseSelectionProvider(databaseSelectionProvider) - .withUserSelectionProvider(getUserSelectionProvider()) + .withUserSelectionProvider(this.userSelectionProviders.getIfUnique()) .withBookmarkManager(getBootBookmarkManager()) .build(); } diff --git a/src/main/java/org/springframework/data/neo4j/config/AbstractReactiveNeo4jConfig.java b/src/main/java/org/springframework/data/neo4j/config/AbstractReactiveNeo4jConfig.java index 74534ac14..c6f854061 100644 --- a/src/main/java/org/springframework/data/neo4j/config/AbstractReactiveNeo4jConfig.java +++ b/src/main/java/org/springframework/data/neo4j/config/AbstractReactiveNeo4jConfig.java @@ -16,6 +16,7 @@ package org.springframework.data.neo4j.config; import org.apiguardian.api.API; +import org.jspecify.annotations.Nullable; import org.neo4j.driver.Driver; import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Autowired; @@ -29,7 +30,6 @@ import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager; import org.springframework.data.neo4j.core.transaction.ReactiveNeo4jTransactionManager; import org.springframework.data.neo4j.repository.config.ReactiveNeo4jRepositoryConfigurationExtension; -import org.springframework.lang.Nullable; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.ReactiveTransactionManager; diff --git a/src/main/java/org/springframework/data/neo4j/config/Neo4jConfigurationSupport.java b/src/main/java/org/springframework/data/neo4j/config/Neo4jConfigurationSupport.java index f86494dc2..b017fe8cb 100644 --- a/src/main/java/org/springframework/data/neo4j/config/Neo4jConfigurationSupport.java +++ b/src/main/java/org/springframework/data/neo4j/config/Neo4jConfigurationSupport.java @@ -90,18 +90,4 @@ abstract class Neo4jConfigurationSupport { return Neo4jEntityScanner.get().scan(getMappingBasePackages()); } - - /** - * Scans the given base package for entities, i.e. Neo4j specific types annotated with {@link Node}. - * - * @param basePackage must not be {@literal null}. - * @return found entities in the package to scan. - * @throws ClassNotFoundException if the given class cannot be loaded by the class loader. - * @deprecated since 6.0.2 Use {@link Neo4jEntityScanner} instead. - */ - @Deprecated - protected final Set> scanForEntities(String basePackage) throws ClassNotFoundException { - - return Neo4jEntityScanner.get().scan(basePackage); - } } diff --git a/src/main/java/org/springframework/data/neo4j/config/Neo4jEntityScanner.java b/src/main/java/org/springframework/data/neo4j/config/Neo4jEntityScanner.java index 216a1fd94..03a13e86a 100644 --- a/src/main/java/org/springframework/data/neo4j/config/Neo4jEntityScanner.java +++ b/src/main/java/org/springframework/data/neo4j/config/Neo4jEntityScanner.java @@ -23,6 +23,7 @@ import java.util.Set; import java.util.stream.Collectors; import org.apiguardian.api.API; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; @@ -31,7 +32,6 @@ import org.springframework.core.type.filter.AnnotationTypeFilter; import org.springframework.data.annotation.Persistent; import org.springframework.data.neo4j.core.schema.Node; import org.springframework.data.neo4j.core.schema.RelationshipProperties; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; @@ -55,7 +55,8 @@ public final class Neo4jEntityScanner { return new Neo4jEntityScanner(resourceLoader); } - private @Nullable final ResourceLoader resourceLoader; + @Nullable + private final ResourceLoader resourceLoader; /** * Create a new {@link Neo4jEntityScanner} instance. @@ -121,8 +122,7 @@ public final class Neo4jEntityScanner { * @param resourceLoader an optional {@link ResourceLoader} to use * @return a {@link ClassPathScanningCandidateComponentProvider} suitable to scan for Neo4j entities */ - private static ClassPathScanningCandidateComponentProvider createClassPathScanningCandidateComponentProvider( - @Nullable ResourceLoader resourceLoader) { + private static ClassPathScanningCandidateComponentProvider createClassPathScanningCandidateComponentProvider(@Nullable ResourceLoader resourceLoader) { ClassPathScanningCandidateComponentProvider delegate = new ClassPathScanningCandidateComponentProvider(false); if (resourceLoader != null) { diff --git a/src/main/java/org/springframework/data/neo4j/config/package-info.java b/src/main/java/org/springframework/data/neo4j/config/package-info.java index 8aa6d1f9d..ee8cb7d7d 100644 --- a/src/main/java/org/springframework/data/neo4j/config/package-info.java +++ b/src/main/java/org/springframework/data/neo4j/config/package-info.java @@ -5,7 +5,7 @@ The package provides some additional annotations that enable auditing. * */ -@NonNullApi +@NullMarked package org.springframework.data.neo4j.config; -import org.springframework.lang.NonNullApi; +import org.jspecify.annotations.NullMarked; diff --git a/src/main/java/org/springframework/data/neo4j/core/DatabaseSelection.java b/src/main/java/org/springframework/data/neo4j/core/DatabaseSelection.java index 4fe182656..015f5448c 100644 --- a/src/main/java/org/springframework/data/neo4j/core/DatabaseSelection.java +++ b/src/main/java/org/springframework/data/neo4j/core/DatabaseSelection.java @@ -18,7 +18,7 @@ package org.springframework.data.neo4j.core; import java.util.Objects; import org.apiguardian.api.API; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * A value holder indicating a database selection based on an optional name. {@literal null} indicates to let the server @@ -33,7 +33,8 @@ public final class DatabaseSelection { private static final DatabaseSelection DEFAULT_DATABASE_NAME = new DatabaseSelection(null); - @Nullable private final String value; + @Nullable + private final String value; public static DatabaseSelection undecided() { diff --git a/src/main/java/org/springframework/data/neo4j/core/DefaultNeo4jClient.java b/src/main/java/org/springframework/data/neo4j/core/DefaultNeo4jClient.java index f2884c944..a0362e6f5 100644 --- a/src/main/java/org/springframework/data/neo4j/core/DefaultNeo4jClient.java +++ b/src/main/java/org/springframework/data/neo4j/core/DefaultNeo4jClient.java @@ -26,6 +26,7 @@ import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.Stream; +import org.jspecify.annotations.Nullable; import org.neo4j.driver.Bookmark; import org.neo4j.driver.Driver; import org.neo4j.driver.Query; @@ -49,7 +50,6 @@ import org.springframework.data.neo4j.core.support.BookmarkManagerReference; import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager; import org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager; import org.springframework.data.neo4j.core.transaction.Neo4jTransactionUtils; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -64,8 +64,10 @@ import org.springframework.util.StringUtils; final class DefaultNeo4jClient implements Neo4jClient, ApplicationContextAware { private final Driver driver; - private @Nullable final DatabaseSelectionProvider databaseSelectionProvider; - private @Nullable final UserSelectionProvider userSelectionProvider; + @Nullable + private final DatabaseSelectionProvider databaseSelectionProvider; + @Nullable + private final UserSelectionProvider userSelectionProvider; private final ConversionService conversionService; private final Neo4jPersistenceExceptionTranslator persistenceExceptionTranslator = new Neo4jPersistenceExceptionTranslator(); @@ -276,7 +278,7 @@ final class DefaultNeo4jClient implements Neo4jClient, ApplicationContextAware { } @Override - public OngoingBindSpec bind(T value) { + public OngoingBindSpec bind(@Nullable T value) { return new DefaultOngoingBindSpec<>(value); } @@ -314,7 +316,8 @@ final class DefaultNeo4jClient implements Neo4jClient, ApplicationContextAware { class DefaultOngoingBindSpec implements OngoingBindSpec { - @Nullable private final T value; + @Nullable + private final T value; DefaultOngoingBindSpec(@Nullable T value) { this.value = value; @@ -360,7 +363,7 @@ final class DefaultNeo4jClient implements Neo4jClient, ApplicationContextAware { } @Override - public OngoingBindSpec bind(T value) { + public OngoingBindSpec bind(@Nullable T value) { return DefaultRunnableSpec.this.bind(value); } @@ -395,7 +398,7 @@ final class DefaultNeo4jClient implements Neo4jClient, ApplicationContextAware { } @Override - public OngoingBindSpec bind(T value) { + public OngoingBindSpec bind(@Nullable T value) { return DefaultRunnableSpec.this.bind(value); } @@ -410,7 +413,6 @@ final class DefaultNeo4jClient implements Neo4jClient, ApplicationContextAware { private final DatabaseSelection databaseSelection; - @Nullable private final UserSelection impersonatedUser; private final RunnableStatement runnableStatement; @@ -418,7 +420,7 @@ final class DefaultNeo4jClient implements Neo4jClient, ApplicationContextAware { private BiFunction mappingFunction; DefaultRecordFetchSpec(DatabaseSelection databaseSelection, - @Nullable UserSelection impersonatedUser, + UserSelection impersonatedUser, RunnableStatement runnableStatement, BiFunction mappingFunction) { @@ -501,7 +503,6 @@ final class DefaultNeo4jClient implements Neo4jClient, ApplicationContextAware { private DatabaseSelection databaseSelection; - @Nullable private UserSelection impersonatedUser; private final Function> callback; @@ -513,7 +514,7 @@ final class DefaultNeo4jClient implements Neo4jClient, ApplicationContextAware { } @Override - public RunnableDelegation in(@Nullable String targetDatabase) { + public RunnableDelegation in(String targetDatabase) { this.databaseSelection = resolveTargetDatabaseName(targetDatabase); return this; diff --git a/src/main/java/org/springframework/data/neo4j/core/DefaultReactiveNeo4jClient.java b/src/main/java/org/springframework/data/neo4j/core/DefaultReactiveNeo4jClient.java index dc188aad9..7ea9847fb 100644 --- a/src/main/java/org/springframework/data/neo4j/core/DefaultReactiveNeo4jClient.java +++ b/src/main/java/org/springframework/data/neo4j/core/DefaultReactiveNeo4jClient.java @@ -15,6 +15,7 @@ */ package org.springframework.data.neo4j.core; +import org.jspecify.annotations.Nullable; import org.neo4j.driver.Bookmark; import org.neo4j.driver.Driver; import org.neo4j.driver.Query; @@ -38,7 +39,6 @@ import org.springframework.data.neo4j.core.support.BookmarkManagerReference; import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager; import org.springframework.data.neo4j.core.transaction.Neo4jTransactionUtils; import org.springframework.data.neo4j.core.transaction.ReactiveNeo4jTransactionManager; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -66,8 +66,10 @@ import java.util.function.Supplier; final class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient, ApplicationContextAware { private final Driver driver; - private @Nullable final ReactiveDatabaseSelectionProvider databaseSelectionProvider; - private @Nullable final ReactiveUserSelectionProvider userSelectionProvider; + @Nullable + private final ReactiveDatabaseSelectionProvider databaseSelectionProvider; + @Nullable + private final ReactiveUserSelectionProvider userSelectionProvider; private final ConversionService conversionService; private final Neo4jPersistenceExceptionTranslator persistenceExceptionTranslator = new Neo4jPersistenceExceptionTranslator(); @@ -241,7 +243,7 @@ final class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient, Applicati } @Override - public Neo4jClient.OngoingBindSpec bind(T value) { + public Neo4jClient.OngoingBindSpec bind(@Nullable T value) { return new DefaultOngoingBindSpec<>(value); } @@ -267,12 +269,13 @@ final class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient, Applicati @Override public Mono run() { - return new DefaultRecordFetchSpec<>(databaseSelection, userSelection, cypherSupplier, this.parameters, null).run(); + return new DefaultRecordFetchSpec<>(databaseSelection, userSelection, cypherSupplier, this.parameters, (t, r) -> null).run(); } class DefaultOngoingBindSpec implements Neo4jClient.OngoingBindSpec { - @Nullable private final T value; + @Nullable + private final T value; DefaultOngoingBindSpec(@Nullable T value) { this.value = value; @@ -318,7 +321,7 @@ final class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient, Applicati } @Override - public Neo4jClient.OngoingBindSpec bind(T value) { + public Neo4jClient.OngoingBindSpec bind(@Nullable T value) { return DefaultRunnableSpec.this.bind(value); } @@ -353,7 +356,7 @@ final class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient, Applicati } @Override - public Neo4jClient.OngoingBindSpec bind(T value) { + public Neo4jClient.OngoingBindSpec bind(@Nullable T value) { return DefaultRunnableSpec.this.bind(value); } @@ -376,8 +379,7 @@ final class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient, Applicati private BiFunction mappingFunction; - DefaultRecordFetchSpec(Mono databaseSelection, Mono userSelection, Supplier cypherSupplier, NamedParameters parameters, - @Nullable BiFunction mappingFunction) { + DefaultRecordFetchSpec(Mono databaseSelection, Mono userSelection, Supplier cypherSupplier, NamedParameters parameters, BiFunction mappingFunction) { this.databaseSelection = databaseSelection; this.userSelection = userSelection; @@ -477,7 +479,7 @@ final class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient, Applicati } @Override - public RunnableDelegation in(@Nullable @SuppressWarnings("HiddenField") String targetDatabase) { + public RunnableDelegation in(@SuppressWarnings("HiddenField") String targetDatabase) { this.databaseSelection = resolveTargetDatabaseName(targetDatabase); return this; diff --git a/src/main/java/org/springframework/data/neo4j/core/DynamicLabels.java b/src/main/java/org/springframework/data/neo4j/core/DynamicLabels.java index 2be8dc1e1..560fe6a90 100644 --- a/src/main/java/org/springframework/data/neo4j/core/DynamicLabels.java +++ b/src/main/java/org/springframework/data/neo4j/core/DynamicLabels.java @@ -21,12 +21,12 @@ import java.util.Collections; import java.util.List; import java.util.function.UnaryOperator; +import org.jspecify.annotations.Nullable; import org.neo4j.cypherdsl.core.Cypher; import org.neo4j.cypherdsl.core.Node; import org.neo4j.cypherdsl.core.StatementBuilder.OngoingMatchAndUpdate; import org.springframework.data.neo4j.core.mapping.Constants; import org.springframework.data.neo4j.core.mapping.NodeDescription; -import org.springframework.lang.Nullable; /** * Decorator for an ongoing update statement that removes obsolete dynamic labels and adds new ones. @@ -42,9 +42,9 @@ final class DynamicLabels implements UnaryOperator { private final List oldLabels; private final List newLabels; - DynamicLabels(@Nullable NodeDescription nodeDescription, Collection oldLabels, Collection newLabels) { + DynamicLabels(@Nullable NodeDescription nodeDescription, Collection oldLabels, @Nullable Collection newLabels) { this.oldLabels = new ArrayList<>(oldLabels); - this.newLabels = new ArrayList<>(newLabels); + this.newLabels = (newLabels != null) ? new ArrayList<>(newLabels) : List.of(); this.rootNode = Cypher.anyNode(Constants.NAME_OF_TYPED_ROOT_NODE.apply(nodeDescription)); } diff --git a/src/main/java/org/springframework/data/neo4j/core/FluentFindOperation.java b/src/main/java/org/springframework/data/neo4j/core/FluentFindOperation.java index 73be3a6a6..cd5b72b79 100644 --- a/src/main/java/org/springframework/data/neo4j/core/FluentFindOperation.java +++ b/src/main/java/org/springframework/data/neo4j/core/FluentFindOperation.java @@ -21,9 +21,9 @@ import java.util.Map; import java.util.Optional; import org.apiguardian.api.API; +import org.jspecify.annotations.Nullable; import org.neo4j.cypherdsl.core.Statement; import org.springframework.data.neo4j.repository.query.QueryFragmentsAndParameters; -import org.springframework.lang.Nullable; /** * {@link FluentFindOperation} allows creation and execution of Neo4j find operations in a fluent API style. @@ -105,7 +105,7 @@ public interface FluentFindOperation { * @return new instance of {@link TerminatingFind}. * @throws IllegalArgumentException if query is {@literal null}. */ - TerminatingFind matching(String query, @Nullable Map parameter); + TerminatingFind matching(String query, Map parameter); /** * Creates an executable query based on fragments and parameters. Hardly useful outside framework-code @@ -136,7 +136,7 @@ public interface FluentFindOperation { * @return new instance of {@link TerminatingFind}. * @throws IllegalArgumentException if statement is {@literal null}. */ - TerminatingFind matching(Statement statement, @Nullable Map parameter); + TerminatingFind matching(Statement statement, Map parameter); /** * Set the filter {@link Statement statement} to be used. diff --git a/src/main/java/org/springframework/data/neo4j/core/FluentOperationSupport.java b/src/main/java/org/springframework/data/neo4j/core/FluentOperationSupport.java index 0c963eaf5..57a69344d 100644 --- a/src/main/java/org/springframework/data/neo4j/core/FluentOperationSupport.java +++ b/src/main/java/org/springframework/data/neo4j/core/FluentOperationSupport.java @@ -19,6 +19,7 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import org.jspecify.annotations.Nullable; import org.neo4j.cypherdsl.core.Statement; import org.springframework.data.neo4j.repository.query.QueryFragmentsAndParameters; import org.springframework.util.Assert; @@ -51,12 +52,15 @@ final class FluentOperationSupport implements FluentFindOperation, FluentSaveOpe private final Neo4jTemplate template; private final Class domainType; private final Class returnType; + @Nullable private final String query; + @Nullable private final Map parameters; + @Nullable private final QueryFragmentsAndParameters queryFragmentsAndParameters; - ExecutableFindSupport(Neo4jTemplate template, Class domainType, Class returnType, String query, - Map parameters) { + ExecutableFindSupport(Neo4jTemplate template, Class domainType, Class returnType, @Nullable String query, + @Nullable Map parameters) { this.template = template; this.domainType = domainType; this.returnType = returnType; @@ -65,7 +69,7 @@ final class FluentOperationSupport implements FluentFindOperation, FluentSaveOpe this.queryFragmentsAndParameters = null; } - ExecutableFindSupport(Neo4jTemplate template, Class domainType, Class returnType, QueryFragmentsAndParameters queryFragmentsAndParameters) { + ExecutableFindSupport(Neo4jTemplate template, Class domainType, Class returnType, @Nullable QueryFragmentsAndParameters queryFragmentsAndParameters) { this.template = template; this.domainType = domainType; this.returnType = returnType; @@ -107,6 +111,7 @@ final class FluentOperationSupport implements FluentFindOperation, FluentSaveOpe } @Override + @Nullable public T oneValue() { List result = doFind(TemplateSupport.FetchType.ONE); @@ -149,7 +154,7 @@ final class FluentOperationSupport implements FluentFindOperation, FluentSaveOpe List result = doSave(Collections.singleton(instance)); if (result.isEmpty()) { - return null; + throw new IllegalStateException("Instance was not saved"); } return result.get(0); } diff --git a/src/main/java/org/springframework/data/neo4j/core/NamedParameters.java b/src/main/java/org/springframework/data/neo4j/core/NamedParameters.java index 7f9eebba9..ba9a0c758 100644 --- a/src/main/java/org/springframework/data/neo4j/core/NamedParameters.java +++ b/src/main/java/org/springframework/data/neo4j/core/NamedParameters.java @@ -24,11 +24,11 @@ import java.util.Map; import java.util.stream.Collectors; import org.apiguardian.api.API; +import org.jspecify.annotations.Nullable; import org.neo4j.cypherdsl.core.Cypher; import org.neo4j.driver.Value; import org.springframework.data.neo4j.core.mapping.Constants; import org.springframework.data.neo4j.core.mapping.MapValueWrapper; -import org.springframework.lang.Nullable; /** * @author Michael J. Simons @@ -141,7 +141,7 @@ final class NamedParameters { } @Nullable - private static String formatValue(@Nullable Object value) { + private static String formatValue(Object value) { if (value == null) { return null; } else if (value instanceof String) { diff --git a/src/main/java/org/springframework/data/neo4j/core/Neo4jClient.java b/src/main/java/org/springframework/data/neo4j/core/Neo4jClient.java index c885ae023..88e59555d 100644 --- a/src/main/java/org/springframework/data/neo4j/core/Neo4jClient.java +++ b/src/main/java/org/springframework/data/neo4j/core/Neo4jClient.java @@ -15,6 +15,7 @@ */ package org.springframework.data.neo4j.core; +import java.io.Serial; import java.util.Collection; import java.util.Map; import java.util.Optional; @@ -25,6 +26,7 @@ import java.util.function.Supplier; import org.apache.commons.logging.LogFactory; import org.apiguardian.api.API; +import org.jspecify.annotations.Nullable; import org.neo4j.driver.Driver; import org.neo4j.driver.QueryRunner; import org.neo4j.driver.Record; @@ -33,7 +35,6 @@ import org.neo4j.driver.types.TypeSystem; import org.springframework.core.log.LogAccessor; import org.springframework.data.neo4j.core.convert.Neo4jConversions; import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager; -import org.springframework.lang.Nullable; /** * Definition of a modern Neo4j client. @@ -127,7 +128,7 @@ public interface Neo4jClient { * @return The builder * @since 6.3.3 */ - public Builder withNeo4jConversions(Neo4jConversions neo4jConversions) { + public Builder withNeo4jConversions(@Nullable Neo4jConversions neo4jConversions) { this.neo4jConversions = neo4jConversions; return this; } @@ -141,7 +142,7 @@ public interface Neo4jClient { * @return The builder * @since 7.1.2 */ - public Builder withNeo4jBookmarkManager(Neo4jBookmarkManager bookmarkManager) { + public Builder withNeo4jBookmarkManager(@Nullable Neo4jBookmarkManager bookmarkManager) { this.bookmarkManager = bookmarkManager; return this; } @@ -266,7 +267,7 @@ public interface Neo4jClient { * @param targetDatabase selected database to use. A {@literal null} value indicates the default database. * @return A runnable query specification that is now bound to a given database. */ - RunnableSpecBoundToDatabase in(@Nullable String targetDatabase); + RunnableSpecBoundToDatabase in(String targetDatabase); /** * Pins the previously defined query to an impersonated user. A value of {@literal null} chooses the user owning @@ -275,7 +276,7 @@ public interface Neo4jClient { * @param asUser The name of the user to impersonate. A {@literal null} value indicates the connected user. * @return A runnable query specification that is now bound to a given database. */ - RunnableSpecBoundToUser asUser(@Nullable String asUser); + RunnableSpecBoundToUser asUser(String asUser); } /** @@ -408,7 +409,7 @@ public interface Neo4jClient { * @param targetDatabase selected database to use. A {@literal null} value indicates the default database. * @return An ongoing delegation */ - RunnableDelegation in(@Nullable String targetDatabase); + RunnableDelegation in(String targetDatabase); } /** @@ -451,6 +452,9 @@ public interface Neo4jClient { @API(status = API.Status.STABLE, since = "6.1.5") class IllegalDatabaseNameException extends IllegalArgumentException { + @Serial + private static final long serialVersionUID = 3496326026855204643L; + private final String illegalDatabaseName; private IllegalDatabaseNameException(String illegalDatabaseName) { diff --git a/src/main/java/org/springframework/data/neo4j/core/Neo4jOperations.java b/src/main/java/org/springframework/data/neo4j/core/Neo4jOperations.java index 5f158415d..f6839382d 100644 --- a/src/main/java/org/springframework/data/neo4j/core/Neo4jOperations.java +++ b/src/main/java/org/springframework/data/neo4j/core/Neo4jOperations.java @@ -21,13 +21,13 @@ import java.util.Optional; import java.util.function.BiPredicate; import org.apiguardian.api.API; +import org.jspecify.annotations.Nullable; import org.neo4j.cypherdsl.core.Statement; import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.data.mapping.PropertyPath; import org.springframework.data.neo4j.core.mapping.Neo4jPersistentProperty; import org.springframework.data.neo4j.repository.NoResultException; import org.springframework.data.neo4j.repository.query.QueryFragmentsAndParameters; -import org.springframework.lang.Nullable; /** * Specifies operations one can perform on a database, based on an Domain Type. @@ -207,6 +207,7 @@ public interface Neo4jOperations { * @return the saved instance. * @since 6.3 */ + @Nullable default T saveAs(T instance, BiPredicate includeProperty) { throw new UnsupportedOperationException(); } @@ -220,6 +221,7 @@ public interface Neo4jOperations { * @return the saved, projected instance. * @since 6.1 */ + @Nullable default R saveAs(T instance, Class resultType) { throw new UnsupportedOperationException(); } diff --git a/src/main/java/org/springframework/data/neo4j/core/Neo4jPersistenceExceptionTranslator.java b/src/main/java/org/springframework/data/neo4j/core/Neo4jPersistenceExceptionTranslator.java index 2d801dc3c..18923b9f8 100644 --- a/src/main/java/org/springframework/data/neo4j/core/Neo4jPersistenceExceptionTranslator.java +++ b/src/main/java/org/springframework/data/neo4j/core/Neo4jPersistenceExceptionTranslator.java @@ -23,6 +23,7 @@ import java.util.function.BiFunction; import org.apache.commons.logging.LogFactory; import org.apiguardian.api.API; +import org.jspecify.annotations.Nullable; import org.neo4j.driver.exceptions.AuthenticationException; import org.neo4j.driver.exceptions.ClientException; import org.neo4j.driver.exceptions.DatabaseException; @@ -61,6 +62,7 @@ public final class Neo4jPersistenceExceptionTranslator implements PersistenceExc private static final Map>> ERROR_CODE_MAPPINGS; @Override + @Nullable public DataAccessException translateExceptionIfPossible(RuntimeException ex) { if (ex instanceof DataAccessException) { diff --git a/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java b/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java index fd07edba7..5b18b7fb4 100644 --- a/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java +++ b/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java @@ -40,6 +40,8 @@ import java.util.stream.Collectors; import org.apache.commons.logging.LogFactory; import org.apiguardian.api.API; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; import org.neo4j.cypherdsl.core.Condition; import org.neo4j.cypherdsl.core.Cypher; import org.neo4j.cypherdsl.core.FunctionInvocation; @@ -49,7 +51,6 @@ import org.neo4j.cypherdsl.core.Statement; import org.neo4j.cypherdsl.core.renderer.Configuration; import org.neo4j.cypherdsl.core.renderer.Renderer; import org.neo4j.driver.Value; -import org.neo4j.driver.Values; import org.neo4j.driver.exceptions.NoSuchRecordException; import org.neo4j.driver.summary.ResultSummary; import org.neo4j.driver.types.Entity; @@ -96,11 +97,11 @@ import org.springframework.data.neo4j.repository.query.QueryFragmentsAndParamete import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.projection.ProjectionInformation; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; -import org.springframework.data.util.TypeInformation; -import org.springframework.lang.NonNull; -import org.springframework.lang.Nullable; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.TransactionException; +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.support.TransactionCallback; import org.springframework.transaction.support.TransactionTemplate; import org.springframework.util.Assert; @@ -135,18 +136,22 @@ public final class Neo4jTemplate implements private final CypherGenerator cypherGenerator; + @Nullable private ClassLoader beanClassLoader; private EventSupport eventSupport; + @Nullable private ProjectionFactory projectionFactory; private Renderer renderer; private Function elementIdOrIdFunction; + @Nullable private TransactionTemplate transactionTemplate; + @Nullable private TransactionTemplate transactionTemplateReadOnly; public Neo4jTemplate(Neo4jClient neo4jClient) { @@ -185,6 +190,18 @@ public final class Neo4jTemplate implements return Objects.requireNonNull(this.projectionFactory, "Projection support for the Neo4j template is only available when the template is a proper and fully initialized Spring bean."); } + private T execute(TransactionCallback action) throws TransactionException { + return Objects.requireNonNull(Objects.requireNonNull(this.transactionTemplate).execute(action)); + } + + private T executeReadOnly(TransactionCallback action) throws TransactionException { + return Objects.requireNonNull(Objects.requireNonNull(this.transactionTemplateReadOnly).execute(action)); + } + + private void executeWithoutResult(Consumer action) throws TransactionException { + Objects.requireNonNull(this.transactionTemplate).executeWithoutResult(action); + } + @Override public long count(Class domainType) { @@ -213,7 +230,7 @@ public final class Neo4jTemplate implements @Override public long count(String cypherQuery, Map parameters) { - return transactionTemplateReadOnly.execute(tx -> { + return executeReadOnly(tx -> { PreparedQuery preparedQuery = PreparedQuery.queryFor(Long.class).withCypherQuery(cypherQuery) .withParameters(parameters).build(); return toExecutableQuery(preparedQuery, true).getRequiredSingleResult(); @@ -227,8 +244,7 @@ public final class Neo4jTemplate implements } private List doFindAll(Class domainType, @Nullable Class resultType) { - return transactionTemplateReadOnly - .execute(tx -> { + return executeReadOnly(tx -> { Neo4jPersistentEntity entityMetaData = neo4jMappingContext.getRequiredPersistentEntity(domainType); return createExecutableQuery( domainType, resultType, QueryFragmentsAndParameters.forFindAll(entityMetaData), true) @@ -238,61 +254,54 @@ public final class Neo4jTemplate implements @Override public List findAll(Statement statement, Class domainType) { - return transactionTemplateReadOnly - .execute(tx -> createExecutableQuery(domainType, statement, true).getResults()); + return executeReadOnly(tx -> createExecutableQuery(domainType, statement, true).getResults()); } @Override public List findAll(Statement statement, Map parameters, Class domainType) { - return transactionTemplateReadOnly - .execute(tx -> createExecutableQuery(domainType, null, statement, parameters, true).getResults()); + return executeReadOnly(tx -> createExecutableQuery(domainType, null, statement, parameters, true).getResults()); } @Override public Optional findOne(Statement statement, Map parameters, Class domainType) { - return transactionTemplateReadOnly - .execute(tx -> createExecutableQuery(domainType, null, statement, parameters, true).getSingleResult()); + return executeReadOnly(tx -> createExecutableQuery(domainType, null, statement, parameters, true).getSingleResult()); } @Override public List findAll(String cypherQuery, Class domainType) { - return transactionTemplateReadOnly - .execute(tx -> createExecutableQuery(domainType, cypherQuery, true).getResults()); + return executeReadOnly(tx -> createExecutableQuery(domainType, cypherQuery, true).getResults()); } @Override public List findAll(String cypherQuery, Map parameters, Class domainType) { - return transactionTemplateReadOnly - .execute(tx -> createExecutableQuery(domainType, null, cypherQuery, parameters, true).getResults()); + return executeReadOnly(tx -> createExecutableQuery(domainType, null, cypherQuery, parameters, true).getResults()); } @Override public Optional findOne(String cypherQuery, Map parameters, Class domainType) { - return transactionTemplateReadOnly - .execute(tx -> createExecutableQuery(domainType, null, cypherQuery, parameters, true).getSingleResult()); + return executeReadOnly(tx -> createExecutableQuery(domainType, null, cypherQuery, parameters, true).getSingleResult()); } @Override public ExecutableFind find(Class domainType) { - return transactionTemplateReadOnly - .execute(tx -> new FluentOperationSupport(this).find(domainType)); + return new FluentOperationSupport(this).find(domainType); } @SuppressWarnings("unchecked") List doFind(@Nullable String cypherQuery, @Nullable Map parameters, Class domainType, Class resultType, TemplateSupport.FetchType fetchType, @Nullable QueryFragmentsAndParameters queryFragmentsAndParameters) { - return transactionTemplateReadOnly.execute(tx -> { - List intermediaResults = Collections.emptyList(); + return executeReadOnly(tx -> { + List intermediaResults; if (cypherQuery == null && queryFragmentsAndParameters == null && fetchType == TemplateSupport.FetchType.ALL) { intermediaResults = doFindAll(domainType, resultType); } else { ExecutableQuery executableQuery; - if (queryFragmentsAndParameters == null) { + if (queryFragmentsAndParameters == null && cypherQuery != null) { executableQuery = createExecutableQuery(domainType, resultType, cypherQuery, parameters == null ? Collections.emptyMap() : parameters, true); } else { - executableQuery = createExecutableQuery(domainType, resultType, queryFragmentsAndParameters, true); + executableQuery = createExecutableQuery(domainType, resultType, Objects.requireNonNull(queryFragmentsAndParameters), true); } intermediaResults = switch (fetchType) { case ALL -> executableQuery.getResults(); @@ -327,7 +336,7 @@ public final class Neo4jTemplate implements Neo4jPersistentEntity entityMetaData = neo4jMappingContext.getRequiredPersistentEntity(domainType); QueryFragmentsAndParameters fragmentsAndParameters = QueryFragmentsAndParameters - .forExistsById(entityMetaData, convertIdValues(entityMetaData.getRequiredIdProperty(), id)); + .forExistsById(entityMetaData, TemplateSupport.convertIdValues(this.neo4jMappingContext, entityMetaData.getRequiredIdProperty(), id)); Statement statement = fragmentsAndParameters.getQueryFragments().toStatement(); Map parameters = fragmentsAndParameters.getParameters(); @@ -337,13 +346,12 @@ public final class Neo4jTemplate implements @Override public Optional findById(Object id, Class domainType) { - return transactionTemplateReadOnly - .execute(tx -> { + return executeReadOnly(tx -> { Neo4jPersistentEntity entityMetaData = neo4jMappingContext.getRequiredPersistentEntity(domainType); return createExecutableQuery(domainType, null, QueryFragmentsAndParameters.forFindById(entityMetaData, - convertIdValues(entityMetaData.getRequiredIdProperty(), id)), + TemplateSupport.convertIdValues(this.neo4jMappingContext, entityMetaData.getRequiredIdProperty(), id)), true) .getSingleResult(); }); @@ -351,62 +359,44 @@ public final class Neo4jTemplate implements @Override public List findAllById(Iterable ids, Class domainType) { - return transactionTemplateReadOnly - .execute(tx -> { + return executeReadOnly(tx -> { Neo4jPersistentEntity entityMetaData = neo4jMappingContext.getRequiredPersistentEntity(domainType); return createExecutableQuery(domainType, null, QueryFragmentsAndParameters.forFindByAllId( - entityMetaData, convertIdValues(entityMetaData.getRequiredIdProperty(), ids)), + entityMetaData, TemplateSupport.convertIdValues(this.neo4jMappingContext, entityMetaData.getRequiredIdProperty(), ids)), true) .getResults(); }); } - private Object convertIdValues(@Nullable Neo4jPersistentProperty idProperty, @Nullable Object idValues) { - - if (idProperty != null && ((Neo4jPersistentEntity) idProperty.getOwner()).isUsingInternalIds()) { - return idValues; - } - - if (idValues != null) { - return neo4jMappingContext.getConversionService().writeValue(idValues, TypeInformation.of(idValues.getClass()), idProperty == null ? null : idProperty.getOptionalConverter()); - } else if (idProperty != null) { - return neo4jMappingContext.getConversionService().writeValue(idValues, idProperty.getTypeInformation(), idProperty.getOptionalConverter()); - } else { - // Not much we can convert here - return Values.NULL; - } - } - @Override public T save(T instance) { - return transactionTemplate - .execute(tx -> saveImpl(instance, Collections.emptySet(), null)); + return execute(tx -> saveImpl(instance, Collections.emptySet(), null)); } @Override + @Nullable public T saveAs(T instance, BiPredicate includeProperty) { if (instance == null) { return null; } - return transactionTemplate - .execute(tx -> saveImpl(instance, TemplateSupport.computeIncludedPropertiesFromPredicate(this.neo4jMappingContext, instance.getClass(), includeProperty), null)); + return execute(tx -> saveImpl(instance, TemplateSupport.computeIncludedPropertiesFromPredicate(this.neo4jMappingContext, instance.getClass(), includeProperty), null)); } @Override + @Nullable public R saveAs(T instance, Class resultType) { - return transactionTemplate.execute(tx -> { + Assert.notNull(resultType, "ResultType must not be null"); + if (instance == null) { + return null; + } - Assert.notNull(resultType, "ResultType must not be null"); - - if (instance == null) { - return null; - } + return execute(tx -> { if (resultType.equals(instance.getClass())) { return resultType.cast(save(instance)); @@ -427,14 +417,14 @@ public final class Neo4jTemplate implements } Neo4jPersistentEntity entityMetaData = neo4jMappingContext.getRequiredPersistentEntity(savedInstance.getClass()); - Neo4jPersistentProperty idProperty = entityMetaData.getIdProperty(); + Neo4jPersistentProperty idProperty = entityMetaData.getRequiredIdProperty(); PersistentPropertyAccessor propertyAccessor = entityMetaData.getPropertyAccessor(savedInstance); return localProjectionFactory.createProjection(resultType, - this.findById(propertyAccessor.getProperty(idProperty), savedInstance.getClass()).get()); + this.findById(Objects.requireNonNull(propertyAccessor.getProperty(idProperty)), savedInstance.getClass()).orElseThrow()); }); } - private T saveImpl(T instance, @Nullable Collection includedProperties, @Nullable NestedRelationshipProcessingStateMachine stateMachine) { + private T saveImpl(T instance, Collection includedProperties, @Nullable NestedRelationshipProcessingStateMachine stateMachine) { if (stateMachine != null && stateMachine.hasProcessedValue(instance)) { return instance; @@ -471,8 +461,10 @@ public final class Neo4jTemplate implements if (!entityMetaData.isUsingDeprecatedInternalId() && TemplateSupport.rendererRendersElementId(renderer)) { return IdentitySupport.getElementId(node); } - return node.id(); - }).get(); + @SuppressWarnings("deprecation") + var id = node.id(); + return id; + }).orElseThrow(); PersistentPropertyAccessor propertyAccessor = entityMetaData.getPropertyAccessor(entityToBeSaved); TemplateSupport.setGeneratedIdIfNecessary(entityMetaData, propertyAccessor, elementId, newOrUpdatedNode); @@ -498,7 +490,7 @@ public final class Neo4jTemplate implements Neo4jPersistentProperty idProperty = entityMetaData.getRequiredIdProperty(); Neo4jClient.RunnableSpec runnableQuery = neo4jClient .query(() -> renderer.render(cypherGenerator.createStatementReturningDynamicLabels(entityMetaData))) - .bind(convertIdValues(idProperty, propertyAccessor.getProperty(idProperty))) + .bind(TemplateSupport.convertIdValues(this.neo4jMappingContext, idProperty, propertyAccessor.getProperty(idProperty))) .to(Constants.NAME_OF_ID).bind(entityMetaData.getStaticLabels()) .to(Constants.NAME_OF_STATIC_LABELS_PARAM); @@ -516,8 +508,7 @@ public final class Neo4jTemplate implements @Override public List saveAll(Iterable instances) { - return transactionTemplate - .execute(tx -> saveAllImpl(instances, Collections.emptySet(), null)); + return execute(tx -> saveAllImpl(instances, Collections.emptySet(), null)); } private boolean requiresSingleStatements(boolean heterogeneousCollection, Neo4jPersistentEntity entityMetaData) { @@ -544,7 +535,7 @@ public final class Neo4jTemplate implements Class domainClass = types.iterator().next(); Collection pps = includeProperty == null ? - includedProperties : + Objects.requireNonNullElseGet(includedProperties, List::of) : TemplateSupport.computeIncludedPropertiesFromPredicate(this.neo4jMappingContext, domainClass, includeProperty); @@ -593,8 +584,8 @@ public final class Neo4jTemplate implements return entitiesToBeSaved.stream().map(t -> { PersistentPropertyAccessor propertyAccessor = entityMetaData.getPropertyAccessor(t.modifiedInstance); Neo4jPersistentProperty idProperty = entityMetaData.getRequiredIdProperty(); - Object id = convertIdValues(idProperty, propertyAccessor.getProperty(idProperty)); - String internalId = idToInternalIdMapping.get(id); + Object id = TemplateSupport.convertIdValues(this.neo4jMappingContext, idProperty, propertyAccessor.getProperty(idProperty)); + String internalId = Objects.requireNonNull(idToInternalIdMapping.get(id)); stateMachine.registerInitialObject(t.originalInstance, internalId); return this.processRelations(entityMetaData, propertyAccessor, t.wasNew, stateMachine, TemplateSupport.computeIncludePropertyPredicate(pps, entityMetaData)); }).collect(Collectors.toList()); @@ -603,17 +594,15 @@ public final class Neo4jTemplate implements @Override public List saveAllAs(Iterable instances, BiPredicate includeProperty) { - return transactionTemplate - .execute(tx -> saveAllImpl(instances, null, includeProperty)); + return execute(tx -> saveAllImpl(instances, null, includeProperty)); } @Override public List saveAllAs(Iterable instances, Class resultType) { - return transactionTemplate - .execute(tx -> { + Assert.notNull(resultType, "ResultType must not be null"); - Assert.notNull(resultType, "ResultType must not be null"); + return execute(tx -> { Class commonElementType = TemplateSupport.findCommonElementType(instances); @@ -645,7 +634,7 @@ public final class Neo4jTemplate implements } Neo4jPersistentEntity entityMetaData = neo4jMappingContext.getRequiredPersistentEntity(commonElementType); - Neo4jPersistentProperty idProperty = entityMetaData.getIdProperty(); + Neo4jPersistentProperty idProperty = entityMetaData.getRequiredIdProperty(); List ids = savedInstances.stream().map(savedInstance -> { PersistentPropertyAccessor propertyAccessor = entityMetaData.getPropertyAccessor(savedInstance); @@ -661,8 +650,7 @@ public final class Neo4jTemplate implements @Override public void deleteById(Object id, Class domainType) { - transactionTemplate - .executeWithoutResult(tx -> { + executeWithoutResult(tx -> { Neo4jPersistentEntity entityMetaData = neo4jMappingContext.getRequiredPersistentEntity(domainType); String nameOfParameter = "id"; @@ -672,7 +660,7 @@ public final class Neo4jTemplate implements Statement statement = cypherGenerator.prepareDeleteOf(entityMetaData, condition); ResultSummary summary = this.neo4jClient.query(renderer.render(statement)) - .bind(convertIdValues(entityMetaData.getRequiredIdProperty(), id)) + .bind(TemplateSupport.convertIdValues(this.neo4jMappingContext, entityMetaData.getRequiredIdProperty(), id)) .to(nameOfParameter).run(); log.debug(() -> String.format("Deleted %d nodes and %d relationships.", summary.counters().nodesDeleted(), @@ -682,38 +670,37 @@ public final class Neo4jTemplate implements @Override public void deleteByIdWithVersion(Object id, Class domainType, Neo4jPersistentProperty versionProperty, - Object versionValue) { + @Nullable Object versionValue) { - transactionTemplate - .executeWithoutResult(tx -> { - Neo4jPersistentEntity entityMetaData = neo4jMappingContext.getRequiredPersistentEntity(domainType); + executeWithoutResult(tx -> { + Neo4jPersistentEntity entityMetaData = neo4jMappingContext.getRequiredPersistentEntity(domainType); - String nameOfParameter = "id"; - Condition condition = entityMetaData.getIdExpression().isEqualTo(parameter(nameOfParameter)) - .and(Cypher.property(Constants.NAME_OF_TYPED_ROOT_NODE.apply(entityMetaData), versionProperty.getPropertyName()) - .isEqualTo(parameter(Constants.NAME_OF_VERSION_PARAM)) - .or(Cypher.property(Constants.NAME_OF_TYPED_ROOT_NODE.apply(entityMetaData), versionProperty.getPropertyName()).isNull())); + String nameOfParameter = "id"; + Condition condition = entityMetaData.getIdExpression().isEqualTo(parameter(nameOfParameter)) + .and(Cypher.property(Constants.NAME_OF_TYPED_ROOT_NODE.apply(entityMetaData), versionProperty.getPropertyName()) + .isEqualTo(parameter(Constants.NAME_OF_VERSION_PARAM)) + .or(Cypher.property(Constants.NAME_OF_TYPED_ROOT_NODE.apply(entityMetaData), versionProperty.getPropertyName()).isNull())); - Statement statement = cypherGenerator.prepareMatchOf(entityMetaData, condition) - .returning(Constants.NAME_OF_TYPED_ROOT_NODE.apply(entityMetaData)).build(); + Statement statement = cypherGenerator.prepareMatchOf(entityMetaData, condition) + .returning(Constants.NAME_OF_TYPED_ROOT_NODE.apply(entityMetaData)).build(); - Map parameters = new HashMap<>(); - parameters.put(nameOfParameter, convertIdValues(entityMetaData.getRequiredIdProperty(), id)); - parameters.put(Constants.NAME_OF_VERSION_PARAM, versionValue); + Map parameters = new HashMap<>(); + parameters.put(nameOfParameter, TemplateSupport.convertIdValues(this.neo4jMappingContext, entityMetaData.getRequiredIdProperty(), id)); + parameters.put(Constants.NAME_OF_VERSION_PARAM, versionValue); - createExecutableQuery(domainType, null, statement, parameters, false).getSingleResult().orElseThrow( - () -> new OptimisticLockingFailureException(OPTIMISTIC_LOCKING_ERROR_MESSAGE) - ); + var lockedEntity = createExecutableQuery(domainType, null, statement, parameters, false).getSingleResult(); + if (lockedEntity.isEmpty()) { + throw new OptimisticLockingFailureException(OPTIMISTIC_LOCKING_ERROR_MESSAGE); + } - deleteById(id, domainType); - }); + deleteById(id, domainType); + }); } @Override public void deleteAllById(Iterable ids, Class domainType) { - transactionTemplate - .executeWithoutResult(tx -> { + executeWithoutResult(tx -> { Neo4jPersistentEntity entityMetaData = neo4jMappingContext.getRequiredPersistentEntity(domainType); String nameOfParameter = "ids"; @@ -723,7 +710,7 @@ public final class Neo4jTemplate implements Statement statement = cypherGenerator.prepareDeleteOf(entityMetaData, condition); ResultSummary summary = this.neo4jClient.query(renderer.render(statement)) - .bind(convertIdValues(entityMetaData.getRequiredIdProperty(), ids)) + .bind(TemplateSupport.convertIdValues(this.neo4jMappingContext, entityMetaData.getRequiredIdProperty(), ids)) .to(nameOfParameter).run(); log.debug(() -> String.format("Deleted %d nodes and %d relationships.", summary.counters().nodesDeleted(), @@ -734,8 +721,7 @@ public final class Neo4jTemplate implements @Override public void deleteAll(Class domainType) { - transactionTemplate - .executeWithoutResult(tx -> { + executeWithoutResult(tx -> { Neo4jPersistentEntity entityMetaData = neo4jMappingContext.getRequiredPersistentEntity(domainType); log.debug(() -> String.format("Deleting all nodes with primary label %s", entityMetaData.getPrimaryLabel())); @@ -762,8 +748,9 @@ public final class Neo4jTemplate implements } private ExecutableQuery createExecutableQuery( - Class domainType, @Nullable Class resultType, - @Nullable String cypherStatement, + Class domainType, + @Nullable Class resultType, + String cypherStatement, Map parameters, boolean readOnly) { @@ -802,6 +789,7 @@ public final class Neo4jTemplate implements stateMachine, includeProperty, startingPropertyPath); } + @SuppressWarnings("deprecation") private T processNestedRelations( Neo4jPersistentEntity sourceEntity, PersistentPropertyAccessor propertyAccessor, @@ -837,7 +825,7 @@ public final class Neo4jTemplate implements idProperty = null; } else { Neo4jPersistentEntity relationshipPropertiesEntity = (Neo4jPersistentEntity) relationshipDescription.getRelationshipPropertiesEntity(); - idProperty = relationshipPropertiesEntity.getIdProperty(); + idProperty = (relationshipPropertiesEntity == null) ? null : relationshipPropertiesEntity.getIdProperty(); } // break recursive procession and deletion of previously created relationships @@ -859,7 +847,11 @@ public final class Neo4jTemplate implements continue; } - Object id = relationshipContext.getRelationshipPropertiesPropertyAccessor(relatedValueToStore).getProperty(idProperty); + PersistentPropertyAccessor relationshipPropertiesPropertyAccessor = relationshipContext.getRelationshipPropertiesPropertyAccessor(relatedValueToStore); + if (relationshipPropertiesPropertyAccessor == null) { + continue; + } + Object id = relationshipPropertiesPropertyAccessor.getProperty(idProperty); if (id != null) { knownRelationshipsIds.add(id); } @@ -869,7 +861,7 @@ public final class Neo4jTemplate implements Statement relationshipRemoveQuery = cypherGenerator.prepareDeleteOf(sourceEntity, relationshipDescription, canUseElementId); neo4jClient.query(renderer.render(relationshipRemoveQuery)) - .bind(convertIdValues(sourceEntity.getIdProperty(), fromId)) // + .bind(TemplateSupport.convertIdValues(this.neo4jMappingContext, sourceEntity.getIdProperty(), fromId)) // .to(Constants.FROM_ID_PARAMETER_NAME) // .bind(knownRelationshipsIds) // .to(Constants.NAME_OF_KNOWN_RELATIONSHIPS_PARAM) // @@ -935,13 +927,17 @@ public final class Neo4jTemplate implements stateMachine.markRelationshipAsProcessed(possibleInternalLongId == null ? relatedInternalId : possibleInternalLongId, relationshipDescription.getRelationshipObverse()); - Object idValue = idProperty != null - ? relationshipContext - .getRelationshipPropertiesPropertyAccessor(relatedValueToStore).getProperty(idProperty) - : null; + Object idValue; + PersistentPropertyAccessor relationshipPropertiesPropertyAccessor = relationshipContext + .getRelationshipPropertiesPropertyAccessor(relatedValueToStore); + if (idProperty == null || relationshipPropertiesPropertyAccessor == null) { + idValue = null; + } else { + idValue = relationshipPropertiesPropertyAccessor.getProperty(idProperty); + } Map properties = new HashMap<>(); - properties.put(Constants.FROM_ID_PARAMETER_NAME, convertIdValues(sourceEntity.getRequiredIdProperty(), fromId)); + properties.put(Constants.FROM_ID_PARAMETER_NAME, TemplateSupport.convertIdValues(this.neo4jMappingContext, sourceEntity.getRequiredIdProperty(), fromId)); properties.put(Constants.TO_ID_PARAMETER_NAME, relatedInternalId); properties.put(Constants.NAME_OF_KNOWN_RELATIONSHIP_PARAM, idValue); boolean isNewRelationship = idValue == null; @@ -955,7 +951,7 @@ public final class Neo4jTemplate implements statementHolder = statementHolder.addProperty(Constants.NAME_OF_RELATIONSHIP_LIST_PARAM, row); Optional relationshipInternalId = neo4jClient.query(renderer.render(statementHolder.getStatement())) - .bind(convertIdValues(sourceEntity.getRequiredIdProperty(), fromId)) // + .bind(TemplateSupport.convertIdValues(this.neo4jMappingContext, sourceEntity.getRequiredIdProperty(), fromId)) // .to(Constants.FROM_ID_PARAMETER_NAME) // .bind(relatedInternalId) // .to(Constants.TO_ID_PARAMETER_NAME) // @@ -975,7 +971,7 @@ public final class Neo4jTemplate implements List row = Collections.singletonList(properties); statementHolder = statementHolder.addProperty(Constants.NAME_OF_RELATIONSHIP_LIST_PARAM, row); neo4jClient.query(renderer.render(statementHolder.getStatement())) - .bind(convertIdValues(sourceEntity.getRequiredIdProperty(), fromId)) // + .bind(TemplateSupport.convertIdValues(this.neo4jMappingContext, sourceEntity.getRequiredIdProperty(), fromId)) // .to(Constants.FROM_ID_PARAMETER_NAME) // .bind(relatedInternalId) // .to(Constants.TO_ID_PARAMETER_NAME) // @@ -984,7 +980,7 @@ public final class Neo4jTemplate implements .bindAll(statementHolder.getProperties()) .run(); } - } else if (relationshipDescription.hasRelationshipProperties()) { + } else if (relationshipDescription.hasRelationshipProperties() && fromId != null) { // check if bidi mapped already var hlp = ((MappingSupport.RelationshipPropertiesWithEntityHolder) relatedValueToStore); var hasProcessedRelationshipEntity = stateMachine.hasProcessedRelationshipEntity(propertyAccessor.getBean(), hlp.getRelatedEntity(), relationshipContext.getRelationship()); @@ -1034,7 +1030,7 @@ public final class Neo4jTemplate implements .bindAll(statementHolder.getProperties()) .run(); } - if (!newRelationshipPropertiesToStore.isEmpty()) { + if (!(newRelationshipPropertiesToStore.isEmpty() || idProperty == null)) { CreateRelationshipStatementHolder statementHolder = neo4jMappingContext.createStatementForImperativeRelationshipsWithPropertiesBatch(true, sourceEntity, relationshipDescription, newRelationshipPropertiesToStore, newRelationshipPropertiesRows, canUseElementId); List all = new ArrayList<>(neo4jClient.query(renderer.render(statementHolder.getStatement())) @@ -1061,10 +1057,10 @@ public final class Neo4jTemplate implements return finalSubgraphRoot; } - private Optional getRelationshipId(Statement statement, Neo4jPersistentProperty idProperty, Object fromId, Object toId) { + private Optional getRelationshipId(Statement statement, @Nullable Neo4jPersistentProperty idProperty, Object fromId, Object toId) { return neo4jClient.query(renderer.render(statement)) - .bind(convertIdValues(idProperty, fromId)) // + .bind(TemplateSupport.convertIdValues(this.neo4jMappingContext, idProperty, fromId)) // .to(Constants.FROM_ID_PARAMETER_NAME) // .bind(toId) // .to(Constants.TO_ID_PARAMETER_NAME) // @@ -1076,10 +1072,10 @@ public final class Neo4jTemplate implements // The pendant to {@link #saveRelatedNode(Object, NodeDescription, PropertyFilter, PropertyFilter.RelaxedPropertyPath)} // We can't do without a query, as we need to refresh the internal id - private Entity loadRelatedNode(NodeDescription targetNodeDescription, Object relatedInternalId) { + private Entity loadRelatedNode(NodeDescription targetNodeDescription, @Nullable Object relatedInternalId) { var targetPersistentEntity = (Neo4jPersistentEntity) targetNodeDescription; - var queryFragmentsAndParameters = QueryFragmentsAndParameters.forFindById(targetPersistentEntity, convertIdValues(targetPersistentEntity.getRequiredIdProperty(), relatedInternalId)); + var queryFragmentsAndParameters = QueryFragmentsAndParameters.forFindById(targetPersistentEntity, TemplateSupport.convertIdValues(this.neo4jMappingContext, targetPersistentEntity.getRequiredIdProperty(), relatedInternalId)); var nodeName = Constants.NAME_OF_TYPED_ROOT_NODE.apply(targetNodeDescription).getValue(); return neo4jClient @@ -1097,9 +1093,12 @@ public final class Neo4jTemplate implements Neo4jPersistentProperty idProperty, Object relationshipInternalId ) { - relationshipContext - .getRelationshipPropertiesPropertyAccessor(relatedValueToStore) - .setProperty(idProperty, relationshipInternalId); + PersistentPropertyAccessor relationshipPropertiesPropertyAccessor = relationshipContext + .getRelationshipPropertiesPropertyAccessor(relatedValueToStore); + if (relationshipPropertiesPropertyAccessor != null) { + relationshipPropertiesPropertyAccessor + .setProperty(idProperty, relationshipInternalId); + } } private Entity saveRelatedNode(Object entity, NodeDescription targetNodeDescription, PropertyFilter includeProperty, PropertyFilter.RelaxedPropertyPath currentPropertyPath) { @@ -1113,10 +1112,10 @@ public final class Neo4jTemplate implements binderFunction = binderFunction.andThen(tree -> { @SuppressWarnings("unchecked") Map properties = (Map) tree.get(Constants.NAME_OF_PROPERTIES_PARAM); - String idPropertyName = targetPersistentEntity.getIdProperty().getPropertyName(); + String idPropertyName = targetPersistentEntity.getRequiredIdProperty().getPropertyName(); IdDescription idDescription = targetPersistentEntity.getIdDescription(); - boolean assignedId = idDescription.isAssignedId() || idDescription.isExternallyGeneratedId(); - if (!includeProperty.isNotFiltering()) { + boolean assignedId = idDescription != null && (idDescription.isAssignedId() || idDescription.isExternallyGeneratedId()); + if (properties != null && !includeProperty.isNotFiltering()) { properties.entrySet() .removeIf(e -> { // we cannot skip the id property if it is an assigned id @@ -1143,6 +1142,7 @@ public final class Neo4jTemplate implements @Override public void setBeanClassLoader(ClassLoader beanClassLoader) { + //noinspection ConstantValue this.beanClassLoader = beanClassLoader == null ? org.springframework.util.ClassUtils.getDefaultClassLoader() : beanClassLoader; } @@ -1152,7 +1152,7 @@ public final class Neo4jTemplate implements this.eventSupport = EventSupport.discoverCallbacks(neo4jMappingContext, beanFactory); SpelAwareProxyProjectionFactory spelAwareProxyProjectionFactory = new SpelAwareProxyProjectionFactory(); - spelAwareProxyProjectionFactory.setBeanClassLoader(beanClassLoader); + spelAwareProxyProjectionFactory.setBeanClassLoader(Objects.requireNonNull(this.beanClassLoader)); spelAwareProxyProjectionFactory.setBeanFactory(beanFactory); this.projectionFactory = spelAwareProxyProjectionFactory; @@ -1166,6 +1166,7 @@ public final class Neo4jTemplate implements if (this.transactionTemplate != null && this.transactionTemplateReadOnly != null) { return; } + PlatformTransactionManager transactionManager = null; var it = beanFactory.getBeanProvider(PlatformTransactionManager.class).stream().iterator(); while (it.hasNext()) { @@ -1203,7 +1204,8 @@ public final class Neo4jTemplate implements private ExecutableQuery createExecutableQuery( - Class domainType, @Nullable Class resultType, + Class domainType, + @Nullable Class resultType, QueryFragmentsAndParameters queryFragmentsAndParameters, boolean readOnlyTransaction) { @@ -1232,15 +1234,13 @@ public final class Neo4jTemplate implements } List doSave(Iterable instances, Class domainType) { - return transactionTemplate - .execute(tx -> { - // empty check - if (!instances.iterator().hasNext()) { - return Collections.emptyList(); - } + if (!instances.iterator().hasNext()) { + return Collections.emptyList(); + } - Class resultType = TemplateSupport.findCommonElementType(instances); + Class resultType = Objects.requireNonNull(TemplateSupport.findCommonElementType(instances), () -> "Could not find a common type element to store and then project multiple instances of type %s".formatted(domainType)); + return execute(tx -> { Collection pps = PropertyFilterSupport.addPropertiesFrom(domainType, resultType, getProjectionFactory(), neo4jMappingContext); @@ -1249,7 +1249,9 @@ public final class Neo4jTemplate implements EntityFromDtoInstantiatingConverter converter = new EntityFromDtoInstantiatingConverter<>(domainType, neo4jMappingContext); for (R instance : instances) { T domainObject = converter.convert(instance); - + if (domainObject == null) { + continue; + } T savedEntity = saveImpl(domainObject, pps, stateMachine); @SuppressWarnings("unchecked") @@ -1271,11 +1273,12 @@ public final class Neo4jTemplate implements DefaultExecutableQuery(PreparedQuery preparedQuery, boolean readOnly) { this.preparedQuery = preparedQuery; - this.txTemplate = readOnly ? transactionTemplateReadOnly : transactionTemplate; + // At this time, both must be initialized + this.txTemplate = Objects.requireNonNull(readOnly ? transactionTemplateReadOnly : transactionTemplate); } - @SuppressWarnings("unchecked") + @SuppressWarnings({"unchecked", "NullAway"}) public List getResults() { return txTemplate .execute(tx -> { @@ -1287,7 +1290,7 @@ public final class Neo4jTemplate implements }); } - @SuppressWarnings("unchecked") + @SuppressWarnings({"unchecked", "NullAway"}) public Optional getSingleResult() { return txTemplate.execute(tx -> { try { @@ -1304,7 +1307,7 @@ public final class Neo4jTemplate implements }); } - @SuppressWarnings("unchecked") + @SuppressWarnings({"unchecked", "NullAway"}) public T getRequiredSingleResult() { return txTemplate.execute(tx -> { Optional one = createFetchSpec().flatMap(Neo4jClient.RecordFetchSpec::one); @@ -1326,11 +1329,12 @@ public final class Neo4jTemplate implements boolean containsPossibleCircles = entityMetaData != null && entityMetaData.containsPossibleCircles(queryFragments::includeField); if (cypherQuery == null || containsPossibleCircles) { Statement statement; - if (containsPossibleCircles && !queryFragments.isScalarValueReturn()) { + // The null check for the metadata is superfluous, but the easiest way to make NullAway happy + if (entityMetaData != null && containsPossibleCircles && !queryFragments.isScalarValueReturn()) { NodesAndRelationshipsByIdStatementProvider nodesAndRelationshipsById = createNodesAndRelationshipsByIdStatementProvider(entityMetaData, queryFragments, queryFragmentsAndParameters.getParameters()); - if (nodesAndRelationshipsById.hasRootNodeIds()) { + if (!nodesAndRelationshipsById.hasRootNodeIds()) { return Optional.empty(); } statement = nodesAndRelationshipsById.toStatement(entityMetaData); @@ -1341,7 +1345,7 @@ public final class Neo4jTemplate implements finalParameters = TemplateSupport.mergeParameters(statement, finalParameters); } - Neo4jClient.MappingSpec newMappingSpec = neo4jClient.query(cypherQuery) + Neo4jClient.MappingSpec newMappingSpec = neo4jClient.query(Objects.requireNonNull(cypherQuery, "Could not compute a query")) .bindAll(finalParameters).fetchAs(preparedQuery.getResultType()); return preparedQuery.getOptionalMappingFunction() .map(newMappingSpec::mappedBy).or(() -> Optional.of(newMappingSpec)); @@ -1364,7 +1368,7 @@ public final class Neo4jTemplate implements .fetchAs(Value.class).mappedBy((t, r) -> r.get(Constants.NAME_OF_SYNTHESIZED_ROOT_NODE)) .one() .map(value -> value.asList(TemplateSupport::convertIdOrElementIdToString)) - .get()); + .orElseThrow()); if (rootNodeIds.isEmpty()) { // fast return if no matching root node(s) are found @@ -1397,11 +1401,15 @@ public final class Neo4jTemplate implements Neo4jPersistentEntity target = (Neo4jPersistentEntity) sourceRelationshipDescription.getTarget(); @SuppressWarnings("unchecked") - String fieldName = ((Association) sourceRelationshipDescription).getInverse().getFieldName(); - PropertyPathWalkStep nextPathStep = currentPathStep.with((sourceRelationshipDescription.hasRelationshipProperties() ? - fieldName + "." + ((Neo4jPersistentEntity) sourceRelationshipDescription.getRelationshipPropertiesEntity()) - .getPersistentProperty(TargetNode.class).getFieldName() : fieldName)); - + String fieldName = ((Association<@NonNull Neo4jPersistentProperty>) sourceRelationshipDescription).getInverse().getFieldName(); + PropertyPathWalkStep nextPathStep; + Neo4jPersistentEntity relationshipPropertiesEntity = (Neo4jPersistentEntity) sourceRelationshipDescription.getRelationshipPropertiesEntity(); + if (sourceRelationshipDescription.hasRelationshipProperties() && relationshipPropertiesEntity != null) { + var targetNodeProperty = Objects.requireNonNull(relationshipPropertiesEntity.getPersistentProperty(TargetNode.class), () -> "Could not get target node property on %s".formatted(relationshipPropertiesEntity.getType())); + nextPathStep = currentPathStep.with(fieldName + "." + targetNodeProperty.getFieldName()); + } else { + nextPathStep = currentPathStep.with(fieldName); + } Collection relationships = target .getRelationshipsInHierarchy( @@ -1430,7 +1438,6 @@ public final class Neo4jTemplate implements } } - @NonNull private Consumer> iterateAndMapNextLevel(Map> relationshipsToRelatedNodes, RelationshipDescription relationshipDescription, PropertyPathWalkStep currentPathStep) { @@ -1439,9 +1446,11 @@ public final class Neo4jTemplate implements Map> relatedNodesVisited = new HashMap<>(relationshipsToRelatedNodes); @SuppressWarnings("unchecked") - List newRelationshipIds = ((List) record.get(Constants.NAME_OF_SYNTHESIZED_RELATIONS)).stream().map(TemplateSupport::convertIdOrElementIdToString).toList(); + var sr = (List) record.get(Constants.NAME_OF_SYNTHESIZED_RELATIONS); + List newRelationshipIds = (sr != null) ? sr.stream().map(TemplateSupport::convertIdOrElementIdToString).toList() : List.of(); @SuppressWarnings("unchecked") - Set relatedIds = new HashSet<>(((List) record.get(Constants.NAME_OF_SYNTHESIZED_RELATED_NODES)).stream().map(TemplateSupport::convertIdOrElementIdToString).toList()); + var srn = (List) record.get(Constants.NAME_OF_SYNTHESIZED_RELATED_NODES); + Set relatedIds = (srn != null) ? new HashSet<>(srn.stream().map(TemplateSupport::convertIdOrElementIdToString).toList()) : Set.of(); // use this list to get down the road // 1. remove already visited ones; diff --git a/src/main/java/org/springframework/data/neo4j/core/PreparedQuery.java b/src/main/java/org/springframework/data/neo4j/core/PreparedQuery.java index 2c8e73b81..64488219d 100644 --- a/src/main/java/org/springframework/data/neo4j/core/PreparedQuery.java +++ b/src/main/java/org/springframework/data/neo4j/core/PreparedQuery.java @@ -16,6 +16,7 @@ package org.springframework.data.neo4j.core; import org.apiguardian.api.API; +import org.jspecify.annotations.Nullable; import org.neo4j.driver.Record; import org.neo4j.driver.Value; import org.neo4j.driver.Values; @@ -27,7 +28,6 @@ import org.springframework.data.neo4j.core.mapping.Constants; import org.springframework.data.neo4j.core.mapping.MappingSupport; import org.springframework.data.neo4j.core.mapping.NoRootNodeMappingException; import org.springframework.data.neo4j.repository.query.QueryFragmentsAndParameters; -import org.springframework.lang.Nullable; import java.util.Collection; import java.util.Collections; @@ -35,6 +35,7 @@ import java.util.HashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; @@ -66,7 +67,9 @@ public final class PreparedQuery { private final Class resultType; private final QueryFragmentsAndParameters queryFragmentsAndParameters; - private final @Nullable Supplier> mappingFunctionSupplier; + @Nullable + private final Supplier> mappingFunctionSupplier; + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") private volatile Optional> lastMappingFunction = Optional.empty(); private PreparedQuery(OptionalBuildSteps optionalBuildSteps) { @@ -127,7 +130,8 @@ public final class PreparedQuery { final Class resultType; final QueryFragmentsAndParameters queryFragmentsAndParameters; - @Nullable Supplier> mappingFunctionSupplier; + @Nullable + Supplier> mappingFunctionSupplier; OptionalBuildSteps(Class resultType, QueryFragmentsAndParameters queryFragmentsAndParameters) { this.resultType = resultType; @@ -140,8 +144,8 @@ public final class PreparedQuery { * @param newParameters The new parameters for the prepared query. * @return This builder. */ - public OptionalBuildSteps withParameters(Map newParameters) { - this.queryFragmentsAndParameters.setParameters(newParameters); + public OptionalBuildSteps withParameters(@Nullable Map newParameters) { + this.queryFragmentsAndParameters.setParameters(Objects.requireNonNullElseGet(newParameters, Map::of)); return this; } @@ -229,6 +233,9 @@ public final class PreparedQuery { } @Override + // Suppressing the warnings for accessing `pathValues`: `partitioningBy` + // will always provide entries for `true` and `false` + @SuppressWarnings("NullAway") public Object apply(TypeSystem t, Record r) { if (r.size() == 1) { diff --git a/src/main/java/org/springframework/data/neo4j/core/PropertyFilterSupport.java b/src/main/java/org/springframework/data/neo4j/core/PropertyFilterSupport.java index 0ae8b79b3..376691fa2 100644 --- a/src/main/java/org/springframework/data/neo4j/core/PropertyFilterSupport.java +++ b/src/main/java/org/springframework/data/neo4j/core/PropertyFilterSupport.java @@ -16,6 +16,7 @@ package org.springframework.data.neo4j.core; import org.apiguardian.api.API; +import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.mapping.PropertyPath; import org.springframework.data.neo4j.core.mapping.GraphPropertyDescription; import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; @@ -27,12 +28,13 @@ import org.springframework.data.projection.ProjectionInformation; import org.springframework.data.repository.query.ResultProcessor; import org.springframework.data.repository.query.ReturnedType; import org.springframework.data.util.TypeInformation; -import org.springframework.lang.Nullable; import java.beans.PropertyDescriptor; import java.util.Collection; import java.util.Collections; import java.util.HashSet; +import java.util.Objects; +import java.util.Optional; /** * This class is responsible for creating a List of {@link PropertyPath} entries that contains all reachable @@ -85,7 +87,7 @@ public final class PropertyFilterSupport { // try to figure out the right property by name for (GraphPropertyDescription graphProperty : domainEntity.getGraphProperties()) { if (graphProperty.getPropertyName().equals(inputProperty.getName())) { - typeInformation = domainEntity.getPersistentProperty(graphProperty.getFieldName()).getTypeInformation(); + typeInformation = Optional.ofNullable(domainEntity.getPersistentProperty(graphProperty.getFieldName())).map(PersistentProperty::getTypeInformation).orElse(null); break; } } @@ -93,13 +95,15 @@ public final class PropertyFilterSupport { if (typeInformation == null) { for (RelationshipDescription relationshipDescription : domainEntity.getRelationships()) { if (relationshipDescription.getFieldName().equals(inputProperty.getName())) { - typeInformation = domainEntity.getPersistentProperty(relationshipDescription.getFieldName()).getTypeInformation(); + typeInformation = Optional.ofNullable(domainEntity.getPersistentProperty(relationshipDescription.getFieldName())).map(PersistentProperty::getTypeInformation).orElse(null); break; } } } } - addPropertiesFrom(domainType, returnType, projectionFactory, propertyPaths, new ProjectionPathProcessor(inputProperty.getName(), typeInformation), neo4jMappingContext); + if (typeInformation != null) { + addPropertiesFrom(domainType, returnType, projectionFactory, propertyPaths, new ProjectionPathProcessor(inputProperty.getName(), typeInformation), neo4jMappingContext); + } } return propertyPaths; } @@ -127,16 +131,18 @@ public final class PropertyFilterSupport { TypeInformation mapValueType = projectionPathProcessor.typeInformation.getRequiredMapValueType(); if (mapValueType.isCollectionLike()) { currentTypeInformation = projectionPathProcessor.typeInformation.getRequiredMapValueType().getComponentType(); - propertyType = projectionPathProcessor.typeInformation.getRequiredMapValueType().getComponentType().getType(); + propertyType = Objects.requireNonNull(currentTypeInformation, "Cannot retrieve collection type").getType(); } else { currentTypeInformation = projectionPathProcessor.typeInformation.getRequiredMapValueType(); - propertyType = projectionPathProcessor.typeInformation.getRequiredMapValueType().getType(); + propertyType = currentTypeInformation.getType(); } } else if (projectionPathProcessor.typeInformation.isCollectionLike()) { currentTypeInformation = projectionPathProcessor.typeInformation.getComponentType(); - propertyType = projectionPathProcessor.typeInformation.getComponentType().getType(); + propertyType = Objects.requireNonNull(currentTypeInformation, "Cannot retrieve collection type").getType(); } + Objects.requireNonNull(currentTypeInformation, "Property type is required"); + // 1. Simple types can be added directly // 2. Something that looks like an entity needs to get processed as such // 3. Embedded projection @@ -150,17 +156,18 @@ public final class PropertyFilterSupport { if (nestedProjectionInformation.isClosed()) { filteredProperties.add(new PropertyFilter.ProjectedPath(propertyPath, false)); for (PropertyDescriptor nestedInputProperty : nestedProjectionInformation.getInputProperties()) { - TypeInformation typeInformation = currentTypeInformation.getProperty(nestedInputProperty.getName()); + TypeInformation typeInformation = currentTypeInformation.getRequiredProperty(nestedInputProperty.getName()); ProjectionPathProcessor nextProjectionPathProcessor = projectionPathProcessor.next(nestedInputProperty, typeInformation); + TypeInformation actualType = Objects.requireNonNull(nextProjectionPathProcessor.typeInformation.getActualType()); if (projectionPathProcessor.isChildLevel() && (domainType.equals(nextProjectionPathProcessor.typeInformation.getType()) - || returnedType.equals(nextProjectionPathProcessor.typeInformation.getActualType().getType()) + || returnedType.equals(actualType.getType()) || returnedType.equals(nextProjectionPathProcessor.typeInformation.getType()))) { break; } - if (projectionPathProcessor.typeInformation.getActualType().getType().equals(nextProjectionPathProcessor.typeInformation.getActualType().getType()) + if (projectionPathProcessor.typeInformation.getActualType() != null && projectionPathProcessor.typeInformation.getActualType().getType().equals(actualType.getType()) || (!projectionPathProcessor.typeInformation.isCollectionLike() && !projectionPathProcessor.typeInformation.isMap() && projectionPathProcessor.typeInformation.getType().equals(nextProjectionPathProcessor.typeInformation.getType()))) { filteredProperties.add(new PropertyFilter.ProjectedPath(propertyPath, true)); } else { @@ -182,13 +189,13 @@ public final class PropertyFilterSupport { final String path; final String name; - private ProjectionPathProcessor(String name, String path, @Nullable TypeInformation typeInformation) { + private ProjectionPathProcessor(String name, String path, TypeInformation typeInformation) { this.typeInformation = typeInformation; this.path = path; this.name = name; } - private ProjectionPathProcessor(String name, @Nullable TypeInformation typeInformation) { + private ProjectionPathProcessor(String name, TypeInformation typeInformation) { this(name, name, typeInformation); } diff --git a/src/main/java/org/springframework/data/neo4j/core/ReactiveFluentFindOperation.java b/src/main/java/org/springframework/data/neo4j/core/ReactiveFluentFindOperation.java index 8ed6070b6..2c48969b2 100644 --- a/src/main/java/org/springframework/data/neo4j/core/ReactiveFluentFindOperation.java +++ b/src/main/java/org/springframework/data/neo4j/core/ReactiveFluentFindOperation.java @@ -24,7 +24,6 @@ import java.util.Map; import org.apiguardian.api.API; import org.neo4j.cypherdsl.core.Statement; import org.springframework.data.neo4j.repository.query.QueryFragmentsAndParameters; -import org.springframework.lang.Nullable; /** * {@link ReactiveFluentFindOperation} allows creation and execution of Neo4j find operations in a fluent API style. @@ -95,7 +94,7 @@ public interface ReactiveFluentFindOperation { * @return new instance of {@link TerminatingFind}. * @throws IllegalArgumentException if query is {@literal null}. */ - TerminatingFind matching(String query, @Nullable Map parameter); + TerminatingFind matching(String query, Map parameter); /** * Creates an executable query based on fragments and parameters. Hardly useful outside framework-code @@ -126,7 +125,7 @@ public interface ReactiveFluentFindOperation { * @return new instance of {@link TerminatingFind}. * @throws IllegalArgumentException if statement is {@literal null}. */ - TerminatingFind matching(Statement statement, @Nullable Map parameter); + TerminatingFind matching(Statement statement, Map parameter); /** * Set the filter {@link Statement statement} to be used. diff --git a/src/main/java/org/springframework/data/neo4j/core/ReactiveFluentOperationSupport.java b/src/main/java/org/springframework/data/neo4j/core/ReactiveFluentOperationSupport.java index 1d94d2af2..ddb4aa631 100644 --- a/src/main/java/org/springframework/data/neo4j/core/ReactiveFluentOperationSupport.java +++ b/src/main/java/org/springframework/data/neo4j/core/ReactiveFluentOperationSupport.java @@ -21,6 +21,7 @@ import reactor.core.publisher.Mono; import java.util.Collections; import java.util.Map; +import org.jspecify.annotations.Nullable; import org.neo4j.cypherdsl.core.Statement; import org.springframework.data.neo4j.repository.query.QueryFragmentsAndParameters; import org.springframework.util.Assert; @@ -54,12 +55,15 @@ final class ReactiveFluentOperationSupport implements ReactiveFluentFindOperatio private final ReactiveNeo4jTemplate template; private final Class domainType; private final Class returnType; + @Nullable private final String query; + @Nullable private final Map parameters; + @Nullable private final QueryFragmentsAndParameters queryFragmentsAndParameters; - ExecutableFindSupport(ReactiveNeo4jTemplate template, Class domainType, Class returnType, String query, - Map parameters) { + ExecutableFindSupport(ReactiveNeo4jTemplate template, Class domainType, Class returnType, @Nullable String query, + @Nullable Map parameters) { this.template = template; this.domainType = domainType; this.returnType = returnType; @@ -68,7 +72,7 @@ final class ReactiveFluentOperationSupport implements ReactiveFluentFindOperatio this.queryFragmentsAndParameters = null; } - ExecutableFindSupport(ReactiveNeo4jTemplate template, Class domainType, Class returnType, QueryFragmentsAndParameters queryFragmentsAndParameters) { + ExecutableFindSupport(ReactiveNeo4jTemplate template, Class domainType, Class returnType, @Nullable QueryFragmentsAndParameters queryFragmentsAndParameters) { this.template = template; this.domainType = domainType; this.returnType = returnType; diff --git a/src/main/java/org/springframework/data/neo4j/core/ReactiveNeo4jClient.java b/src/main/java/org/springframework/data/neo4j/core/ReactiveNeo4jClient.java index a31081ce9..0baa56a74 100644 --- a/src/main/java/org/springframework/data/neo4j/core/ReactiveNeo4jClient.java +++ b/src/main/java/org/springframework/data/neo4j/core/ReactiveNeo4jClient.java @@ -15,6 +15,7 @@ */ package org.springframework.data.neo4j.core; +import org.jspecify.annotations.Nullable; import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -34,7 +35,6 @@ import org.neo4j.driver.types.TypeSystem; import org.springframework.core.log.LogAccessor; import org.springframework.data.neo4j.core.Neo4jClient.BindSpec; import org.springframework.data.neo4j.core.convert.Neo4jConversions; -import org.springframework.lang.Nullable; /** * Reactive Neo4j client. The main difference to the {@link Neo4jClient imperative Neo4j client} is the fact that all @@ -123,7 +123,7 @@ public interface ReactiveNeo4jClient { * @return The builder * @since 6.3.3 */ - public Builder withNeo4jConversions(Neo4jConversions neo4jConversions) { + public Builder withNeo4jConversions(@Nullable Neo4jConversions neo4jConversions) { this.neo4jConversions = neo4jConversions; return this; } @@ -137,7 +137,7 @@ public interface ReactiveNeo4jClient { * @return The builder * @since 7.1.2 */ - public Builder withNeo4jBookmarkManager(Neo4jBookmarkManager bookmarkManager) { + public Builder withNeo4jBookmarkManager(@Nullable Neo4jBookmarkManager bookmarkManager) { this.bookmarkManager = bookmarkManager; return this; } @@ -306,7 +306,7 @@ public interface ReactiveNeo4jClient { * @param targetDatabase selected database to use. A {@literal null} value indicates the default database. * @return A runnable query specification that is now bound to a given database. */ - RunnableSpecBoundToDatabase in(@Nullable String targetDatabase); + RunnableSpecBoundToDatabase in(String targetDatabase); /** * Pins the previously defined query to an impersonated user. A value of {@literal null} chooses the user owning @@ -315,7 +315,7 @@ public interface ReactiveNeo4jClient { * @param asUser The name of the user to impersonate. A {@literal null} value indicates the connected user. * @return A runnable query specification that is now bound to a given database. */ - RunnableSpecBoundToUser asUser(@Nullable String asUser); + RunnableSpecBoundToUser asUser(String asUser); } /** @@ -361,7 +361,7 @@ public interface ReactiveNeo4jClient { * @param targetDatabase selected database to use. A {@literal null} value indicates the default database. * @return An ongoing delegation */ - RunnableDelegation in(@Nullable String targetDatabase); + RunnableDelegation in(String targetDatabase); } /** diff --git a/src/main/java/org/springframework/data/neo4j/core/ReactiveNeo4jOperations.java b/src/main/java/org/springframework/data/neo4j/core/ReactiveNeo4jOperations.java index d08cff905..87146101a 100644 --- a/src/main/java/org/springframework/data/neo4j/core/ReactiveNeo4jOperations.java +++ b/src/main/java/org/springframework/data/neo4j/core/ReactiveNeo4jOperations.java @@ -15,6 +15,7 @@ */ package org.springframework.data.neo4j.core; +import org.jspecify.annotations.Nullable; import org.springframework.data.mapping.PropertyPath; import org.springframework.data.neo4j.core.mapping.Neo4jPersistentProperty; import org.springframework.data.neo4j.repository.query.QueryFragmentsAndParameters; @@ -27,7 +28,6 @@ import java.util.function.BiPredicate; import org.apiguardian.api.API; import org.neo4j.cypherdsl.core.Statement; import org.springframework.dao.IncorrectResultSizeDataAccessException; -import org.springframework.lang.Nullable; /** * Specifies reactive operations one can perform on a database, based on an Domain Type. @@ -272,8 +272,7 @@ public interface ReactiveNeo4jOperations { */ Mono deleteById(Object id, Class domainType); - Mono deleteByIdWithVersion(Object id, Class domainType, Neo4jPersistentProperty versionProperty, - @Nullable Object versionValue); + Mono deleteByIdWithVersion(Object id, Class domainType, Neo4jPersistentProperty versionProperty, @Nullable Object versionValue); /** * Deletes all entities with one of the given ids, including all entities related to that entity. diff --git a/src/main/java/org/springframework/data/neo4j/core/ReactiveNeo4jTemplate.java b/src/main/java/org/springframework/data/neo4j/core/ReactiveNeo4jTemplate.java index 8e8bceca6..b37e3a2e3 100644 --- a/src/main/java/org/springframework/data/neo4j/core/ReactiveNeo4jTemplate.java +++ b/src/main/java/org/springframework/data/neo4j/core/ReactiveNeo4jTemplate.java @@ -15,8 +15,33 @@ */ package org.springframework.data.neo4j.core; +import static org.neo4j.cypherdsl.core.Cypher.anyNode; +import static org.neo4j.cypherdsl.core.Cypher.asterisk; +import static org.neo4j.cypherdsl.core.Cypher.parameter; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BiFunction; +import java.util.function.BiPredicate; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; + import org.apache.commons.logging.LogFactory; import org.apiguardian.api.API; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; import org.neo4j.cypherdsl.core.Condition; import org.neo4j.cypherdsl.core.Cypher; import org.neo4j.cypherdsl.core.FunctionInvocation; @@ -26,7 +51,6 @@ import org.neo4j.cypherdsl.core.Statement; import org.neo4j.cypherdsl.core.renderer.Configuration; import org.neo4j.cypherdsl.core.renderer.Renderer; import org.neo4j.driver.Value; -import org.neo4j.driver.Values; import org.neo4j.driver.types.Entity; import org.neo4j.driver.types.MapAccessor; import org.neo4j.driver.types.TypeSystem; @@ -72,42 +96,17 @@ import org.springframework.data.neo4j.repository.query.QueryFragmentsAndParamete import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.projection.ProjectionInformation; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; -import org.springframework.data.util.TypeInformation; -import org.springframework.lang.NonNull; -import org.springframework.lang.Nullable; import org.springframework.transaction.ReactiveTransactionManager; import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.reactive.TransactionalOperator; import org.springframework.util.Assert; + import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.util.function.Tuple2; import reactor.util.function.Tuple3; import reactor.util.function.Tuples; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.BiFunction; -import java.util.function.BiPredicate; -import java.util.function.Function; -import java.util.function.Supplier; -import java.util.stream.Collectors; - -import static org.neo4j.cypherdsl.core.Cypher.anyNode; -import static org.neo4j.cypherdsl.core.Cypher.asterisk; -import static org.neo4j.cypherdsl.core.Cypher.parameter; - /** * @author Michael J. Simons * @author Gerrit Meier @@ -138,14 +137,18 @@ public final class ReactiveNeo4jTemplate implements } }; + @Nullable private TransactionalOperator transactionalOperatorReadOnly; + @Nullable private TransactionalOperator transactionalOperator; + @Nullable private ClassLoader beanClassLoader; private ReactiveEventSupport eventSupport; + @Nullable private ProjectionFactory projectionFactory; private Renderer renderer; @@ -202,13 +205,29 @@ public final class ReactiveNeo4jTemplate implements public Mono count(String cypherQuery, Map parameters) { PreparedQuery preparedQuery = PreparedQuery.queryFor(Long.class).withCypherQuery(cypherQuery) .withParameters(parameters).build(); - return transactionalOperatorReadOnly.transactional(this.toExecutableQuery(preparedQuery).flatMap(ExecutableQuery::getSingleResult)); + return executeReadOnly(this.toExecutableQuery(preparedQuery).flatMap(ExecutableQuery::getSingleResult)); + } + + private Mono executeReadOnly(Mono action) { + return Objects.requireNonNull(this.transactionalOperatorReadOnly).transactional(action); + } + + private Flux executeReadOnly(Flux action) { + return Objects.requireNonNull(this.transactionalOperatorReadOnly).transactional(action); + } + + private Mono execute(Mono action) { + return Objects.requireNonNull(this.transactionalOperator).transactional(action); + } + + private Flux execute(Flux action) { + return Objects.requireNonNull(this.transactionalOperator).transactional(action); } @Override public Flux findAll(Class domainType) { - return transactionalOperatorReadOnly.transactional(doFindAll(domainType, null)); + return executeReadOnly(doFindAll(domainType, null)); } private Flux doFindAll(Class domainType, @Nullable Class resultType) { @@ -221,34 +240,34 @@ public final class ReactiveNeo4jTemplate implements @Override public Flux findAll(Statement statement, Class domainType) { - return transactionalOperatorReadOnly.transactional(createExecutableQuery(domainType, statement).flatMapMany(ExecutableQuery::getResults)); + return executeReadOnly(createExecutableQuery(domainType, statement).flatMapMany(ExecutableQuery::getResults)); } @Override public Flux findAll(Statement statement, Map parameters, Class domainType) { - return transactionalOperatorReadOnly.transactional(createExecutableQuery(domainType, null, statement, parameters).flatMapMany(ExecutableQuery::getResults)); + return executeReadOnly(createExecutableQuery(domainType, null, statement, parameters).flatMapMany(ExecutableQuery::getResults)); } @Override public Mono findOne(Statement statement, Map parameters, Class domainType) { - return transactionalOperatorReadOnly.transactional(createExecutableQuery(domainType, null, statement, parameters).flatMap(ExecutableQuery::getSingleResult)); + return executeReadOnly(createExecutableQuery(domainType, null, statement, parameters).flatMap(ExecutableQuery::getSingleResult)); } @Override public Flux findAll(String cypherQuery, Class domainType) { - return transactionalOperatorReadOnly.transactional(createExecutableQuery(domainType, cypherQuery).flatMapMany(ExecutableQuery::getResults)); + return executeReadOnly(createExecutableQuery(domainType, cypherQuery).flatMapMany(ExecutableQuery::getResults)); } @Override public Flux findAll(String cypherQuery, Map parameters, Class domainType) { - return transactionalOperatorReadOnly.transactional(createExecutableQuery(domainType, null, cypherQuery, parameters).flatMapMany(ExecutableQuery::getResults)); + return executeReadOnly(createExecutableQuery(domainType, null, cypherQuery, parameters).flatMapMany(ExecutableQuery::getResults)); } @Override public Mono findOne(String cypherQuery, Map parameters, Class domainType) { - return transactionalOperatorReadOnly.transactional(createExecutableQuery(domainType, null, cypherQuery, parameters).flatMap(ExecutableQuery::getSingleResult)); + return executeReadOnly(createExecutableQuery(domainType, null, cypherQuery, parameters).flatMap(ExecutableQuery::getSingleResult)); } @Override @@ -265,15 +284,15 @@ public final class ReactiveNeo4jTemplate implements } else { Mono> executableQuery; if (queryFragmentsAndParameters == null) { - executableQuery = createExecutableQuery(domainType, resultType, cypherQuery, + executableQuery = createExecutableQuery(domainType, resultType, Objects.requireNonNull(cypherQuery), parameters == null ? Collections.emptyMap() : parameters); } else { executableQuery = createExecutableQuery(domainType, resultType, queryFragmentsAndParameters); } intermediaResults = switch (fetchType) { - case ALL -> transactionalOperatorReadOnly.transactional(executableQuery.flatMapMany(ExecutableQuery::getResults)); - case ONE -> transactionalOperatorReadOnly.transactional(executableQuery.flatMap(ExecutableQuery::getSingleResult).flux()); + case ALL -> executeReadOnly(executableQuery.flatMapMany(ExecutableQuery::getResults)); + case ONE -> executeReadOnly(executableQuery.flatMap(ExecutableQuery::getSingleResult).flux()); }; } @@ -296,7 +315,7 @@ public final class ReactiveNeo4jTemplate implements Neo4jPersistentEntity entityMetaData = neo4jMappingContext.getRequiredPersistentEntity(domainType); QueryFragmentsAndParameters fragmentsAndParameters = QueryFragmentsAndParameters - .forExistsById(entityMetaData, convertIdValues(entityMetaData.getRequiredIdProperty(), id)); + .forExistsById(entityMetaData, TemplateSupport.convertIdValues(this.neo4jMappingContext, entityMetaData.getRequiredIdProperty(), id)); Statement statement = fragmentsAndParameters.getQueryFragments().toStatement(); Map parameters = fragmentsAndParameters.getParameters(); @@ -309,9 +328,9 @@ public final class ReactiveNeo4jTemplate implements Neo4jPersistentEntity entityMetaData = neo4jMappingContext.getRequiredPersistentEntity(domainType); - return transactionalOperatorReadOnly.transactional(createExecutableQuery(domainType, null, + return executeReadOnly(createExecutableQuery(domainType, null, QueryFragmentsAndParameters.forFindById(entityMetaData, - convertIdValues(entityMetaData.getRequiredIdProperty(), id))) + TemplateSupport.convertIdValues(this.neo4jMappingContext, entityMetaData.getRequiredIdProperty(), id))) .flatMap(ExecutableQuery::getSingleResult)); } @@ -320,9 +339,9 @@ public final class ReactiveNeo4jTemplate implements Neo4jPersistentEntity entityMetaData = neo4jMappingContext.getRequiredPersistentEntity(domainType); - return transactionalOperatorReadOnly.transactional(createExecutableQuery(domainType, null, + return executeReadOnly(createExecutableQuery(domainType, null, QueryFragmentsAndParameters.forFindByAllId(entityMetaData, - convertIdValues(entityMetaData.getRequiredIdProperty(), ids))) + TemplateSupport.convertIdValues(this.neo4jMappingContext, entityMetaData.getRequiredIdProperty(), ids))) .flatMapMany(ExecutableQuery::getResults)); } @@ -333,36 +352,20 @@ public final class ReactiveNeo4jTemplate implements return createExecutableQuery(domainType, null, queryFragmentsAndParameters); } - private Object convertIdValues(@Nullable Neo4jPersistentProperty idProperty, @Nullable Object idValues) { - - if (idProperty != null && ((Neo4jPersistentEntity) idProperty.getOwner()).isUsingInternalIds()) { - return idValues; - } - - if (idValues != null) { - return neo4jMappingContext.getConversionService().writeValue(idValues, TypeInformation.of(idValues.getClass()), idProperty == null ? null : idProperty.getOptionalConverter()); - } else if (idProperty != null) { - return neo4jMappingContext.getConversionService().writeValue(idValues, idProperty.getTypeInformation(), idProperty.getOptionalConverter()); - } else { - // Not much we can convert here - return Values.NULL; - } - } - @Override public Mono save(T instance) { - return transactionalOperator.transactional(saveImpl(instance, Collections.emptySet(), null)); + return execute(saveImpl(instance, Collections.emptySet(), null)); } @Override public Mono saveAs(T instance, BiPredicate includeProperty) { if (instance == null) { - return null; + return Mono.empty(); } - return transactionalOperator.transactional(saveImpl(instance, TemplateSupport.computeIncludedPropertiesFromPredicate(this.neo4jMappingContext, instance.getClass(), includeProperty), null)); + return execute(saveImpl(instance, TemplateSupport.computeIncludedPropertiesFromPredicate(this.neo4jMappingContext, instance.getClass(), includeProperty), null)); } @Override @@ -371,7 +374,7 @@ public final class ReactiveNeo4jTemplate implements Assert.notNull(resultType, "ResultType must not be null"); if (instance == null) { - return null; + return Mono.empty(); } if (resultType.equals(instance.getClass())) { @@ -383,7 +386,7 @@ public final class ReactiveNeo4jTemplate implements Collection pps = PropertyFilterSupport.addPropertiesFrom(instance.getClass(), resultType, localProjectionFactory, neo4jMappingContext); - Mono savingPublisher = transactionalOperator.transactional(saveImpl(instance, pps, null)); + Mono savingPublisher = execute(saveImpl(instance, pps, null)); if (!resultType.isInterface()) { return savingPublisher.map(savedInstance -> { @@ -399,9 +402,9 @@ public final class ReactiveNeo4jTemplate implements return savingPublisher.flatMap(savedInstance -> { Neo4jPersistentEntity entityMetaData = neo4jMappingContext.getRequiredPersistentEntity(savedInstance.getClass()); - Neo4jPersistentProperty idProperty = entityMetaData.getIdProperty(); + Neo4jPersistentProperty idProperty = entityMetaData.getRequiredIdProperty(); PersistentPropertyAccessor propertyAccessor = entityMetaData.getPropertyAccessor(savedInstance); - return transactionalOperatorReadOnly.transactional(this.findById(propertyAccessor.getProperty(idProperty), savedInstance.getClass()) + return executeReadOnly(this.findById(Objects.requireNonNull(propertyAccessor.getProperty(idProperty)), savedInstance.getClass()) .map(loadedValue -> localProjectionFactory.createProjection(resultType, loadedValue))); }); } @@ -412,7 +415,7 @@ public final class ReactiveNeo4jTemplate implements return Flux.empty(); } - Class resultType = TemplateSupport.findCommonElementType(instances); + Class resultType = Objects.requireNonNull(TemplateSupport.findCommonElementType(instances), () -> "Could not find a common type element to store and then project multiple instances of type %s".formatted(domainType)); Collection pps = PropertyFilterSupport.addPropertiesFrom(domainType, resultType, getProjectionFactory(), neo4jMappingContext); @@ -423,20 +426,24 @@ public final class ReactiveNeo4jTemplate implements return Flux.fromIterable(instances) .concatMap(instance -> { T domainObject = converter.convert(instance); + if (domainObject == null) { + return Mono.empty(); + } @SuppressWarnings("unchecked") - Mono result = transactionalOperator.transactional(saveImpl(domainObject, pps, stateMachine, knownRelationshipsIds) + Mono result = execute(saveImpl(domainObject, pps, stateMachine, knownRelationshipsIds) .map(savedEntity -> (R) new DtoInstantiatingConverter(resultType, neo4jMappingContext).convertDirectly(savedEntity))); return result; }); } - private Mono saveImpl(T instance, @Nullable Collection includedProperties, @Nullable NestedRelationshipProcessingStateMachine stateMachine) { + private Mono saveImpl(T instance, Collection includedProperties, @Nullable NestedRelationshipProcessingStateMachine stateMachine) { return saveImpl(instance, includedProperties, stateMachine, new HashSet<>()); } - private Mono saveImpl(T instance, @Nullable Collection includedProperties, @Nullable NestedRelationshipProcessingStateMachine stateMachine, Collection knownRelationshipsIds) { + @SuppressWarnings("deprecation") + private Mono saveImpl(T instance, Collection includedProperties, @Nullable NestedRelationshipProcessingStateMachine stateMachine, Collection knownRelationshipsIds) { if (stateMachine != null && stateMachine.hasProcessedValue(instance)) { return Mono.just(instance); @@ -500,7 +507,7 @@ public final class ReactiveNeo4jTemplate implements Neo4jPersistentProperty idProperty = entityMetaData.getRequiredIdProperty(); ReactiveNeo4jClient.RunnableSpec runnableQuery = neo4jClient .query(() -> renderer.render(cypherGenerator.createStatementReturningDynamicLabels(entityMetaData))) - .bind(convertIdValues(idProperty, propertyAccessor.getProperty(idProperty))) + .bind(TemplateSupport.convertIdValues(this.neo4jMappingContext, idProperty, propertyAccessor.getProperty(idProperty))) .to(Constants.NAME_OF_ID).bind(entityMetaData.getStaticLabels()).to(Constants.NAME_OF_STATIC_LABELS_PARAM); if (entityMetaData.hasVersionProperty()) { @@ -518,13 +525,13 @@ public final class ReactiveNeo4jTemplate implements @Override public Flux saveAll(Iterable instances) { - return transactionalOperator.transactional(saveAllImpl(instances, Collections.emptySet(), null)); + return execute(saveAllImpl(instances, Collections.emptySet(), null)); } @Override public Flux saveAllAs(Iterable instances, BiPredicate includeProperty) { - return transactionalOperator.transactional(saveAllImpl(instances, null, includeProperty)); + return execute(saveAllImpl(instances, null, includeProperty)); } @Override @@ -552,20 +559,21 @@ public final class ReactiveNeo4jTemplate implements Collection pps = PropertyFilterSupport.addPropertiesFrom(commonElementType, resultType, localProjectionFactory, neo4jMappingContext); - Flux savedInstances = transactionalOperator.transactional(saveAllImpl(instances, pps, null)); + Flux savedInstances = execute(saveAllImpl(instances, pps, null)); if (projectionInformation.isClosed()) { return savedInstances.map(instance -> localProjectionFactory.createProjection(resultType, instance)); } Neo4jPersistentEntity entityMetaData = neo4jMappingContext.getRequiredPersistentEntity(commonElementType); - Neo4jPersistentProperty idProperty = entityMetaData.getIdProperty(); + Neo4jPersistentProperty idProperty = entityMetaData.getRequiredIdProperty(); return savedInstances.concatMap(savedInstance -> { PersistentPropertyAccessor propertyAccessor = entityMetaData.getPropertyAccessor(savedInstance); - return transactionalOperatorReadOnly.transactional(findById(propertyAccessor.getProperty(idProperty), commonElementType)); + return executeReadOnly(findById(Objects.requireNonNull(propertyAccessor.getProperty(idProperty)), commonElementType)); }).map(instance -> localProjectionFactory.createProjection(resultType, instance)); } + @SuppressWarnings("unchecked") private Flux saveAllImpl(Iterable instances, @Nullable Collection includedProperties, @Nullable BiPredicate includeProperty) { Set> types = new HashSet<>(); @@ -583,7 +591,7 @@ public final class ReactiveNeo4jTemplate implements Class domainClass = types.iterator().next(); Collection pps = includeProperty == null ? - includedProperties : + Objects.requireNonNullElseGet(includedProperties, List::of) : TemplateSupport.computeIncludedPropertiesFromPredicate(this.neo4jMappingContext, domainClass, includeProperty); @@ -645,9 +653,9 @@ public final class ReactiveNeo4jTemplate implements Condition condition = entityMetaData.getIdExpression().in(parameter(nameOfParameter)); Statement statement = cypherGenerator.prepareDeleteOf(entityMetaData, condition); - return transactionalOperator.transactional(Mono.defer(() -> + return execute(Mono.defer(() -> this.neo4jClient.query(() -> renderer.render(statement)) - .bind(convertIdValues(entityMetaData.getRequiredIdProperty(), ids)) + .bind(TemplateSupport.convertIdValues(this.neo4jMappingContext, entityMetaData.getRequiredIdProperty(), ids)) .to(nameOfParameter).run().then())); } @@ -661,15 +669,14 @@ public final class ReactiveNeo4jTemplate implements Condition condition = entityMetaData.getIdExpression().isEqualTo(parameter(nameOfParameter)); Statement statement = cypherGenerator.prepareDeleteOf(entityMetaData, condition); - return transactionalOperator.transactional(Mono.defer(() -> + return execute(Mono.defer(() -> this.neo4jClient.query(() -> renderer.render(statement)) - .bind(convertIdValues(entityMetaData.getRequiredIdProperty(), id)) + .bind(TemplateSupport.convertIdValues(this.neo4jMappingContext, entityMetaData.getRequiredIdProperty(), id)) .to(nameOfParameter).run().then())); } @Override - public Mono deleteByIdWithVersion(Object id, Class domainType, Neo4jPersistentProperty versionProperty, - Object versionValue) { + public Mono deleteByIdWithVersion(Object id, Class domainType, Neo4jPersistentProperty versionProperty, @Nullable Object versionValue) { String nameOfParameter = "id"; Neo4jPersistentEntity entityMetaData = neo4jMappingContext.getRequiredPersistentEntity(domainType); @@ -682,10 +689,10 @@ public final class ReactiveNeo4jTemplate implements .returning(Constants.NAME_OF_TYPED_ROOT_NODE.apply(entityMetaData)).build(); Map parameters = new HashMap<>(); - parameters.put(nameOfParameter, convertIdValues(entityMetaData.getRequiredIdProperty(), id)); + parameters.put(nameOfParameter, TemplateSupport.convertIdValues(this.neo4jMappingContext, entityMetaData.getRequiredIdProperty(), id)); parameters.put(Constants.NAME_OF_VERSION_PARAM, versionValue); - return transactionalOperator.transactional(Mono.defer(() -> + return execute(Mono.defer(() -> this.neo4jClient.query(() -> renderer.render(statement)) .bindAll(parameters) .fetch().one().switchIfEmpty(Mono.defer(() -> { @@ -702,7 +709,7 @@ public final class ReactiveNeo4jTemplate implements Neo4jPersistentEntity entityMetaData = neo4jMappingContext.getRequiredPersistentEntity(domainType); Statement statement = cypherGenerator.prepareDeleteOf(entityMetaData); - return transactionalOperator.transactional(Mono.defer(() -> this.neo4jClient.query(() -> renderer.render(statement)).run().then())); + return execute(Mono.defer(() -> this.neo4jClient.query(() -> renderer.render(statement)).run().then())); } private Mono> createExecutableQuery(Class domainType, Statement statement) { @@ -719,7 +726,7 @@ public final class ReactiveNeo4jTemplate implements return createExecutableQuery(domainType, resultType, renderer.render(statement), TemplateSupport.mergeParameters(statement, parameters)); } - private Mono> createExecutableQuery(Class domainType, @Nullable Class resultType, @Nullable String cypherQuery, + private Mono> createExecutableQuery(Class domainType, @Nullable Class resultType, String cypherQuery, Map parameters) { Supplier> mappingFunction = TemplateSupport @@ -748,8 +755,9 @@ public final class ReactiveNeo4jTemplate implements return createExecutableQuery(domainType, resultType, queryFragments.toStatement(), queryFragmentsAndParameters.getParameters()); } + @SuppressWarnings({"unchecked"}) private Mono createNodesAndRelationshipsByIdStatementProvider(Neo4jPersistentEntity entityMetaData, - QueryFragments queryFragments, Map parameters) { + QueryFragments queryFragments, Map parameters) { return Mono.deferContextual(ctx -> { Class rootClass = entityMetaData.getUnderlyingClass(); @@ -776,10 +784,7 @@ public final class ReactiveNeo4jTemplate implements return Tuples.of(newRelationshipIds, newRelatedNodeIds); }) .one() - .map((t) -> { - //noinspection unchecked - return (Tuple2, Collection>) t; - }) + .map((t) -> (Tuple2, Collection>) t) .expand(iterateAndMapNextLevel(relationshipDescription, queryFragments, rootClass, PropertyPathWalkStep.empty())); }) .then(Mono.fromSupplier(() -> new NodesAndRelationshipsByIdStatementProvider(rootNodeIds, relationshipsToRelatedNodeIds.keySet(), relationshipsToRelatedNodeIds.values().stream().flatMap(Collection::stream).toList(), queryFragments, elementIdOrIdFunction))); @@ -790,18 +795,23 @@ public final class ReactiveNeo4jTemplate implements } + @SuppressWarnings("unchecked") private Flux, Collection>> iterateNextLevel(Collection relatedNodeIds, - RelationshipDescription sourceRelationshipDescription, QueryFragments queryFragments, - Class rootClass, PropertyPathWalkStep currentPathStep) { + RelationshipDescription sourceRelationshipDescription, QueryFragments queryFragments, + Class rootClass, PropertyPathWalkStep currentPathStep) { NodeDescription target = sourceRelationshipDescription.getTarget(); @SuppressWarnings("unchecked") - String fieldName = ((Association) sourceRelationshipDescription).getInverse().getFieldName(); + String fieldName = ((Association<@NonNull Neo4jPersistentProperty>) sourceRelationshipDescription).getInverse().getFieldName(); - PropertyPathWalkStep nextPathStep = currentPathStep.with((sourceRelationshipDescription.hasRelationshipProperties() ? - fieldName + "." + ((Neo4jPersistentEntity) sourceRelationshipDescription.getRelationshipPropertiesEntity()) - .getPersistentProperty(TargetNode.class).getFieldName() : fieldName)); + PropertyPathWalkStep nextPathStep; + if (sourceRelationshipDescription.hasRelationshipProperties()) { + Neo4jPersistentEntity relationshipPropertiesEntity = (Neo4jPersistentEntity) sourceRelationshipDescription.getRequiredRelationshipPropertiesEntity(); + nextPathStep = currentPathStep.with(fieldName + "." + Objects.requireNonNull(relationshipPropertiesEntity.getPersistentProperty(TargetNode.class), () -> "Could not get target node property on %s".formatted(relationshipPropertiesEntity.getType())).getFieldName()); + } else { + nextPathStep = currentPathStep.with(fieldName); + } return Flux.fromIterable(target .getRelationshipsInHierarchy( @@ -829,16 +839,12 @@ public final class ReactiveNeo4jTemplate implements return Tuples.of(newRelationshipIds, newRelatedNodeIds); }) .one() - .map((t) -> { - //noinspection unchecked - return (Tuple2, Collection>) t; - }) + .map((t) -> (Tuple2, Collection>) t) .expand(object -> iterateAndMapNextLevel(relDe, queryFragments, rootClass, nextPathStep).apply(object)); }); } - @NonNull private Function, Collection>, Publisher, Collection>>> iterateAndMapNextLevel( RelationshipDescription relationshipDescription, QueryFragments queryFragments, Class rootClass, PropertyPathWalkStep currentPathStep) { @@ -900,6 +906,7 @@ public final class ReactiveNeo4jTemplate implements stateMachine, knownRelationshipsIds, includeProperty, startingPropertyPath); } + @SuppressWarnings("deprecation") private Mono processNestedRelations(Neo4jPersistentEntity sourceEntity, PersistentPropertyAccessor parentPropertyAccessor, boolean isParentObjectNew, NestedRelationshipProcessingStateMachine stateMachine, Collection knownRelationshipsIds, @@ -933,7 +940,7 @@ public final class ReactiveNeo4jTemplate implements idProperty = null; } else { Neo4jPersistentEntity relationshipPropertiesEntity = (Neo4jPersistentEntity) relationshipDescription.getRelationshipPropertiesEntity(); - idProperty = relationshipPropertiesEntity.getIdProperty(); + idProperty = relationshipPropertiesEntity == null ? null : relationshipPropertiesEntity.getIdProperty(); } // break recursive procession and deletion of previously created relationships @@ -950,12 +957,13 @@ public final class ReactiveNeo4jTemplate implements if (idProperty != null) { for (Object relatedValueToStore : relatedValuesToStore) { + //noinspection ConstantValue if (relatedValueToStore == null) { continue; } - Object id = relationshipContext - .getRelationshipPropertiesPropertyAccessor(relatedValueToStore) + Object id = Objects.requireNonNull(relationshipContext + .getRelationshipPropertiesPropertyAccessor(relatedValueToStore)) .getProperty(idProperty); if (id != null) { knownRelationshipsIds.add(id); @@ -967,7 +975,7 @@ public final class ReactiveNeo4jTemplate implements relationshipDeleteMonos.add( neo4jClient.query(renderer.render(relationshipRemoveQuery)) - .bind(convertIdValues(sourceEntity.getIdProperty(), fromId)) // + .bind(TemplateSupport.convertIdValues(this.neo4jMappingContext, sourceEntity.getIdProperty(), fromId)) // .to(Constants.FROM_ID_PARAMETER_NAME) // .bind(knownRelationshipsIds) // .to(Constants.NAME_OF_KNOWN_RELATIONSHIPS_PARAM) // @@ -1016,7 +1024,7 @@ public final class ReactiveNeo4jTemplate implements queryOrSave = savedEntity .map(entity -> Tuples.of(new AtomicReference<>((Object) (TemplateSupport.rendererCanUseElementIdIfPresent(renderer, targetEntity) ? entity.elementId() : entity.id())), new AtomicReference<>(entity))) .doOnNext(t -> { - var relatedInternalId = t.getT1().get(); + var relatedInternalId = Objects.requireNonNull(t.getT1().get(), "Related internal id is null"); stateMachine.markEntityAsProcessed(relatedValueToStore, relatedInternalId); if (relatedValueToStore instanceof MappingSupport.RelationshipPropertiesWithEntityHolder) { Object entity = ((MappingSupport.RelationshipPropertiesWithEntityHolder) relatedValueToStore).getRelatedEntity(); @@ -1031,7 +1039,9 @@ public final class ReactiveNeo4jTemplate implements Neo4jPersistentProperty requiredIdProperty = targetEntity.getRequiredIdProperty(); PersistentPropertyAccessor targetPropertyAccessor = targetEntity.getPropertyAccessor(newRelatedObject); Object possibleInternalLongId = targetPropertyAccessor.getProperty(requiredIdProperty); + //noinspection OptionalOfNullableMisuse relatedInternalId = TemplateSupport.retrieveOrSetRelatedId(targetEntity, targetPropertyAccessor, Optional.ofNullable(savedEntity), relatedInternalId); + //noinspection ConstantValue if (savedEntity != null) { TemplateSupport.updateVersionPropertyIfPossible(targetEntity, targetPropertyAccessor, savedEntity); } @@ -1039,21 +1049,22 @@ public final class ReactiveNeo4jTemplate implements stateMachine.markRelationshipAsProcessed(possibleInternalLongId == null ? relatedInternalId : possibleInternalLongId, relationshipDescription.getRelationshipObverse()); - Object idValue = idProperty != null - ? relationshipContext - .getRelationshipPropertiesPropertyAccessor(relatedValueToStore).getProperty(idProperty) + PersistentPropertyAccessor relationshipPropertiesPropertyAccessor = relationshipContext + .getRelationshipPropertiesPropertyAccessor(relatedValueToStore); + Object idValue = (idProperty != null && relationshipPropertiesPropertyAccessor != null) + ? relationshipPropertiesPropertyAccessor.getProperty(idProperty) : null; boolean isNewRelationship = idValue == null; CreateRelationshipStatementHolder statementHolder = neo4jMappingContext.createStatementForSingleRelationship( sourceEntity, relationshipDescription, relatedValueToStore, isNewRelationship, canUseElementId); - Map properties = new HashMap<>(); - properties.put(Constants.FROM_ID_PARAMETER_NAME, convertIdValues(sourceEntity.getRequiredIdProperty(), fromId)); + Map properties = new HashMap<>(); + properties.put(Constants.FROM_ID_PARAMETER_NAME, TemplateSupport.convertIdValues(this.neo4jMappingContext, sourceEntity.getRequiredIdProperty(), fromId)); properties.put(Constants.TO_ID_PARAMETER_NAME, relatedInternalId); properties.put(Constants.NAME_OF_KNOWN_RELATIONSHIP_PARAM, idValue); var update = true; - if (!relationshipDescription.isDynamic() && relationshipDescription.hasRelationshipProperties()) { + if (!relationshipDescription.isDynamic() && relationshipDescription.hasRelationshipProperties() && fromId != null) { var hlp = ((MappingSupport.RelationshipPropertiesWithEntityHolder) relatedValueToStore); var hasProcessedRelationshipEntity = stateMachine.hasProcessedRelationshipEntity(parentPropertyAccessor.getBean(), hlp.getRelatedEntity(), relationshipContext.getRelationship()); if (hasProcessedRelationshipEntity) { @@ -1070,7 +1081,7 @@ public final class ReactiveNeo4jTemplate implements if (update) { return neo4jClient .query(renderer.render(statementHolder.getStatement())) - .bind(convertIdValues(sourceEntity.getRequiredIdProperty(), fromId)) // + .bind(TemplateSupport.convertIdValues(this.neo4jMappingContext, sourceEntity.getRequiredIdProperty(), fromId)) // .to(Constants.FROM_ID_PARAMETER_NAME) // .bind(relatedInternalId) // .to(Constants.TO_ID_PARAMETER_NAME) // @@ -1081,9 +1092,8 @@ public final class ReactiveNeo4jTemplate implements .mappedBy((t, r) -> IdentitySupport.mapperForRelatedIdValues(idProperty).apply(r)) .one() .flatMap(relationshipInternalId -> { - if (idProperty != null && isNewRelationship) { - relationshipContext - .getRelationshipPropertiesPropertyAccessor(relatedValueToStore) + if (idProperty != null && isNewRelationship && relationshipPropertiesPropertyAccessor != null) { + relationshipPropertiesPropertyAccessor .setProperty(idProperty, relationshipInternalId); knownRelationshipsIds.add(relationshipInternalId); } @@ -1130,16 +1140,16 @@ public final class ReactiveNeo4jTemplate implements .thenMany(Flux.concat(relationshipCreationCreations)) .doOnNext(objects -> objects.applyFinalResultToOwner(parentPropertyAccessor)) .checkpoint() - .then(stateMachine.updateRelationshipIds(this::getRelationshipId)) + .then(stateMachine.updateRelationshipIdsReactive(this::getRelationshipId)) .then(Mono.fromSupplier(parentPropertyAccessor::getBean)); return deleteAndThanCreateANew; } - private Mono getRelationshipId(Statement statement, Neo4jPersistentProperty idProperty, Object fromId, Object toId) { + private Mono getRelationshipId(Statement statement, @Nullable Neo4jPersistentProperty idProperty, Object fromId, Object toId) { return neo4jClient.query(renderer.render(statement)) - .bind(convertIdValues(idProperty, fromId)) // + .bind(TemplateSupport.convertIdValues(this.neo4jMappingContext, idProperty, fromId)) // .to(Constants.FROM_ID_PARAMETER_NAME) // .bind(toId) // .to(Constants.TO_ID_PARAMETER_NAME) // @@ -1150,10 +1160,10 @@ public final class ReactiveNeo4jTemplate implements // The pendant to {@link #saveRelatedNode(Object, Neo4jPersistentEntity, PropertyFilter, PropertyFilter.RelaxedPropertyPath)} // We can't do without a query, as we need to refresh the internal id - private Mono loadRelatedNode(NodeDescription targetNodeDescription, Object relatedInternalId) { + private Mono loadRelatedNode(NodeDescription targetNodeDescription, @Nullable Object relatedInternalId) { var targetPersistentEntity = (Neo4jPersistentEntity) targetNodeDescription; - var queryFragmentsAndParameters = QueryFragmentsAndParameters.forFindById(targetPersistentEntity, convertIdValues(targetPersistentEntity.getRequiredIdProperty(), relatedInternalId)); + var queryFragmentsAndParameters = QueryFragmentsAndParameters.forFindById(targetPersistentEntity, TemplateSupport.convertIdValues(this.neo4jMappingContext, targetPersistentEntity.getRequiredIdProperty(), relatedInternalId)); var nodeName = Constants.NAME_OF_TYPED_ROOT_NODE.apply(targetNodeDescription).getValue(); return neo4jClient @@ -1175,14 +1185,14 @@ public final class ReactiveNeo4jTemplate implements DynamicLabels dynamicLabels = t.getT2(); @SuppressWarnings("unchecked") Function> binderFunction = neo4jMappingContext.getRequiredBinderFunctionFor(entityType); - String idPropertyName = targetNodeDescription.getIdProperty().getPropertyName(); + String idPropertyName = targetNodeDescription.getRequiredIdProperty().getPropertyName(); IdDescription idDescription = targetNodeDescription.getIdDescription(); - boolean assignedId = idDescription.isAssignedId() || idDescription.isExternallyGeneratedId(); + boolean assignedId = idDescription != null && (idDescription.isAssignedId() || idDescription.isExternallyGeneratedId()); binderFunction = binderFunction.andThen(tree -> { @SuppressWarnings("unchecked") Map properties = (Map) tree.get(Constants.NAME_OF_PROPERTIES_PARAM); - if (!includeProperty.isNotFiltering()) { + if (properties != null && !includeProperty.isNotFiltering()) { properties.entrySet().removeIf(e -> { // we cannot skip the id property if it is an assigned id boolean isIdProperty = e.getKey().equals(idPropertyName); @@ -1219,7 +1229,7 @@ public final class ReactiveNeo4jTemplate implements boolean containsPossibleCircles = entityMetaData != null && entityMetaData.containsPossibleCircles(queryFragments::includeField); if (cypherQuery == null || containsPossibleCircles) { - if (containsPossibleCircles && !queryFragments.isScalarValueReturn()) { + if (entityMetaData != null && containsPossibleCircles && !queryFragments.isScalarValueReturn()) { return createNodesAndRelationshipsByIdStatementProvider(entityMetaData, queryFragments, finalParameters) .map(nodesAndRelationshipsById -> { var statement = nodesAndRelationshipsById.toStatement(entityMetaData); @@ -1256,7 +1266,7 @@ public final class ReactiveNeo4jTemplate implements this.eventSupport = ReactiveEventSupport.discoverCallbacks(neo4jMappingContext, beanFactory); SpelAwareProxyProjectionFactory spelAwareProxyProjectionFactory = new SpelAwareProxyProjectionFactory(); - spelAwareProxyProjectionFactory.setBeanClassLoader(beanClassLoader); + spelAwareProxyProjectionFactory.setBeanClassLoader(Objects.requireNonNull(this.beanClassLoader)); spelAwareProxyProjectionFactory.setBeanFactory(beanFactory); this.projectionFactory = spelAwareProxyProjectionFactory; @@ -1324,7 +1334,7 @@ public final class ReactiveNeo4jTemplate implements @SuppressWarnings("unchecked") public Flux getResults() { - return transactionalOperator.transactional(fetchSpec.all().switchOnFirst((signal, f) -> { + return execute(fetchSpec.all().switchOnFirst((signal, f) -> { if (signal.hasValue() && preparedQuery.resultsHaveBeenAggregated()) { return f.concatMap(nested -> Flux.fromIterable((Collection) nested).distinct()).distinct(); } @@ -1337,14 +1347,14 @@ public final class ReactiveNeo4jTemplate implements * @throws IncorrectResultSizeDataAccessException if there is no or more than one result */ public Mono getSingleResult() { - return transactionalOperator.transactional(fetchSpec.one().map(t -> { + return execute(fetchSpec.one().map(t -> { if (t instanceof LinkedHashSet) { @SuppressWarnings("unchecked") T firstItem = (T) ((LinkedHashSet) t).iterator().next(); return firstItem; } return t; - }).onErrorMap(IndexOutOfBoundsException.class, e -> new IncorrectResultSizeDataAccessException(e.getMessage(), 1))); + }).onErrorMap(IndexOutOfBoundsException.class, e -> new IncorrectResultSizeDataAccessException(Objects.requireNonNull(e.getMessage()), 1))); } } } diff --git a/src/main/java/org/springframework/data/neo4j/core/RelationshipHandler.java b/src/main/java/org/springframework/data/neo4j/core/RelationshipHandler.java index 7ad227076..ec770e41e 100644 --- a/src/main/java/org/springframework/data/neo4j/core/RelationshipHandler.java +++ b/src/main/java/org/springframework/data/neo4j/core/RelationshipHandler.java @@ -16,6 +16,7 @@ package org.springframework.data.neo4j.core; import org.apiguardian.api.API; +import org.jspecify.annotations.Nullable; import org.springframework.core.CollectionFactory; import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.neo4j.core.mapping.Neo4jPersistentProperty; @@ -34,6 +35,8 @@ import java.util.Optional; @API(status = API.Status.INTERNAL, since = "6.1") final class RelationshipHandler { + private static final int DEFAULT_SIZE = 32; + enum Cardinality { ONE_TO_ONE, @@ -42,7 +45,7 @@ final class RelationshipHandler { DYNAMIC_ONE_TO_MANY } - static RelationshipHandler forProperty(Neo4jPersistentProperty property, Object rawValue) { + static RelationshipHandler forProperty(Neo4jPersistentProperty property, @Nullable Object rawValue) { Cardinality cardinality; Collection newRelationshipObjectCollection = Collections.emptyList(); @@ -51,13 +54,16 @@ final class RelationshipHandler { // Order is important here, all map based associations are dynamic, but not all dynamic associations are one to many if (property.isCollectionLike()) { cardinality = Cardinality.ONE_TO_MANY; - newRelationshipObjectCollection = CollectionFactory.createCollection(property.getType(), ((Collection) rawValue).size()); + var size = rawValue == null ? DEFAULT_SIZE : ((Collection) rawValue).size(); + newRelationshipObjectCollection = CollectionFactory.createCollection(property.getType(), size); } else if (property.isDynamicOneToManyAssociation()) { cardinality = Cardinality.DYNAMIC_ONE_TO_MANY; - newRelationshipObjectCollectionMap = CollectionFactory.createMap(property.getType(), ((Map) rawValue).size()); + var size = rawValue == null ? DEFAULT_SIZE : ((Map) rawValue).size(); + newRelationshipObjectCollectionMap = CollectionFactory.createMap(property.getType(), size); } else if (property.isDynamicAssociation()) { cardinality = Cardinality.DYNAMIC_ONE_TO_ONE; - newRelationshipObjectCollectionMap = CollectionFactory.createMap(property.getType(), ((Map) rawValue).size()); + var size = rawValue == null ? DEFAULT_SIZE : ((Map) rawValue).size(); + newRelationshipObjectCollectionMap = CollectionFactory.createMap(property.getType(), size); } else { cardinality = Cardinality.ONE_TO_ONE; } @@ -69,6 +75,7 @@ final class RelationshipHandler { /** * The raw value as passed to the template. */ + @Nullable private final Object rawValue; private final Cardinality cardinality; @@ -76,7 +83,7 @@ final class RelationshipHandler { private final Map newRelatedObjectsByType; RelationshipHandler(Neo4jPersistentProperty property, - Object rawValue, Cardinality cardinality, + @Nullable Object rawValue, Cardinality cardinality, Collection newRelatedObjects, Map newRelatedObjectsByType) { this.property = property; @@ -100,9 +107,12 @@ final class RelationshipHandler { } else { @SuppressWarnings("unchecked") Collection newCollection = (Collection) newRelatedObjectsByType - .computeIfAbsent(key, k -> CollectionFactory.createCollection( - property.getTypeInformation().getRequiredActualType().getType(), - ((Collection) ((Map) rawValue).get(key)).size())); + .computeIfAbsent(key, k -> { + Collection objects = rawValue == null ? null : (Collection) ((Map) rawValue).get(key); + return CollectionFactory.createCollection( + property.getTypeInformation().getRequiredActualType().getType(), + objects != null ? objects.size() : DEFAULT_SIZE); + }); newCollection.add(potentiallyRecreatedRelatedObject); } } diff --git a/src/main/java/org/springframework/data/neo4j/core/ResultSummaries.java b/src/main/java/org/springframework/data/neo4j/core/ResultSummaries.java index 16f455fde..e69e7cef3 100644 --- a/src/main/java/org/springframework/data/neo4j/core/ResultSummaries.java +++ b/src/main/java/org/springframework/data/neo4j/core/ResultSummaries.java @@ -22,6 +22,7 @@ import java.util.stream.Collectors; import java.util.stream.Stream; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.neo4j.driver.NotificationClassification; import org.neo4j.driver.NotificationSeverity; import org.neo4j.driver.summary.InputPosition; @@ -29,7 +30,6 @@ import org.neo4j.driver.summary.Notification; import org.neo4j.driver.summary.Plan; import org.neo4j.driver.summary.ResultSummary; import org.springframework.core.log.LogAccessor; -import org.springframework.lang.Nullable; /** * Utility class for dealing with result summaries. diff --git a/src/main/java/org/springframework/data/neo4j/core/SingleValueMappingFunction.java b/src/main/java/org/springframework/data/neo4j/core/SingleValueMappingFunction.java index 25e9e7747..bf8fc68f4 100644 --- a/src/main/java/org/springframework/data/neo4j/core/SingleValueMappingFunction.java +++ b/src/main/java/org/springframework/data/neo4j/core/SingleValueMappingFunction.java @@ -17,12 +17,12 @@ package org.springframework.data.neo4j.core; import java.util.function.BiFunction; +import org.jspecify.annotations.Nullable; import org.neo4j.driver.Record; import org.neo4j.driver.Value; import org.neo4j.driver.Values; import org.neo4j.driver.types.TypeSystem; import org.springframework.core.convert.ConversionService; -import org.springframework.lang.Nullable; /** * Used to automatically map single valued records to a sensible Java type based on {@link Value#asObject()}. @@ -42,8 +42,8 @@ final class SingleValueMappingFunction implements BiFunction implements BiFunction> allClasses = StreamSupport.stream(collection.spliterator(), true) - .filter(o -> o != null) + .filter(Objects::nonNull) .map(Object::getClass).collect(Collectors.toSet()); if (allClasses.isEmpty()) { @@ -156,8 +157,9 @@ public final class TemplateSupport { Entity newOrUpdatedNode ) { if (entityMetaData.hasVersionProperty()) { + var versionProperty = entityMetaData.getRequiredVersionProperty(); propertyAccessor.setProperty( - entityMetaData.getVersionProperty(), newOrUpdatedNode.get(entityMetaData.getVersionProperty().getPropertyName()).asLong()); + versionProperty, newOrUpdatedNode.get(versionProperty.getPropertyName()).asLong()); } } @@ -168,7 +170,7 @@ public final class TemplateSupport { * @param parameters The original parameters * @return Merged parameters */ - static Map mergeParameters(Statement statement, @Nullable Map parameters) { + static Map mergeParameters(Statement statement, Map parameters) { Map mergedParameters = new HashMap<>(statement.getCatalog().getParameters()); if (parameters != null) { @@ -205,7 +207,8 @@ public final class TemplateSupport { } boolean hasRootNodeIds() { - return parameters.get(ROOT_NODE_IDS).isEmpty(); + var ids = parameters.get(ROOT_NODE_IDS); + return ids != null && !ids.isEmpty(); } Statement toStatement(NodeDescription nodeDescription) { @@ -291,10 +294,10 @@ public final class TemplateSupport { @SuppressWarnings("unchecked") Map properties = (Map) tree.get(Constants.NAME_OF_PROPERTIES_PARAM); - String idPropertyName = entityMetaData.getIdProperty().getPropertyName(); + String idPropertyName = entityMetaData.getRequiredIdProperty().getPropertyName(); IdDescription idDescription = entityMetaData.getIdDescription(); - boolean assignedId = idDescription.isAssignedId() || idDescription.isExternallyGeneratedId(); - if (!includeProperty.isNotFiltering()) { + boolean assignedId = idDescription != null && (idDescription.isAssignedId() || idDescription.isExternallyGeneratedId()); + if (!(includeProperty.isNotFiltering() || properties == null)) { properties.entrySet() .removeIf(e -> { // we cannot skip the id property if it is an assigned id @@ -317,7 +320,7 @@ public final class TemplateSupport { * @return A map as expected by the property filter. */ static Collection computeIncludedPropertiesFromPredicate(Neo4jMappingContext mappingContext, - Class domainType, @Nullable BiPredicate predicate) { + Class domainType, BiPredicate predicate) { if (predicate == null) { return Collections.emptySet(); } @@ -357,6 +360,7 @@ public final class TemplateSupport { * @param databaseEntity A fallback entity to retrieve the deprecated internal long id * @param The type of the entity */ + @SuppressWarnings("deprecation") static void setGeneratedIdIfNecessary( Neo4jPersistentEntity entityMetaData, PersistentPropertyAccessor propertyAccessor, @@ -388,10 +392,11 @@ public final class TemplateSupport { * @param The type of the entity * @return The actual related internal id being used. */ + @SuppressWarnings("deprecation") static Object retrieveOrSetRelatedId( Neo4jPersistentEntity entityMetadata, PersistentPropertyAccessor propertyAccessor, - Optional databaseEntity, + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") Optional databaseEntity, @Nullable Object relatedInternalId ) { if (!entityMetadata.isUsingInternalIds()) { @@ -442,7 +447,11 @@ public final class TemplateSupport { return value.toString(); } - static Object convertToLongIdOrStringElementId(Collection ids) { + @Nullable + static Object convertToLongIdOrStringElementId(@Nullable Collection ids) { + if (ids == null) { + return null; + } try { return ids.stream() .map(Long::valueOf).collect(Collectors.toSet()); @@ -452,6 +461,22 @@ public final class TemplateSupport { } } + static Object convertIdValues(Neo4jMappingContext ctx, @Nullable Neo4jPersistentProperty idProperty, @Nullable Object idValues) { + + if (idProperty != null && ((Neo4jPersistentEntity) idProperty.getOwner()).isUsingInternalIds()) { + return (idValues != null) ? idValues : Values.NULL; + } + + if (idValues != null) { + return ctx.getConversionService().writeValue(idValues, TypeInformation.of(idValues.getClass()), idProperty == null ? null : idProperty.getOptionalConverter()); + } else if (idProperty != null) { + return ctx.getConversionService().writeValue(idValues, idProperty.getTypeInformation(), idProperty.getOptionalConverter()); + } else { + // Not much we can convert here + return Values.NULL; + } + } + private TemplateSupport() { } } diff --git a/src/main/java/org/springframework/data/neo4j/core/UserSelection.java b/src/main/java/org/springframework/data/neo4j/core/UserSelection.java index 6c0fed519..22c3591d4 100644 --- a/src/main/java/org/springframework/data/neo4j/core/UserSelection.java +++ b/src/main/java/org/springframework/data/neo4j/core/UserSelection.java @@ -18,7 +18,7 @@ package org.springframework.data.neo4j.core; import java.util.Objects; import org.apiguardian.api.API; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.util.Assert; /** @@ -57,7 +57,8 @@ public final class UserSelection { return new UserSelection(value); } - @Nullable private final String value; + @Nullable + private final String value; private UserSelection(@Nullable String value) { this.value = value; diff --git a/src/main/java/org/springframework/data/neo4j/core/convert/AdditionalTypes.java b/src/main/java/org/springframework/data/neo4j/core/convert/AdditionalTypes.java index e26949acb..405b2d2ca 100644 --- a/src/main/java/org/springframework/data/neo4j/core/convert/AdditionalTypes.java +++ b/src/main/java/org/springframework/data/neo4j/core/convert/AdditionalTypes.java @@ -33,10 +33,12 @@ import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.TimeZone; import java.util.UUID; +import org.jspecify.annotations.Nullable; import org.neo4j.driver.Value; import org.neo4j.driver.Values; import org.neo4j.driver.exceptions.value.LossyCoercion; @@ -267,7 +269,8 @@ final class AdditionalTypes { @SuppressWarnings({"raw", "unchecked"}) // Due to dynamic enum retrieval @Override - public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + @Nullable + public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { if (source == null) { return Value.class.isAssignableFrom(targetType.getType()) ? Values.NULL : null; @@ -299,6 +302,7 @@ final class AdditionalTypes { } @Override + @Nullable public Set getConvertibleTypes() { return null; } @@ -315,12 +319,14 @@ final class AdditionalTypes { } private static boolean describesSupportedEnumVariant(TypeDescriptor typeDescriptor) { + var elementTypeDescriptor = typeDescriptor.getElementTypeDescriptor(); return typeDescriptor.isArray() - && Enum.class.isAssignableFrom(typeDescriptor.getElementTypeDescriptor().getType()); + && elementTypeDescriptor != null && Enum.class.isAssignableFrom(elementTypeDescriptor.getType()); } @Override - public Object convert(Object object, TypeDescriptor sourceType, TypeDescriptor targetType) { + @Nullable + public Object convert(@Nullable Object object, TypeDescriptor sourceType, TypeDescriptor targetType) { if (object == null) { return Value.class.isAssignableFrom(targetType.getType()) ? Values.NULL : null; @@ -329,17 +335,17 @@ final class AdditionalTypes { if (Value.class.isAssignableFrom(sourceType.getType())) { Value source = (Value) object; - TypeDescriptor elementTypeDescriptor = targetType.getElementTypeDescriptor(); + TypeDescriptor elementTypeDescriptor = Objects.requireNonNull(targetType.getElementTypeDescriptor()); Object[] targetArray = (Object[]) Array.newInstance(elementTypeDescriptor.getType(), source.size()); Arrays.setAll(targetArray, i -> delegate.convert(source.get(i), TypeDescriptor.valueOf(Value.class), elementTypeDescriptor)); return targetArray; } else { - Enum[] source = (Enum[]) object; + Enum[] source = (Enum[]) object; return Values.value(Arrays.stream(source) - .map(e -> delegate.convert(e, sourceType.getElementTypeDescriptor(), TypeDescriptor.valueOf(Value.class))) + .map(e -> delegate.convert(e, Objects.requireNonNull(sourceType.getElementTypeDescriptor()), TypeDescriptor.valueOf(Value.class))) .toArray()); } } @@ -357,6 +363,7 @@ final class AdditionalTypes { return Values.value(aFloat.toString()); } + @Nullable static Locale asLocale(Value value) { return StringUtils.parseLocale(value.asString()); diff --git a/src/main/java/org/springframework/data/neo4j/core/convert/ConvertWith.java b/src/main/java/org/springframework/data/neo4j/core/convert/ConvertWith.java index 103879b4b..a34f825ed 100644 --- a/src/main/java/org/springframework/data/neo4j/core/convert/ConvertWith.java +++ b/src/main/java/org/springframework/data/neo4j/core/convert/ConvertWith.java @@ -23,6 +23,7 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import org.apiguardian.api.API; +import org.jspecify.annotations.Nullable; import org.neo4j.driver.Value; import org.neo4j.driver.Values; @@ -73,11 +74,13 @@ public @interface ConvertWith { */ final class UnsetConverter implements Neo4jPersistentPropertyConverter { - @Override public Value write(Object source) { + @Override public Value write(@Nullable Object source) { return Values.NULL; } - @Override public Object read(Value source) { + @Override + @Nullable + public Object read(@Nullable Value source) { return null; } } diff --git a/src/main/java/org/springframework/data/neo4j/core/convert/DefaultNeo4jPersistentPropertyConverterFactory.java b/src/main/java/org/springframework/data/neo4j/core/convert/DefaultNeo4jPersistentPropertyConverterFactory.java index 55559e166..c6e54c98f 100644 --- a/src/main/java/org/springframework/data/neo4j/core/convert/DefaultNeo4jPersistentPropertyConverterFactory.java +++ b/src/main/java/org/springframework/data/neo4j/core/convert/DefaultNeo4jPersistentPropertyConverterFactory.java @@ -18,7 +18,6 @@ package org.springframework.data.neo4j.core.convert; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.BeanFactory; import org.springframework.data.neo4j.core.mapping.Neo4jPersistentProperty; -import org.springframework.lang.Nullable; import org.springframework.util.StringUtils; /** @@ -28,10 +27,9 @@ import org.springframework.util.StringUtils; */ final class DefaultNeo4jPersistentPropertyConverterFactory implements Neo4jPersistentPropertyConverterFactory { - @Nullable private final BeanFactory beanFactory; - DefaultNeo4jPersistentPropertyConverterFactory(@Nullable BeanFactory beanFactory) { + DefaultNeo4jPersistentPropertyConverterFactory(BeanFactory beanFactory) { this.beanFactory = beanFactory; } diff --git a/src/main/java/org/springframework/data/neo4j/core/convert/Neo4jConversionService.java b/src/main/java/org/springframework/data/neo4j/core/convert/Neo4jConversionService.java index f2865ac98..8b526e7a9 100644 --- a/src/main/java/org/springframework/data/neo4j/core/convert/Neo4jConversionService.java +++ b/src/main/java/org/springframework/data/neo4j/core/convert/Neo4jConversionService.java @@ -16,10 +16,10 @@ package org.springframework.data.neo4j.core.convert; import org.apiguardian.api.API; +import org.jspecify.annotations.Nullable; import org.neo4j.driver.Value; import org.springframework.dao.TypeMismatchDataAccessException; import org.springframework.data.util.TypeInformation; -import org.springframework.lang.Nullable; /** * This service orchestrates a standard Spring conversion service with {@link org.springframework.data.neo4j.core.convert.Neo4jConversions} registered. @@ -40,7 +40,8 @@ public interface Neo4jConversionService { * @param The type to be returned * @return The converted value */ - @Nullable T convert(Object source, Class targetType); + @Nullable + T convert(Object source, Class targetType); /** * Returns whether we have a custom conversion registered to read {@code sourceType} into a native type. The returned @@ -66,9 +67,7 @@ public interface Neo4jConversionService { * @throws TypeMismatchDataAccessException In case the value cannot be converted to the target type */ @Nullable - Object readValue( - @Nullable Value source, TypeInformation targetType, @Nullable Neo4jPersistentPropertyConverter conversionOverride - ); + Object readValue(@Nullable Value source, TypeInformation targetType, @Nullable Neo4jPersistentPropertyConverter conversionOverride); /** * Converts an {@link Object} to a driver's value object. @@ -77,9 +76,7 @@ public interface Neo4jConversionService { * @param sourceType The type information describing the target type. * @return A driver compatible value object. */ - Value writeValue( - @Nullable Object value, TypeInformation sourceType, @Nullable Neo4jPersistentPropertyConverter conversionOverride - ); + Value writeValue(@Nullable Object value, TypeInformation sourceType, @Nullable Neo4jPersistentPropertyConverter conversionOverride); /** * @param type A type that should be checked whether it's simple or not. diff --git a/src/main/java/org/springframework/data/neo4j/core/convert/Neo4jPersistentPropertyConverter.java b/src/main/java/org/springframework/data/neo4j/core/convert/Neo4jPersistentPropertyConverter.java index 17eecabde..aec562952 100644 --- a/src/main/java/org/springframework/data/neo4j/core/convert/Neo4jPersistentPropertyConverter.java +++ b/src/main/java/org/springframework/data/neo4j/core/convert/Neo4jPersistentPropertyConverter.java @@ -16,8 +16,8 @@ package org.springframework.data.neo4j.core.convert; import org.apiguardian.api.API; +import org.jspecify.annotations.Nullable; import org.neo4j.driver.Value; -import org.springframework.lang.Nullable; /** * This interface represents a pair of methods capable of converting values of type {@code T} to and from {@link Value values}. @@ -41,5 +41,5 @@ public interface Neo4jPersistentPropertyConverter { * @param source The value to read, never null or {@link org.neo4j.driver.Values#NULL} * @return The converted value, maybe null if {@code source} was equals to {@link org.neo4j.driver.Values#NULL}. */ - @Nullable T read(Value source); + @Nullable T read(@Nullable Value source); } diff --git a/src/main/java/org/springframework/data/neo4j/core/convert/Neo4jPersistentPropertyToMapConverter.java b/src/main/java/org/springframework/data/neo4j/core/convert/Neo4jPersistentPropertyToMapConverter.java index fd4e0df48..1e8ad756b 100644 --- a/src/main/java/org/springframework/data/neo4j/core/convert/Neo4jPersistentPropertyToMapConverter.java +++ b/src/main/java/org/springframework/data/neo4j/core/convert/Neo4jPersistentPropertyToMapConverter.java @@ -18,8 +18,8 @@ package org.springframework.data.neo4j.core.convert; import java.util.Map; import org.apiguardian.api.API; +import org.jspecify.annotations.Nullable; import org.neo4j.driver.Value; -import org.springframework.lang.Nullable; /** * You need to provide an implementation of this interface in case you want to store a property of an entity as separate diff --git a/src/main/java/org/springframework/data/neo4j/core/convert/TemporalAmountConverter.java b/src/main/java/org/springframework/data/neo4j/core/convert/TemporalAmountConverter.java index 2fd258d65..2e0064c48 100644 --- a/src/main/java/org/springframework/data/neo4j/core/convert/TemporalAmountConverter.java +++ b/src/main/java/org/springframework/data/neo4j/core/convert/TemporalAmountConverter.java @@ -23,12 +23,12 @@ import java.util.Collections; import java.util.HashSet; import java.util.Set; +import org.jspecify.annotations.Nullable; import org.neo4j.driver.Value; import org.neo4j.driver.Values; import org.neo4j.driver.types.IsoDuration; import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.converter.GenericConverter; -import org.springframework.lang.Nullable; /** * This generic converter has been introduced to augment the {@link TemporalAmountAdapter} with the type information passed @@ -53,14 +53,14 @@ final class TemporalAmountConverter implements GenericConverter { } @Override + @Nullable public Object convert(@Nullable Object value, TypeDescriptor sourceType, TypeDescriptor targetType) { if (TemporalAmount.class.isAssignableFrom(sourceType.getType())) { return Values.value(value); } - boolean valueIsLiteralNullOrNullValue = value == null || value == Values.NULL; - Object convertedValue = valueIsLiteralNullOrNullValue ? null : adapter.apply(((Value) value).asIsoDuration()); + Object convertedValue = value == null || value == Values.NULL ? null : adapter.apply(((Value) value).asIsoDuration()); if (convertedValue instanceof IsoDuration && isZero((IsoDuration) convertedValue)) { if (Period.class.isAssignableFrom(targetType.getType())) { diff --git a/src/main/java/org/springframework/data/neo4j/core/convert/package-info.java b/src/main/java/org/springframework/data/neo4j/core/convert/package-info.java index 0c8dbcba0..fbad27a4c 100644 --- a/src/main/java/org/springframework/data/neo4j/core/convert/package-info.java +++ b/src/main/java/org/springframework/data/neo4j/core/convert/package-info.java @@ -4,7 +4,7 @@ converters. * */ -@NonNullApi +@NullMarked package org.springframework.data.neo4j.core.convert; -import org.springframework.lang.NonNullApi; +import org.jspecify.annotations.NullMarked; diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/CreateRelationshipStatementHolder.java b/src/main/java/org/springframework/data/neo4j/core/mapping/CreateRelationshipStatementHolder.java index 08a08bcff..9390c041e 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/CreateRelationshipStatementHolder.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/CreateRelationshipStatementHolder.java @@ -22,7 +22,6 @@ import org.apiguardian.api.API; import org.neo4j.cypherdsl.core.Statement; import org.springframework.data.neo4j.core.Neo4jTemplate; import org.springframework.data.neo4j.core.ReactiveNeo4jTemplate; -import org.springframework.lang.NonNull; /** * The {@link CreateRelationshipStatementHolder} holds the Cypher Statement to create a relationship as well as the optional @@ -40,7 +39,7 @@ public final class CreateRelationshipStatementHolder { private final Statement statement; private final Map properties; - CreateRelationshipStatementHolder(@NonNull Statement statement, @NonNull Map properties) { + CreateRelationshipStatementHolder(Statement statement, Map properties) { this.statement = statement; this.properties = properties; } diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/CypherGenerator.java b/src/main/java/org/springframework/data/neo4j/core/mapping/CypherGenerator.java index 5348331a7..ffa58a044 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/CypherGenerator.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/CypherGenerator.java @@ -31,6 +31,7 @@ import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Objects; +import java.util.Optional; import java.util.function.Function; import java.util.function.Predicate; import java.util.function.UnaryOperator; @@ -39,6 +40,7 @@ import java.util.regex.Pattern; import javax.lang.model.SourceVersion; import org.apiguardian.api.API; +import org.jspecify.annotations.Nullable; import org.neo4j.cypherdsl.core.Condition; import org.neo4j.cypherdsl.core.Cypher; import org.neo4j.cypherdsl.core.Expression; @@ -55,6 +57,7 @@ import org.neo4j.cypherdsl.core.SortItem; import org.neo4j.cypherdsl.core.Statement; import org.neo4j.cypherdsl.core.StatementBuilder; import org.neo4j.cypherdsl.core.StatementBuilder.OngoingMatchAndUpdate; +import org.neo4j.cypherdsl.core.StatementBuilder.OngoingReadingWithoutWhere; import org.neo4j.cypherdsl.core.StatementBuilder.OngoingUpdate; import org.neo4j.cypherdsl.core.SymbolicName; import org.neo4j.cypherdsl.core.renderer.Configuration; @@ -63,8 +66,6 @@ import org.springframework.data.domain.Sort; import org.springframework.data.mapping.MappingException; import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.neo4j.core.schema.TargetNode; -import org.springframework.lang.NonNull; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -131,15 +132,14 @@ public enum CypherGenerator { * @param condition Optional conditions to add * @return An ongoing match */ - public StatementBuilder.OrderableOngoingReadingAndWith prepareMatchOf(NodeDescription nodeDescription, - @Nullable Condition condition) { + @SuppressWarnings("deprecation") + public StatementBuilder.OrderableOngoingReadingAndWith prepareMatchOf(NodeDescription nodeDescription, @Nullable Condition condition) { Node rootNode = createRootNode(nodeDescription); List expressions = new ArrayList<>(); expressions.add(rootNode.getRequiredSymbolicName()); if (nodeDescription instanceof Neo4jPersistentEntity entity && entity.isUsingDeprecatedInternalId()) { - //noinspection deprecation expressions.add(rootNode.internalId().as(Constants.NAME_OF_INTERNAL_ID)); } expressions.add(elementIdOrIdFunction.apply(rootNode).as(Constants.NAME_OF_ELEMENT_ID)); @@ -148,11 +148,11 @@ public enum CypherGenerator { } public StatementBuilder.OngoingReading prepareMatchOf(NodeDescription nodeDescription, - @Nullable List initialMatchOn, + List initialMatchOn, @Nullable Condition condition) { Node rootNode = createRootNode(nodeDescription); - StatementBuilder.OngoingReadingWithoutWhere match = prepareMatchOfRootNode(rootNode, initialMatchOn); + OngoingReadingWithoutWhere match = prepareMatchOfRootNode(rootNode, initialMatchOn); List expressions = new ArrayList<>(); expressions.add(Cypher.collect(elementIdOrIdFunction.apply(rootNode)).as(Constants.NAME_OF_SYNTHESIZED_ROOT_NODE)); @@ -163,12 +163,11 @@ public enum CypherGenerator { } public StatementBuilder.OngoingReading prepareMatchOf(NodeDescription nodeDescription, - RelationshipDescription relationshipDescription, @Nullable List initialMatchOn, - @Nullable Condition condition) { + RelationshipDescription relationshipDescription, @Nullable List initialMatchOn, @Nullable Condition condition) { Node rootNode = createRootNode(nodeDescription); - StatementBuilder.OngoingReadingWithoutWhere match = prepareMatchOfRootNode(rootNode, initialMatchOn); + OngoingReadingWithoutWhere match = prepareMatchOfRootNode(rootNode, initialMatchOn); Node targetNode = node(relationshipDescription.getTarget().getPrimaryLabel(), relationshipDescription.getTarget().getAdditionalLabels()) @@ -202,7 +201,6 @@ public enum CypherGenerator { .with(expressions.toArray(IdentifiableElement[]::new)); } - @NonNull public Node createRootNode(NodeDescription nodeDescription) { String primaryLabel = nodeDescription.getPrimaryLabel(); List additionalLabels = nodeDescription.getAdditionalLabels(); @@ -210,11 +208,11 @@ public enum CypherGenerator { return node(primaryLabel, additionalLabels).named(Constants.NAME_OF_TYPED_ROOT_NODE.apply(nodeDescription)); } - private StatementBuilder.OngoingReadingWithoutWhere prepareMatchOfRootNode( + private OngoingReadingWithoutWhere prepareMatchOfRootNode( Node rootNode, @Nullable List initialMatchOn ) { - StatementBuilder.OngoingReadingWithoutWhere match = null; + OngoingReadingWithoutWhere match = null; if (initialMatchOn == null || initialMatchOn.isEmpty()) { match = Cypher.match(rootNode); } else { @@ -226,7 +224,7 @@ public enum CypherGenerator { } } } - return match; + return Objects.requireNonNull(match); } /** @@ -239,8 +237,7 @@ public enum CypherGenerator { */ public Statement createStatementReturningDynamicLabels(NodeDescription nodeDescription) { - IdDescription idDescription = nodeDescription.getIdDescription(); - Assert.notNull(idDescription, "Cannot load specific nodes by id without a corresponding attribute"); + IdDescription idDescription = Objects.requireNonNull(nodeDescription.getIdDescription(), "Cannot load specific nodes by id without a corresponding attribute"); final Node rootNode = createRootNode(nodeDescription); @@ -290,7 +287,7 @@ public enum CypherGenerator { Neo4jPersistentProperty property = (Neo4jPersistentProperty) idProperty; Condition result = Cypher.noCondition(); - for (String key : property.getOptionalConverter().write(null).keys()) { + for (String key : Objects.requireNonNull(property.getOptionalConverter()).write(null).keys()) { Property expression = Cypher.property(containerName, key); result = result.and(expression.isEqualTo(actualParameter.property(key))); } @@ -319,7 +316,7 @@ public enum CypherGenerator { return bs.returning(rootNode).build(); }; - if (!idDescription.isInternallyGeneratedId()) { + if (idDescription != null && !idDescription.isInternallyGeneratedId()) { GraphPropertyDescription idPropertyDescription = ((Neo4jPersistentEntity) nodeDescription).getRequiredIdProperty(); if (((Neo4jPersistentEntity) nodeDescription).hasVersionProperty()) { @@ -403,6 +400,7 @@ public enum CypherGenerator { } } + @SuppressWarnings("deprecation") public Statement prepareSaveOfMultipleInstancesOf(NodeDescription nodeDescription) { Assert.isTrue(!nodeDescription.isUsingInternalIds(), @@ -413,7 +411,7 @@ public enum CypherGenerator { IdDescription idDescription = nodeDescription.getIdDescription(); @SuppressWarnings("ConstantConditions") // We now already that the node is using internal ids, and as such, an IdDescription must be present - String nameOfIdProperty = idDescription.getOptionalGraphPropertyName() + String nameOfIdProperty = Optional.ofNullable(idDescription).flatMap(IdDescription::getOptionalGraphPropertyName) .orElseThrow(() -> new MappingException("External id does not correspond to a graph property")); List expressions = new ArrayList<>(); @@ -431,9 +429,8 @@ public enum CypherGenerator { .build(); } - @NonNull public Statement prepareSaveOfRelationship(Neo4jPersistentEntity neo4jPersistentEntity, - RelationshipDescription relationship, @Nullable String dynamicRelationshipType, boolean canUseElementId) { + RelationshipDescription relationship, String dynamicRelationshipType, boolean canUseElementId) { final Node startNode = neo4jPersistentEntity.isUsingInternalIds() ? anyNode(START_NODE_NAME) : node(neo4jPersistentEntity.getPrimaryLabel(), neo4jPersistentEntity.getAdditionalLabels()) @@ -457,7 +454,8 @@ public enum CypherGenerator { .build(); } - private static Function getNodeIdFunction(@Nullable Neo4jPersistentEntity entity, boolean canUseElementId) { + @SuppressWarnings("deprecation") + private static Function getNodeIdFunction(Neo4jPersistentEntity entity, boolean canUseElementId) { Function startNodeIdFunction; var idProperty = entity.getRequiredIdProperty(); @@ -473,7 +471,8 @@ public enum CypherGenerator { return startNodeIdFunction; } - private static Function getEndNodeIdFunction(@Nullable Neo4jPersistentEntity entity, boolean canUseElementId) { + @SuppressWarnings("deprecation") + private static Function getEndNodeIdFunction(Neo4jPersistentEntity entity, boolean canUseElementId) { Function startNodeIdFunction; if (entity == null) { @@ -505,7 +504,6 @@ public enum CypherGenerator { return result; } - @NonNull public Statement prepareSaveOfRelationships(Neo4jPersistentEntity neo4jPersistentEntity, RelationshipDescription relationship, @Nullable String dynamicRelationshipType, boolean canUseElementId) { @@ -518,7 +516,7 @@ public enum CypherGenerator { String type = relationship.isDynamic() ? dynamicRelationshipType : relationship.getType(); Relationship relationshipFragment = (relationship.isOutgoing() ? - startNode.relationshipTo(endNode, type) : + startNode.relationshipTo(endNode, type) : // CypherDSL is fine with a null type startNode.relationshipFrom(endNode, type)).named(RELATIONSHIP_NAME); String row = "relationship"; @@ -534,7 +532,6 @@ public enum CypherGenerator { .build(); } - @NonNull public Statement prepareSaveOfRelationshipWithProperties(Neo4jPersistentEntity neo4jPersistentEntity, RelationshipDescription relationship, boolean isNew, @@ -582,7 +579,6 @@ public enum CypherGenerator { .build(); } - @NonNull public Statement prepareUpdateOfRelationshipsWithProperties(Neo4jPersistentEntity neo4jPersistentEntity, RelationshipDescription relationship, boolean isNew, boolean canUseElementId) { @@ -638,7 +634,6 @@ public enum CypherGenerator { return result; } - @NonNull public Statement prepareDeleteOf( Neo4jPersistentEntity neo4jPersistentEntity, RelationshipDescription relationshipDescription, @@ -683,13 +678,14 @@ public enum CypherGenerator { * @param sort The {@link Sort sort} that should be turned into a valid Cypher {@code ORDER}-clause * @return An optional order clause. Will be {@literal null} on sorts that are {@literal null} or unsorted. */ - public @Nullable String createOrderByFragment(@Nullable Sort sort) { + @Nullable + public String createOrderByFragment(@Nullable Sort sort) { if (sort == null || sort.isUnsorted()) { return null; } Statement statement = match(anyNode()).returning("n") - .orderBy(sort.stream().filter(Objects::nonNull).map(order -> { + .orderBy(sort.stream().map(order -> { String property = order.getProperty().trim(); Expression expression; if (LOOKS_LIKE_A_FUNCTION.matcher(property).matches()) { @@ -711,12 +707,12 @@ public enum CypherGenerator { try { Assert.isTrue(SourceVersion.isIdentifier(property), "Name must be a valid identifier."); expression = Cypher.name(property); - } catch (IllegalArgumentException e) { - if (e.getMessage().endsWith(".")) { - throw new IllegalArgumentException( - e.getMessage().substring(0, e.getMessage().length() - 1)); + } catch (IllegalArgumentException ex) { + var msg = Optional.ofNullable(ex.getMessage()).orElse(""); + if (msg.endsWith(".")) { + throw new IllegalArgumentException(msg.substring(0, msg.length() - 1)); } - throw e; + throw ex; } } if (order.isIgnoreCase()) { @@ -750,7 +746,6 @@ public enum CypherGenerator { nodeDescription, Constants.NAME_OF_TYPED_ROOT_NODE.apply(nodeDescription), includeField, - null, processedRelationships)); Collections.addAll(returnContent, additionalExpressions); return returnContent; @@ -767,18 +762,18 @@ public enum CypherGenerator { } - public StatementBuilder.OngoingReading prepareFindOf(NodeDescription nodeDescription, @Nullable List initialMatchOn, @Nullable Condition condition) { + public StatementBuilder.OngoingReading prepareFindOf(NodeDescription nodeDescription, List initialMatchOn, @Nullable Condition condition) { var rootNode = createRootNode(nodeDescription); return prepareMatchOfRootNode(rootNode, initialMatchOn).where(conditionOrNoCondition(condition)); } private MapProjection projectPropertiesAndRelationships(PropertyFilter.RelaxedPropertyPath parentPath, Neo4jPersistentEntity nodeDescription, SymbolicName nodeName, - Predicate includedProperties, @Nullable RelationshipDescription relationshipDescription, List processedRelationships, Expression... additionalExpressions) { + Predicate includedProperties, @Nullable List processedRelationships) { Collection relationships = ((DefaultNeo4jPersistentEntity) nodeDescription).getRelationshipsInHierarchy(includedProperties, parentPath); relationships.removeIf(r -> !includedProperties.test(parentPath.append(r.getFieldName()))); - List propertiesProjection = projectNodeProperties(parentPath, nodeDescription, nodeName, relationshipDescription, includedProperties); + List propertiesProjection = projectNodeProperties(parentPath, nodeDescription, nodeName, includedProperties); List contentOfProjection = new ArrayList<>(propertiesProjection); contentOfProjection.addAll(generateListsFor(parentPath, nodeDescription, relationships, nodeName, includedProperties, processedRelationships)); @@ -790,8 +785,9 @@ public enum CypherGenerator { * this list can also contain two "keys" in a row. The {@link MapProjection} will take care to handle them as * self-reflecting fields. Example with self-reflection and explicit value: {@code n {.id, name: n.name}}. */ + @SuppressWarnings("deprecation") private List projectNodeProperties(PropertyFilter.RelaxedPropertyPath parentPath, NodeDescription nodeDescription, SymbolicName nodeName, - @Nullable RelationshipDescription relationshipDescription, Predicate includeField) { + Predicate includeField) { List nodePropertiesProjection = new ArrayList<>(); Node node = anyNode(nodeName); @@ -827,7 +823,6 @@ public enum CypherGenerator { nodePropertiesProjection.add(Cypher.labels(node)); if (nodeDescription instanceof Neo4jPersistentEntity entity && entity.isUsingDeprecatedInternalId()) { nodePropertiesProjection.add(Constants.NAME_OF_INTERNAL_ID); - //noinspection deprecation nodePropertiesProjection.add(node.internalId()); } nodePropertiesProjection.add(Constants.NAME_OF_ELEMENT_ID); @@ -839,9 +834,10 @@ public enum CypherGenerator { * @see CypherGenerator#projectNodeProperties */ private List generateListsFor(PropertyFilter.RelaxedPropertyPath parentPath, Neo4jPersistentEntity nodeDescription, Collection relationships, SymbolicName nodeName, - Predicate includedProperties, List processedRelationships) { + Predicate includedProperties, @Nullable List processedRelationships) { List mapProjectionLists = new ArrayList<>(); + List processed = Objects.requireNonNullElseGet(processedRelationships, ArrayList::new); for (RelationshipDescription relationshipDescription : relationships) { @@ -850,11 +846,11 @@ public enum CypherGenerator { // if we already processed the other way before, do not try to jump in the infinite loop // unless it is a root node relationship if (relationshipDescription.hasRelationshipObverse() - && processedRelationships.contains(relationshipDescription.getRelationshipObverse())) { + && processed.contains(relationshipDescription.getRelationshipObverse())) { continue; } - generateListFor(parentPath, nodeDescription, relationshipDescription, nodeName, processedRelationships, fieldName, mapProjectionLists, includedProperties); + generateListFor(parentPath, nodeDescription, relationshipDescription, nodeName, processed, fieldName, mapProjectionLists, includedProperties); } return mapProjectionLists; @@ -876,10 +872,15 @@ public enum CypherGenerator { Neo4jPersistentEntity endNodeDescription = (Neo4jPersistentEntity) relationshipDescription.getTarget(); processedRelationships.add(relationshipDescription); - PropertyFilter.RelaxedPropertyPath newParentPath = relationshipDescription.hasRelationshipProperties() - ? parentPath.append(relationshipDescription.getFieldName()).append(((Neo4jPersistentEntity) relationshipDescription.getRelationshipPropertiesEntity()) - .getPersistentProperty(TargetNode.class).getFieldName()) - : parentPath.append(relationshipDescription.getFieldName()); + PropertyFilter.RelaxedPropertyPath newParentPath; + newParentPath = parentPath.append(relationshipDescription.getFieldName()); + if (relationshipDescription.hasRelationshipProperties()) { + var persistentProperty = ((Neo4jPersistentEntity) relationshipDescription.getRequiredRelationshipPropertiesEntity()).getPersistentProperty(TargetNode.class); + if (persistentProperty != null) { + newParentPath = newParentPath + .append(persistentProperty.getFieldName()); + } + } if (relationshipDescription.isDynamic()) { Relationship relationship = relationshipDescription.isOutgoing() @@ -888,7 +889,7 @@ public enum CypherGenerator { relationship = relationship.named(relationshipTargetName); MapProjection mapProjection = projectPropertiesAndRelationships(newParentPath, endNodeDescription, relationshipFieldName, - includedProperties, relationshipDescription, new ArrayList<>(processedRelationships)); + includedProperties, new ArrayList<>(processedRelationships)); if (relationshipDescription.hasRelationshipProperties()) { relationship = relationship.named(relationshipSymbolicName); @@ -906,7 +907,7 @@ public enum CypherGenerator { : startNode.relationshipFrom(endNode, relationshipType); MapProjection mapProjection = projectPropertiesAndRelationships(newParentPath, endNodeDescription, relationshipFieldName, - includedProperties, relationshipDescription, new ArrayList<>(processedRelationships)); + includedProperties, new ArrayList<>(processedRelationships)); if (relationshipDescription.hasRelationshipProperties()) { relationship = relationship.named(relationshipSymbolicName); diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jConversionService.java b/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jConversionService.java index a6c919f42..2902882cd 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jConversionService.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jConversionService.java @@ -16,10 +16,12 @@ package org.springframework.data.neo4j.core.mapping; import java.util.Collection; +import java.util.Objects; import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.Predicate; +import org.jspecify.annotations.Nullable; import org.neo4j.driver.Value; import org.neo4j.driver.Values; import org.springframework.core.CollectionFactory; @@ -32,7 +34,6 @@ import org.springframework.data.neo4j.core.convert.Neo4jConversionService; import org.springframework.data.neo4j.core.convert.Neo4jConversions; import org.springframework.data.neo4j.core.convert.Neo4jPersistentPropertyConverter; import org.springframework.data.util.TypeInformation; -import org.springframework.lang.Nullable; /** * @author Michael J. Simons @@ -68,13 +69,12 @@ final class DefaultNeo4jConversionService implements Neo4jConversionService { @Override @Nullable - public Object readValue(@Nullable Value source, TypeInformation targetType, - @Nullable Neo4jPersistentPropertyConverter conversionOverride) { + public Object readValue(@Nullable Value source, TypeInformation targetType, @Nullable Neo4jPersistentPropertyConverter conversionOverride) { BiFunction, Object> conversion; boolean applyConversionToCompleteCollection = false; if (conversionOverride == null) { - conversion = (v, t) -> conversionService.convert(v, t); + conversion = conversionService::convert; } else { applyConversionToCompleteCollection = conversionOverride instanceof NullSafeNeo4jPersistentPropertyConverter && ((NullSafeNeo4jPersistentPropertyConverter) conversionOverride).isForCollection(); @@ -94,8 +94,10 @@ final class DefaultNeo4jConversionService implements Neo4jConversionService { Class rawType = type.getType(); if (!valueIsLiteralNullOrNullValue && isCollection(type) && !applyConversionToCompleteCollection) { + // value can't be null at this point in time + @SuppressWarnings("NullAway") Collection target = CollectionFactory - .createCollection(rawType, type.getComponentType().getType(), value.size()); + .createCollection(rawType, Objects.requireNonNull(type.getComponentType()).getType(), value.size()); value.values() .forEach(element -> target.add(conversion.apply(element, type.getComponentType().getType()))); return target; diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jEntityConverter.java b/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jEntityConverter.java index 70c13bd7e..623a3485e 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jEntityConverter.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jEntityConverter.java @@ -23,6 +23,7 @@ import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; @@ -35,6 +36,8 @@ import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.StreamSupport; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; import org.neo4j.driver.Record; import org.neo4j.driver.Value; import org.neo4j.driver.Values; @@ -57,8 +60,6 @@ import org.springframework.data.neo4j.core.mapping.callback.EventSupport; import org.springframework.data.neo4j.core.schema.TargetNode; import org.springframework.data.util.ReflectionUtils; import org.springframework.data.util.TypeInformation; -import org.springframework.lang.NonNull; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -107,18 +108,20 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter { } @Override - @Nullable public R read(Class targetType, MapAccessor mapAccessor) { knownObjects.nextRecord(); labelNodeCache.clear(); @SuppressWarnings("unchecked") // ¯\_(ツ)_/¯ - Neo4jPersistentEntity rootNodeDescription = (Neo4jPersistentEntity) nodeDescriptionStore.getNodeDescription(targetType); + Neo4jPersistentEntity rootNodeDescription = Objects.requireNonNull((Neo4jPersistentEntity) nodeDescriptionStore.getNodeDescription(targetType), () -> "Can't read an entity of type %s without description".formatted(targetType)); MapAccessor queryRoot = determineQueryRoot(mapAccessor, rootNodeDescription, true); + if (queryRoot == null) { + throw new IllegalStateException("No query root"); + } try { - return queryRoot == null ? null : map(queryRoot, queryRoot, rootNodeDescription); + return map(queryRoot, queryRoot, rootNodeDescription); } catch (Exception e) { throw new MappingException("Error mapping " + mapAccessor, e); } @@ -229,10 +232,15 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter { @Override public void write(Object source, Map parameters) { - Map properties = new HashMap<>(); Neo4jPersistentEntity nodeDescription = (Neo4jPersistentEntity) nodeDescriptionStore .getNodeDescription(source.getClass()); + if (nodeDescription == null) { + return; + } + + Map properties = new HashMap<>(); + if (nodeDescription.hasRelationshipPropertyPersistTypeInfoFlag()) { // add type info when write to the database properties.put(Constants.NAME_OF_RELATIONSHIP_TYPE, nodeDescription.getPrimaryLabel()); @@ -275,7 +283,6 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter { Neo4jPersistentProperty vectorProperty = nodeDescription.getRequiredVectorProperty(); parameters.put(Constants.NAME_OF_VECTOR_PROPERTY, vectorProperty.getPropertyName()); parameters.put(Constants.NAME_OF_VECTOR_VALUE, conversionService.writeValue(propertyAccessor.getProperty(vectorProperty), vectorProperty.getTypeInformation(), vectorProperty.getOptionalConverter())); - return; } } @@ -287,8 +294,9 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter { * @param record Record that should be merged * @return A map accessor combining a {@link Node} and an arbitrary record */ + @SuppressWarnings("deprecation") private static MapAccessor mergeRootNodeWithRecord(Node node, MapAccessor record) { - Map mergedAttributes = new HashMap<>(node.size() + record.size() + 1); + Map mergedAttributes = new HashMap<>(node.size() + record.size() + 1); mergedAttributes.put(Constants.NAME_OF_INTERNAL_ID, IdentitySupport.getInternalId(node)); mergedAttributes.put(Constants.NAME_OF_ELEMENT_ID, node.elementId()); @@ -312,8 +320,9 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter { return map(queryResult, nodeDescription, nodeDescription, null, null, relationshipsFromResult, nodesFromResult); } + @SuppressWarnings("unchecked") private ET map(MapAccessor queryResult, Neo4jPersistentEntity nodeDescription, NodeDescription genericTargetNodeDescription, - @Nullable Object lastMappedEntity, @Nullable RelationshipDescription relationshipDescription, Collection relationshipsFromResult, Collection nodesFromResult) { + @Nullable Object lastMappedEntity, @Nullable RelationshipDescription relationshipDescription, @Nullable Collection relationshipsFromResult, Collection nodesFromResult) { // prior to SDN 7 local `getInternalId` didn't check relationships, so in that case, they have never been a known // object. The centralized methods checks those too now. The condition is to recreate the old behaviour without @@ -339,7 +348,8 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter { populateProperties(queryResult, (Neo4jPersistentEntity) genericTargetNodeDescription, nodeDescription, internalId, instance, lastMappedEntity, relationshipsFromResult, nodesFromResult, false); - PersistentPropertyAccessor propertyAccessor = concreteNodeDescription.getPropertyAccessor(getMostCurrentInstance(internalId, instance)); + var mostCurrentInstance = Objects.requireNonNull(getMostCurrentInstance(internalId, instance), "Could not get the most current instance for the internal id %s".formatted(internalId)); + PersistentPropertyAccessor propertyAccessor = concreteNodeDescription.getPropertyAccessor(mostCurrentInstance); ET bean = propertyAccessor.getBean(); bean = eventSupport.maybeCallAfterConvert(bean, concreteNodeDescription, queryResult); @@ -367,7 +377,7 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter { populateProperties(queryResult, (Neo4jPersistentEntity) genericTargetNodeDescription, nodeDescription, internalId, mappedObject, lastMappedEntity, relationshipsFromResult, nodesFromResult, true); } // due to a needed side effect in `populateProperties`, the entity might have been changed - return getMostCurrentInstance(internalId, mappedObject); + return Objects.requireNonNull(getMostCurrentInstance(internalId, mappedObject), "Could not get mapped instance for internal id %s".formatted(internalId)); } private boolean hasMoreFields(Map currentQueryResult, Set> savedQueryResults) { @@ -384,35 +394,37 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter { return !currentFields.isEmpty(); } + @SuppressWarnings("unchecked") @Nullable - private ET getMostCurrentInstance(String internalId, ET fallbackInstance) { - return (ET) (knownObjects.getObject(internalId) != null ? knownObjects.getObject(internalId) : fallbackInstance); + private ET getMostCurrentInstance(@Nullable String internalId, @Nullable ET fallbackInstance) { + return (ET) (internalId != null && knownObjects.getObject(internalId) != null ? knownObjects.getObject(internalId) : fallbackInstance); } - - private void populateProperties(MapAccessor queryResult, Neo4jPersistentEntity baseNodeDescription, Neo4jPersistentEntity moreConcreteNodeDescription, String internalId, + private void populateProperties(MapAccessor queryResult, Neo4jPersistentEntity baseNodeDescription, Neo4jPersistentEntity moreConcreteNodeDescription, @Nullable String internalId, ET mappedObject, @Nullable Object lastMappedEntity, - Collection relationshipsFromResult, Collection nodesFromResult, boolean objectAlreadyMapped) { + @Nullable Collection relationshipsFromResult, Collection nodesFromResult, boolean objectAlreadyMapped) { List allLabels = getLabels(queryResult, moreConcreteNodeDescription); NodeDescriptionAndLabels nodeDescriptionAndLabels = nodeDescriptionStore .deriveConcreteNodeDescription(moreConcreteNodeDescription, allLabels); @SuppressWarnings("unchecked") - Neo4jPersistentEntity concreteNodeDescription = (Neo4jPersistentEntity) nodeDescriptionAndLabels - .getNodeDescription(); + Neo4jPersistentEntity concreteNodeDescription = Objects.requireNonNull((Neo4jPersistentEntity) nodeDescriptionAndLabels + .getNodeDescription(), "Couldn't find required node description"); if (!concreteNodeDescription.requiresPropertyPopulation()) { return; } PersistentPropertyAccessor propertyAccessor = concreteNodeDescription.getPropertyAccessor(mappedObject); - Predicate isConstructorParameter = concreteNodeDescription - .getInstanceCreatorMetadata()::isCreatorParameter; + Predicate isConstructorParameter = parameter -> { + var metadata = concreteNodeDescription.getInstanceCreatorMetadata(); + return metadata != null && metadata.isCreatorParameter(parameter); + }; boolean isKotlinType = KotlinDetector.isKotlinType(concreteNodeDescription.getType()); // Fill simple properties - PropertyHandler handler = populateFrom(queryResult, propertyAccessor, + PropertyHandler<@NonNull Neo4jPersistentProperty> handler = populateFrom(queryResult, propertyAccessor, isConstructorParameter, nodeDescriptionAndLabels.getDynamicLabels(), lastMappedEntity, isKotlinType, objectAlreadyMapped); PropertyHandlerSupport.of(concreteNodeDescription).doWithProperties(handler); // in a cyclic graph / with bidirectional relationships, we could end up in a state in which we @@ -422,10 +434,9 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter { knownObjects.mappedWithQueryResult(internalId, queryResult); AssociationHandlerSupport.of(concreteNodeDescription).doWithAssociations( - populateFrom(queryResult, baseNodeDescription, propertyAccessor, isConstructorParameter, objectAlreadyMapped, relationshipsFromResult, nodesFromResult, internalId)); + populateFrom(queryResult, baseNodeDescription, propertyAccessor, isConstructorParameter, objectAlreadyMapped, relationshipsFromResult, nodesFromResult)); } - @NonNull private Neo4jPersistentEntity getMostConcreteTargetNodeDescription( Neo4jPersistentEntity genericTargetNodeDescription, MapAccessor possibleValueNode) { @@ -444,18 +455,16 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter { * @param queryResult The complete query result * @return The list of labels defined by the query variable {@link Constants#NAME_OF_LABELS}. */ - @NonNull private List getLabels(MapAccessor queryResult, @Nullable NodeDescription nodeDescription) { Value labelsValue = queryResult.get(Constants.NAME_OF_LABELS); List labels = new ArrayList<>(); if (!labelsValue.isNull()) { labels = labelsValue.asList(Value::asString); - } else if (queryResult instanceof Node) { - Node nodeRepresentation = (Node) queryResult; + } else if (queryResult instanceof Node nodeRepresentation) { nodeRepresentation.labels().forEach(labels::add); } else if (queryResult instanceof Relationship) { Value value = queryResult.get(Constants.NAME_OF_RELATIONSHIP_TYPE); - if (value.isNull()) { + if (value.isNull() && nodeDescription != null) { labels.addAll(nodeDescription.getStaticLabels()); } else { labels.add(value.asString()); @@ -484,14 +493,16 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter { private ET instantiate(Neo4jPersistentEntity nodeDescription, NodeDescription genericNodeDescription, MapAccessor values, Collection surplusLabels, @Nullable Object lastMappedEntity, - Collection relationshipsFromResult, Collection nodesFromResult) { + @Nullable Collection relationshipsFromResult, Collection nodesFromResult) { - ParameterValueProvider parameterValueProvider = new ParameterValueProvider() { + ParameterValueProvider<@NonNull Neo4jPersistentProperty> parameterValueProvider = new ParameterValueProvider<>() { - @SuppressWarnings("unchecked") // Needed for the last cast. It's easier that way than using the parameter type info and checking for primitives + @SuppressWarnings("unchecked") + // Needed for the last cast. It's easier that way than using the parameter type info and checking for primitives @Override - public T getParameterValue(Parameter parameter) { - Neo4jPersistentProperty matchingProperty = nodeDescription.getRequiredPersistentProperty(parameter.getName()); + @Nullable + public T getParameterValue(Parameter parameter) { + Neo4jPersistentProperty matchingProperty = nodeDescription.getRequiredPersistentProperty(Objects.requireNonNull(parameter.getName(), "Parameter names are not available")); Object result; if (matchingProperty.isRelationship()) { @@ -499,7 +510,7 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter { .filter(r -> { String propertyFieldName = matchingProperty.getFieldName(); return r.getFieldName().equals(propertyFieldName); - }).findFirst().get(); + }).findFirst().orElseThrow(); // If we cannot find any value it does not mean that there isn't any. // The result set might contain associations not named CONCRETE_TYPE_TARGET but ABSTRACT_TYPE_TARGET. // For this we bubble up the hierarchy of NodeDescriptions. @@ -522,7 +533,7 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter { } else if (matchingProperty.isEntityWithRelationshipProperties()) { result = lastMappedEntity; } else { - result = conversionService.readValue(extractValueOf(matchingProperty, values), parameter.getType(), matchingProperty.getOptionalConverter()); + result = conversionService.readValue(extractValueOf(matchingProperty, values), parameter.getType(), matchingProperty.getOptionalConverter()); } return (T) result; } @@ -531,7 +542,7 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter { return entityInstantiators.getInstantiatorFor(nodeDescription).createInstance(nodeDescription, parameterValueProvider); } - private PropertyHandler populateFrom(MapAccessor queryResult, + private PropertyHandler<@NonNull Neo4jPersistentProperty> populateFrom(MapAccessor queryResult, PersistentPropertyAccessor propertyAccessor, Predicate isConstructorParameter, Collection surplusLabels, @Nullable Object targetNode, boolean ownerIsKotlinType, boolean objectAlreadyMapped) { @@ -561,15 +572,15 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter { }; } - @Nullable - private static Object getValueOrDefault(boolean ownerIsKotlinType, Class rawType, @Nullable Object value) { + private static Object getValueOrDefault(boolean ownerIsKotlinType, Class rawType, Object value) { return value == null && !ownerIsKotlinType && rawType.isPrimitive() ? ReflectionUtils.getPrimitiveDefault(rawType) : value; } - private AssociationHandler populateFrom(MapAccessor queryResult, NodeDescription baseDescription, - PersistentPropertyAccessor propertyAccessor, Predicate isConstructorParameter, - boolean objectAlreadyMapped, Collection relationshipsFromResult, Collection nodesFromResult, String internalId) { + private AssociationHandler<@NonNull Neo4jPersistentProperty> populateFrom(MapAccessor queryResult, NodeDescription baseDescription, + PersistentPropertyAccessor propertyAccessor, Predicate isConstructorParameter, + boolean objectAlreadyMapped, @Nullable Collection relationshipsFromResult, Collection nodesFromResult + ) { return association -> { @@ -590,41 +601,35 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter { Object propertyValue = propertyAccessor.getProperty(persistentProperty); - boolean propertyValueNotNull = propertyValue != null; + if (propertyValue != null) { - boolean populatedCollection = objectAlreadyMapped && persistentProperty.isCollectionLike() - && propertyValueNotNull - && !((Collection) propertyValue).isEmpty(); + boolean populatedCollection = objectAlreadyMapped && persistentProperty.isCollectionLike() && !((Collection) propertyValue).isEmpty(); + boolean populatedMap = objectAlreadyMapped && persistentProperty.isMap() && !((Map) propertyValue).isEmpty(); + boolean populatedScalarValue = objectAlreadyMapped && !persistentProperty.isCollectionLike() && !persistentProperty.isMap(); - boolean populatedMap = objectAlreadyMapped && persistentProperty.isMap() - && propertyValueNotNull - && !((Map) propertyValue).isEmpty(); + if (populatedCollection) { + createInstanceOfRelationships(persistentProperty, queryResult, (RelationshipDescription) association, baseDescription, relationshipsFromResult, nodesFromResult, false) + .ifPresent(value -> { + Collection providedCollection = (Collection) value; + Collection existingValue = (Collection) propertyValue; + Collection newValue = CollectionFactory.createCollection(existingValue.getClass(), providedCollection.size() + existingValue.size()); - boolean populatedScalarValue = objectAlreadyMapped && !persistentProperty.isCollectionLike() && !persistentProperty.isMap() - && propertyValueNotNull; + RelationshipDescription relationshipDescription = (RelationshipDescription) association; + Map mergedValues = new HashMap<>(); + mergeCollections(relationshipDescription, existingValue, mergedValues); + mergeCollections(relationshipDescription, providedCollection, mergedValues); - if (populatedCollection) { - createInstanceOfRelationships(persistentProperty, queryResult, (RelationshipDescription) association, baseDescription, relationshipsFromResult, nodesFromResult, false) - .ifPresent(value -> { - Collection providedCollection = (Collection) value; - Collection existingValue = (Collection) propertyValue; - Collection newValue = CollectionFactory.createCollection(existingValue.getClass(), providedCollection.size() + existingValue.size()); + newValue.addAll(mergedValues.values()); + propertyAccessor.setProperty(persistentProperty, newValue); + }); + } - RelationshipDescription relationshipDescription = (RelationshipDescription) association; - Map mergedValues = new HashMap<>(); - mergeCollections(relationshipDescription, existingValue, mergedValues); - mergeCollections(relationshipDescription, providedCollection, mergedValues); + boolean propertyAlreadyPopulated = populatedCollection || populatedMap || populatedScalarValue; - newValue.addAll(mergedValues.values()); - propertyAccessor.setProperty(persistentProperty, newValue); - }); - } - - boolean propertyAlreadyPopulated = populatedCollection || populatedMap || populatedScalarValue; - - // avoid unnecessary re-assignment of values - if (propertyAlreadyPopulated) { - return; + // avoid unnecessary re-assignment of values + if (propertyAlreadyPopulated) { + return; + } } createInstanceOfRelationships(persistentProperty, queryResult, (RelationshipDescription) association, baseDescription, relationshipsFromResult, nodesFromResult) @@ -636,15 +641,17 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter { private void mergeCollections(RelationshipDescription relationshipDescription, Collection values, Map mergedValues) { for (Object existingValueInCollection : values) { if (relationshipDescription.hasRelationshipProperties()) { - Object existingIdPropertyValue = ((Neo4jPersistentEntity) relationshipDescription.getRelationshipPropertiesEntity()) + Neo4jPersistentEntity relationshipPropertiesEntity = (Neo4jPersistentEntity) relationshipDescription.getRequiredRelationshipPropertiesEntity(); + Object existingIdPropertyValue = relationshipPropertiesEntity .getPropertyAccessor(existingValueInCollection) - .getProperty(((Neo4jPersistentEntity) relationshipDescription.getRelationshipPropertiesEntity()).getIdProperty()); + .getProperty(relationshipPropertiesEntity.getRequiredIdProperty()); mergedValues.put(existingIdPropertyValue, existingValueInCollection); } else if (!relationshipDescription.isDynamic()) { // should not happen because this is all inside populatedCollection (but better safe than sorry) - Object existingIdPropertyValue = ((Neo4jPersistentEntity) relationshipDescription.getTarget()) + Neo4jPersistentEntity target = (Neo4jPersistentEntity) relationshipDescription.getTarget(); + Object existingIdPropertyValue = target .getPropertyAccessor(existingValueInCollection) - .getProperty(((Neo4jPersistentEntity) relationshipDescription.getTarget()).getIdProperty()); + .getProperty(target.getRequiredIdProperty()); mergedValues.put(existingIdPropertyValue, existingValueInCollection); } @@ -652,14 +659,15 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter { } private Optional createInstanceOfRelationships(Neo4jPersistentProperty persistentProperty, MapAccessor values, - RelationshipDescription relationshipDescription, NodeDescription baseDescription, Collection relationshipsFromResult, + RelationshipDescription relationshipDescription, NodeDescription baseDescription, @Nullable Collection relationshipsFromResult, Collection nodesFromResult) { return createInstanceOfRelationships(persistentProperty, values, relationshipDescription, baseDescription, relationshipsFromResult, nodesFromResult, true); } + @SuppressWarnings("deprecation") private Optional createInstanceOfRelationships(Neo4jPersistentProperty persistentProperty, MapAccessor values, - RelationshipDescription relationshipDescription, NodeDescription baseDescription, Collection relationshipsFromResult, - Collection nodesFromResult, boolean fetchMore) { + RelationshipDescription relationshipDescription, NodeDescription baseDescription, @Nullable Collection relationshipsFromResult, + Collection nodesFromResult, boolean fetchMore) { String typeOfRelationship = relationshipDescription.getType(); String targetLabel = relationshipDescription.getTarget().getPrimaryLabel(); @@ -672,8 +680,9 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter { BiConsumer mappedObjectHandler; Function keyTransformer; - if (persistentProperty.isDynamicAssociation() && persistentProperty.getComponentType().isEnum()) { - keyTransformer = f -> conversionService.convert(f, persistentProperty.getComponentType()); + Class componentType = persistentProperty.getComponentType(); + if (persistentProperty.isDynamicAssociation() && (componentType != null && componentType.isEnum())) { + keyTransformer = f -> conversionService.convert(f, componentType); } else { keyTransformer = Function.identity(); } @@ -747,7 +756,7 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter { // and therefor should not be in the list. // Otherwise, for highly linked data it could potentially cause a StackOverflowError. String direction = relationshipDescription.getDirection().name(); - if (knownObjects.hasProcessedRelationshipCompletely("R" + direction + IdentitySupport.getElementId(possibleRelationship))) { + if (relationshipsFromResult != null && knownObjects.hasProcessedRelationshipCompletely("R" + direction + IdentitySupport.getElementId(possibleRelationship))) { relationshipsFromResult.remove(possibleRelationship); } // If the target is the same(equal) node, get the related object from the cache. @@ -769,7 +778,7 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter { if (relationshipDescription.hasRelationshipProperties()) { Object relationshipProperties; - Neo4jPersistentEntity relationshipPropertiesEntity = (Neo4jPersistentEntity) relationshipDescription.getRelationshipPropertiesEntity(); + Neo4jPersistentEntity relationshipPropertiesEntity = (Neo4jPersistentEntity) relationshipDescription.getRequiredRelationshipPropertiesEntity(); if (fetchMore) { relationshipProperties = map(possibleRelationship, relationshipPropertiesEntity, relationshipPropertiesEntity, mappedObject, relationshipDescription, relationshipsFromResult, nodesFromResult); } else { @@ -813,7 +822,7 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter { .asRelationship(); Object relationshipProperties; - Neo4jPersistentEntity relationshipPropertiesEntity = (Neo4jPersistentEntity) relationshipDescription.getRelationshipPropertiesEntity(); + Neo4jPersistentEntity relationshipPropertiesEntity = (Neo4jPersistentEntity) relationshipDescription.getRequiredRelationshipPropertiesEntity(); if (fetchMore) { relationshipProperties = map(relatedEntityRelationship, relationshipPropertiesEntity, relationshipPropertiesEntity, valueEntry, relationshipDescription, relationshipsFromResult, nodesFromResult); } else { @@ -834,7 +843,7 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter { if (persistentProperty.getTypeInformation().isCollectionLike()) { List returnedValues = relationshipDescription.hasRelationshipProperties() ? relationshipsAndProperties : value; - Collection target = CollectionFactory.createCollection(persistentProperty.getRawType(), persistentProperty.getComponentType(), returnedValues.size()); + Collection target = CollectionFactory.createCollection(persistentProperty.getRawType(), componentType, returnedValues.size()); target.addAll(returnedValues); return Optional.of(target); } else { @@ -874,14 +883,14 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter { return allNodesInResult; } - private Collection extractMatchingRelationships(Collection relationshipsFromResult, + private Collection extractMatchingRelationships(@Nullable Collection relationshipsFromResult, RelationshipDescription relationshipDescription, String typeOfRelationship, Predicate relationshipPredicate) { Predicate onlyWithMatchingType = r -> r.type().equals(typeOfRelationship) || relationshipDescription.isDynamic(); - return relationshipsFromResult.stream() + return (relationshipsFromResult != null) ? relationshipsFromResult.stream() .filter(onlyWithMatchingType.and(relationshipPredicate)) - .collect(Collectors.toList()); + .collect(Collectors.toList()) : List.of(); } private Collection extractRelationships(MapAccessor allValues) { @@ -898,6 +907,7 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter { return allRelationshipsInResult; } + @SuppressWarnings("deprecation") private static Value extractValueOf(Neo4jPersistentProperty property, MapAccessor propertyContainer) { if (property.isInternalIdProperty()) { if (Neo4jPersistentEntity.DEPRECATED_GENERATED_ID_TYPES.contains(property.getType())) { @@ -974,7 +984,7 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter { } } - private boolean isInCreation(@Nullable String internalId) { + private boolean isInCreation(String internalId) { if (internalId == null) { return false; } @@ -1037,7 +1047,7 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter { read.lock(); - return previousRecords.contains(internalId) || internalCurrentRecord.get(internalId); + return previousRecords.contains(internalId) || Optional.ofNullable(internalCurrentRecord.get(internalId)).orElse(Boolean.FALSE); } finally { read.unlock(); @@ -1074,20 +1084,25 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter { internalCurrentRecord.clear(); } - private void mappedWithQueryResult(String internalId, MapAccessor queryResult) { - try { - write.lock(); - mappedQueryResults.computeIfAbsent(internalId, id -> ConcurrentHashMap.newKeySet()) - .add(queryResult.asMap()); - } finally { - write.unlock(); + private void mappedWithQueryResult(@Nullable String internalId, MapAccessor queryResult) { + if (internalId != null) { + try { + write.lock(); + mappedQueryResults.computeIfAbsent(internalId, id -> ConcurrentHashMap.newKeySet()) + .add(queryResult.asMap()); + } finally { + write.unlock(); + } } } - private Set> getQueryResultsFor(String internalId) { + private Set> getQueryResultsFor(@Nullable String internalId) { + if (internalId == null) { + return Set.of(); + } try { read.lock(); - return mappedQueryResults.get(internalId); + return Objects.requireNonNullElseGet(mappedQueryResults.get(internalId), Set::of); } finally { read.unlock(); } diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jIsNewStrategy.java b/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jIsNewStrategy.java index 85fc99759..a54a5741a 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jIsNewStrategy.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jIsNewStrategy.java @@ -15,12 +15,12 @@ */ package org.springframework.data.neo4j.core.mapping; +import java.util.Objects; import java.util.function.Function; import org.apache.commons.logging.LogFactory; import org.springframework.core.log.LogAccessor; import org.springframework.data.support.IsNewStrategy; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -53,7 +53,7 @@ final class DefaultNeo4jIsNewStrategy implements IsNewStrategy { Assert.notNull(entityMetaData, "Entity meta data must not be null"); - IdDescription idDescription = entityMetaData.getIdDescription(); + IdDescription idDescription = Objects.requireNonNull(entityMetaData.getIdDescription(), () -> "Cannot determine id description for entity %s".formatted(entityMetaData.getType())); Class valueType = entityMetaData.getRequiredIdProperty().getType(); if (idDescription.isExternallyGeneratedId() && valueType.isPrimitive()) { @@ -84,7 +84,7 @@ final class DefaultNeo4jIsNewStrategy implements IsNewStrategy { private final Class valueType; - private @Nullable final Function valueLookup; + private final Function valueLookup; private DefaultNeo4jIsNewStrategy(IdDescription idDescription, Class valueType, Function valueLookup) { @@ -104,7 +104,7 @@ final class DefaultNeo4jIsNewStrategy implements IsNewStrategy { if (idDescription.isInternallyGeneratedId()) { boolean isNew; - if (value != null && valueType.isPrimitive() && Number.class.isInstance(value)) { + if (value != null && valueType.isPrimitive() && value instanceof Number) { isNew = ((Number) value).longValue() < 0; } else { isNew = value == null; @@ -118,7 +118,7 @@ final class DefaultNeo4jIsNewStrategy implements IsNewStrategy { return value == null; } - if (Number.class.isInstance(value)) { + if (value instanceof Number) { return ((Number) value).longValue() == 0; } } diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentEntity.java b/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentEntity.java index 83550c981..0565279e7 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentEntity.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentEntity.java @@ -22,6 +22,7 @@ import java.util.Collections; import java.util.Comparator; import java.util.HashSet; import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.TreeSet; @@ -32,6 +33,8 @@ import java.util.stream.Collectors; import java.util.stream.Stream; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.log.LogAccessor; import org.springframework.data.annotation.Persistent; @@ -49,8 +52,6 @@ import org.springframework.data.neo4j.core.schema.TargetNode; import org.springframework.data.support.IsNewStrategy; import org.springframework.data.util.Lazy; import org.springframework.data.util.TypeInformation; -import org.springframework.lang.NonNull; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -85,6 +86,7 @@ final class DefaultNeo4jPersistentEntity extends BasicPersistentEntity isRelationshipPropertiesEntity; + @Nullable private NodeDescription parentNodeDescription; private List> childNodeDescriptionsInHierarchy; @@ -255,14 +257,14 @@ final class DefaultNeo4jPersistentEntity extends BasicPersistentEntity targetEntities = new HashSet<>(); - AssociationHandlerSupport.of(this).doWithAssociations((Association association) -> { + Set> targetEntities = new HashSet<>(); + AssociationHandlerSupport.of(this).doWithAssociations((Association<@NonNull Neo4jPersistentProperty> association) -> { Neo4jPersistentProperty inverse = association.getInverse(); if (inverse.isDynamicAssociation()) { Relationship relationship = inverse.findAnnotation(Relationship.class); Assert.state(relationship == null || relationship.type().isEmpty(), () -> "Dynamic relationships cannot be used with a fixed type; omit @Relationship or use @Relationship(direction = " - + relationship.direction().name() + ") without a type in " + this.getUnderlyingClass() + " on field " + + Optional.ofNullable(relationship).map(Relationship::direction).orElse(Relationship.Direction.OUTGOING).name() + ") without a type in " + this.getUnderlyingClass() + " on field " + inverse.getFieldName()); Assert.state(!targetEntities.contains(inverse.getAssociationTargetType()), @@ -329,7 +331,6 @@ final class DefaultNeo4jPersistentEntity extends BasicPersistentEntity type) { Node nodeAnnotation = AnnotatedElementUtils.findMergedAnnotation(type, Node.class); @@ -365,7 +366,6 @@ final class DefaultNeo4jPersistentEntity extends BasicPersistentEntity computeOwnAdditionalLabels() { List result = new ArrayList<>(); @@ -398,7 +398,6 @@ final class DefaultNeo4jPersistentEntity extends BasicPersistentEntity computeParentLabels() { List parentLabels = new ArrayList<>(); Neo4jPersistentEntity parentNodeDescriptionCalculated = (Neo4jPersistentEntity) parentNodeDescription; @@ -430,9 +429,10 @@ final class DefaultNeo4jPersistentEntity extends BasicPersistentEntity true).orElse(false); + return getVectorProperty() != null; } + @Nullable public Neo4jPersistentProperty getVectorProperty() { return this.vectorProperty.getNullable(); } @@ -511,13 +511,11 @@ final class DefaultNeo4jPersistentEntity extends BasicPersistentEntity getRelationshipsInHierarchy(Predicate propertyFilter) { return getRelationshipsInHierarchy(propertyFilter, PropertyFilter.RelaxedPropertyPath.withRootType(this.getUnderlyingClass())); } - @NonNull public Collection getRelationshipsInHierarchy(Predicate propertyFilter, PropertyFilter.RelaxedPropertyPath path) { Collection relationships = new HashSet<>(getRelationships()); @@ -599,7 +597,7 @@ final class DefaultNeo4jPersistentEntity extends BasicPersistentEntity parent) { + public void setParentNodeDescription(@Nullable NodeDescription parent) { this.parentNodeDescription = parent; } @@ -634,9 +632,14 @@ final class DefaultNeo4jPersistentEntity extends BasicPersistentEntity) relationship.getRelationshipPropertiesEntity()).getPersistentProperty(TargetNode.class).getFieldName() - : ""; + String relationshipPropertiesPrefix; + if (!relationship.hasRelationshipProperties()) { + relationshipPropertiesPrefix = ""; + } else { + Neo4jPersistentEntity relationshipPropertiesEntity = (Neo4jPersistentEntity) relationship.getRequiredRelationshipPropertiesEntity(); + var targetNodeProperty = Objects.requireNonNull(relationshipPropertiesEntity.getPersistentProperty(TargetNode.class), () -> "Could not get target node property on %s".formatted(relationshipPropertiesEntity.getType())); + relationshipPropertiesPrefix = "." + targetNodeProperty.getFieldName(); + } PropertyFilter.RelaxedPropertyPath nextPath = relaxedPropertyPath.append(relationship.getFieldName() + relationshipPropertiesPrefix); if (calculatePossibleCircles(targetNode, visitedNodes, includeField, nextPath)) { return true; @@ -661,8 +664,14 @@ final class DefaultNeo4jPersistentEntity extends BasicPersistentEntity) relationship.getRelationshipPropertiesEntity()) - .getPersistentProperty(TargetNode.class).getFieldName() : ""; + String relationshipPropertiesPrefix; + if (!relationship.hasRelationshipProperties()) { + relationshipPropertiesPrefix = ""; + } else { + Neo4jPersistentEntity relationshipPropertiesEntity = (Neo4jPersistentEntity) relationship.getRequiredRelationshipPropertiesEntity(); + var targetNodeProperty = Objects.requireNonNull(relationshipPropertiesEntity.getPersistentProperty(TargetNode.class), () -> "Could not get target node property on %s".formatted(relationshipPropertiesEntity.getType())); + relationshipPropertiesPrefix = "." + targetNodeProperty.getFieldName(); + } if (calculatePossibleCircles(targetNode, branchedVisitedNodes, includeField, path.append(relationship.getFieldName() + relationshipPropertiesPrefix))) { return true; } diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentProperty.java b/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentProperty.java index c14f13bcd..515cb6b5a 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentProperty.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentProperty.java @@ -18,8 +18,11 @@ package org.springframework.data.neo4j.core.mapping; import java.lang.reflect.Field; import java.lang.reflect.ParameterizedType; import java.util.Collections; +import java.util.Objects; import java.util.Optional; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; import org.springframework.core.ResolvableType; import org.springframework.data.annotation.ReadOnlyProperty; import org.springframework.data.mapping.Association; @@ -37,8 +40,6 @@ import org.springframework.data.neo4j.core.schema.TargetNode; import org.springframework.data.util.Lazy; import org.springframework.data.util.ReflectionUtils; import org.springframework.data.util.TypeInformation; -import org.springframework.lang.NonNull; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -63,7 +64,8 @@ final class DefaultNeo4jPersistentProperty extends AnnotationBasedPersistentProp private final Lazy> customConversion; - private final @Nullable PersistentPropertyCharacteristics optionalCharacteristics; + @Nullable + private final PersistentPropertyCharacteristics optionalCharacteristics; /** * Creates a new {@link AnnotationBasedPersistentProperty}. @@ -110,7 +112,7 @@ final class DefaultNeo4jPersistentProperty extends AnnotationBasedPersistentProp } @Override - protected Association createAssociation() { + protected Association<@NonNull Neo4jPersistentProperty> createAssociation() { Neo4jPersistentEntity obverseOwner; @@ -120,31 +122,29 @@ final class DefaultNeo4jPersistentProperty extends AnnotationBasedPersistentProp if (this.hasActualTypeAnnotation(RelationshipProperties.class)) { TypeInformation typeInformation = getRelationshipPropertiesTargetType(getActualType()); - obverseOwner = this.mappingContext.addPersistentEntity(typeInformation).get(); - relationshipPropertiesClass = this.mappingContext.addPersistentEntity(TypeInformation.of(getActualType())).get(); + obverseOwner = this.mappingContext.addPersistentEntity(typeInformation).orElseThrow(); + relationshipPropertiesClass = this.mappingContext.addPersistentEntity(TypeInformation.of(getActualType())).orElseThrow(); } else { - Class associationTargetType = this.getAssociationTargetType(); + Class associationTargetType = Objects.requireNonNull(this.getAssociationTargetType()); obverseOwner = this.mappingContext.addPersistentEntity(TypeInformation.of(associationTargetType)).orElse(null); Assert.notNull(obverseOwner, "Obverse owner could not be added"); if (dynamicAssociation) { - TypeInformation mapValueType = this.getTypeInformation().getMapValueType(); + TypeInformation mapValueType = Objects.requireNonNull(this.getTypeInformation().getMapValueType()); + TypeInformation componentType = mapValueType.getComponentType(); + if (componentType != null) { + TypeInformation actualType = mapValueType.getActualType(); - boolean relationshipPropertiesCollection = - this.mappingContext.getPersistentEntity(mapValueType.getActualType().getType()) - .isRelationshipPropertiesEntity(); + if (actualType != null && this.mappingContext.getRequiredPersistentEntity(actualType.getType()) + .isRelationshipPropertiesEntity()) { + TypeInformation typeInformation = getRelationshipPropertiesTargetType(actualType.getType()); + obverseOwner = this.mappingContext.addPersistentEntity(typeInformation).orElseThrow(); + relationshipPropertiesClass = this.mappingContext + .addPersistentEntity(componentType).orElseThrow(); - boolean relationshipPropertiesScalar = - mapValueType.getType().isAnnotationPresent(RelationshipProperties.class); - - if (relationshipPropertiesCollection) { - TypeInformation typeInformation = getRelationshipPropertiesTargetType(mapValueType.getActualType().getType()); - obverseOwner = this.mappingContext.addPersistentEntity(typeInformation).get(); - relationshipPropertiesClass = this.mappingContext - .addPersistentEntity(mapValueType.getComponentType()).get(); - - } else if (relationshipPropertiesScalar) { - relationshipPropertiesClass = this.mappingContext.addPersistentEntity(mapValueType.getComponentType()).get(); + } else if (mapValueType.getType().isAnnotationPresent(RelationshipProperties.class)) { + relationshipPropertiesClass = this.mappingContext.addPersistentEntity(componentType).orElseThrow(); + } } } } @@ -164,6 +164,8 @@ final class DefaultNeo4jPersistentProperty extends AnnotationBasedPersistentProp // Try to determine if there is a relationship definition that expresses logically the same relationship // on the other end. + // At this point, obverseOwner can't be null + @SuppressWarnings("NullAway") Optional obverseRelationshipDescription = obverseOwner.getRelationships().stream() .filter(rel -> rel.getType().equals(type) && rel.getTarget().equals(this.getOwner()) @@ -180,7 +182,6 @@ final class DefaultNeo4jPersistentProperty extends AnnotationBasedPersistentProp return relationshipDescription; } - @NonNull private TypeInformation getRelationshipPropertiesTargetType(Class relationshipPropertiesType) { Field targetNodeField = ReflectionUtils.findField(relationshipPropertiesType, @@ -190,8 +191,8 @@ final class DefaultNeo4jPersistentProperty extends AnnotationBasedPersistentProp throw new MappingException("Missing @TargetNode declaration in " + relationshipPropertiesType); } TypeInformation relationshipPropertiesTypeInformation = TypeInformation.of(relationshipPropertiesType); - Class type = relationshipPropertiesTypeInformation.getProperty(targetNodeField.getName()).getType(); - if (Object.class == type && this.getField().getGenericType() instanceof ParameterizedType pt && pt.getActualTypeArguments().length == 1) { + Class type = Objects.requireNonNull(relationshipPropertiesTypeInformation.getProperty(targetNodeField.getName())).getType(); + if (Object.class == type && this.getRequiredField().getGenericType() instanceof ParameterizedType pt && pt.getActualTypeArguments().length == 1) { return TypeInformation.of(ResolvableType.forType(pt.getActualTypeArguments()[0])); } return TypeInformation.of(type); @@ -231,6 +232,7 @@ final class DefaultNeo4jPersistentProperty extends AnnotationBasedPersistentProp } @Override + @Nullable public Neo4jPersistentPropertyConverter getOptionalConverter() { return isEntity() ? null : customConversion.getOptional() .map(Neo4jPersistentPropertyConverter.class::cast) @@ -253,8 +255,7 @@ final class DefaultNeo4jPersistentProperty extends AnnotationBasedPersistentProp .findAnnotation(org.springframework.data.neo4j.core.schema.Property.class); String targetName = this.getName(); - if (propertyAnnotation != null && !propertyAnnotation.name().isEmpty() - && propertyAnnotation.name().trim().length() != 0) { + if (propertyAnnotation != null && !propertyAnnotation.name().trim().isEmpty()) { targetName = propertyAnnotation.name().trim(); } diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultRelationshipDescription.java b/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultRelationshipDescription.java index c20eac72e..b1f5f8781 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultRelationshipDescription.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultRelationshipDescription.java @@ -17,16 +17,17 @@ package org.springframework.data.neo4j.core.mapping; import java.util.Objects; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; import org.springframework.data.mapping.Association; import org.springframework.data.neo4j.core.schema.Relationship; -import org.springframework.lang.Nullable; /** * @author Michael J. Simons * @author Gerrit Meier * @since 6.0 */ -final class DefaultRelationshipDescription extends Association implements RelationshipDescription { +final class DefaultRelationshipDescription extends Association<@NonNull Neo4jPersistentProperty> implements RelationshipDescription { private final String type; @@ -40,8 +41,10 @@ final class DefaultRelationshipDescription extends Association relationshipPropertiesClass; + @Nullable private RelationshipDescription relationshipObverse; private final boolean cascadeUpdates; @@ -97,6 +100,7 @@ final class DefaultRelationshipDescription extends Association getRelationshipPropertiesEntity() { return relationshipPropertiesClass; } @@ -107,11 +111,12 @@ final class DefaultRelationshipDescription extends Association sourceEntity = context.getRequiredPersistentEntity(entityInstance.getClass()); PersistentPropertyAccessor sourceAccessor = sourceEntity.getPropertyAccessor(entityInstance); - Neo4jPersistentEntity targetEntity = context.addPersistentEntity(TypeInformation.of(targetType)).orElse(null); - Assert.notNull(targetEntity, "Target entity could not be created for a DTO"); + Neo4jPersistentEntity targetEntity = context.addPersistentEntity(TypeInformation.of(targetType)).orElseThrow(() -> new IllegalStateException("Target entity could not be created for a DTO")); InstanceCreatorMetadata creator = targetEntity.getInstanceCreatorMetadata(); Object dto = context.getInstantiatorFor(targetEntity) @@ -98,7 +98,6 @@ public final class DtoInstantiatingConverter implements Converter targetProperty, PersistentEntity sourceEntity, PersistentPropertyAccessor sourceAccessor) { @@ -111,18 +110,15 @@ public final class DtoInstantiatingConverter implements Converter targetEntity = context.addPersistentEntity(TypeInformation.of(targetType)) .orElseThrow(() -> new MappingException( "Could not add a persistent entity for the projection target type '" + targetType.getName() + "'")); - InstanceCreatorMetadata> creator = targetEntity.getInstanceCreatorMetadata(); + InstanceCreatorMetadata<@NonNull ? extends PersistentProperty> creator = targetEntity.getInstanceCreatorMetadata(); Object dto = context.getInstantiatorFor(targetEntity) .createInstance(targetEntity, diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/EntityFromDtoInstantiatingConverter.java b/src/main/java/org/springframework/data/neo4j/core/mapping/EntityFromDtoInstantiatingConverter.java index 65a0daf7e..94b978c28 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/EntityFromDtoInstantiatingConverter.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/EntityFromDtoInstantiatingConverter.java @@ -17,9 +17,11 @@ package org.springframework.data.neo4j.core.mapping; import java.util.Collection; import java.util.Map; +import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; import org.apiguardian.api.API; +import org.jspecify.annotations.Nullable; import org.springframework.core.CollectionFactory; import org.springframework.core.convert.converter.Converter; import org.springframework.data.mapping.InstanceCreatorMetadata; @@ -33,7 +35,6 @@ import org.springframework.data.neo4j.core.convert.Neo4jConversionService; import org.springframework.data.neo4j.core.schema.TargetNode; import org.springframework.data.util.ReflectionUtils; import org.springframework.data.util.TypeInformation; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -65,6 +66,7 @@ public final class EntityFromDtoInstantiatingConverter implements Converter implements Converter sourceEntity = context.addPersistentEntity(TypeInformation.of(dtoInstance.getClass())) - .get(); + .orElseThrow(); PersistentPropertyAccessor sourceAccessor = sourceEntity.getPropertyAccessor(dtoInstance); - PersistentEntity targetEntity = context.getPersistentEntity(targetEntityType); - InstanceCreatorMetadata creator = targetEntity.getInstanceCreatorMetadata(); + PersistentEntity targetEntity = Objects.requireNonNull(context.getPersistentEntity(targetEntityType)); + InstanceCreatorMetadata creator = Objects.requireNonNull(targetEntity.getInstanceCreatorMetadata()); @SuppressWarnings({ "rawtypes", "unchecked" }) T entity = (T) context.getInstantiatorFor(targetEntity) .createInstance(targetEntity, new ParameterValueProvider() { @Override + @Nullable public Object getParameterValue(Parameter parameter) { - PersistentProperty targetProperty = targetEntity.getPersistentProperty(parameter.getName()); + PersistentProperty targetProperty = targetEntity.getPersistentProperty(Objects.requireNonNull(parameter.getName(), "Parameter names are not available")); if (targetProperty == null) { throw new MappingException("Cannot map constructor parameter " + parameter.getName() + " to a property of class " + targetEntityType); diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/IdDescription.java b/src/main/java/org/springframework/data/neo4j/core/mapping/IdDescription.java index b15f5b9d7..fad47c6ca 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/IdDescription.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/IdDescription.java @@ -18,6 +18,7 @@ package org.springframework.data.neo4j.core.mapping; import java.util.Optional; import org.apiguardian.api.API; +import org.jspecify.annotations.Nullable; import org.neo4j.cypherdsl.core.Cypher; import org.neo4j.cypherdsl.core.Expression; import org.neo4j.cypherdsl.core.Node; @@ -25,7 +26,6 @@ import org.neo4j.cypherdsl.core.SymbolicName; import org.springframework.data.neo4j.core.schema.GeneratedValue; import org.springframework.data.neo4j.core.schema.IdGenerator; import org.springframework.data.util.Lazy; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -40,17 +40,20 @@ public final class IdDescription { /** * The class representing a generator for new ids or null for assigned ids. */ - private @Nullable final Class> idGeneratorClass; + @Nullable + private final Class> idGeneratorClass; /** * A reference to an ID generator. */ - private @Nullable final String idGeneratorRef; + @Nullable + private final String idGeneratorRef; /** * The property that stores the id if applicable. */ - private @Nullable final String graphPropertyName; + @Nullable + private final String graphPropertyName; private final boolean isDeprecated; private final Lazy idExpression; @@ -70,8 +73,8 @@ public final class IdDescription { } public static IdDescription forExternallyGeneratedIds(SymbolicName symbolicName, - @Nullable Class> idGeneratorClass, - @Nullable String idGeneratorRef, String graphPropertyName) { + Class> idGeneratorClass, + String idGeneratorRef, String graphPropertyName) { Assert.notNull(graphPropertyName, "Graph property name is required"); try { @@ -87,8 +90,9 @@ public final class IdDescription { } } + @SuppressWarnings("deprecation") private IdDescription(SymbolicName symbolicName, @Nullable Class> idGeneratorClass, - @Nullable String idGeneratorRef, @Nullable String graphPropertyName, boolean isDeprecated) { + @Nullable String idGeneratorRef, @Nullable String graphPropertyName, boolean isDeprecated) { this.idGeneratorClass = idGeneratorClass; this.idGeneratorRef = idGeneratorRef != null && idGeneratorRef.isEmpty() ? null : idGeneratorRef; @@ -98,7 +102,6 @@ public final class IdDescription { this.idExpression = Lazy.of(() -> { final Node rootNode = Cypher.anyNode(symbolicName); if (this.isInternallyGeneratedId()) { - //noinspection deprecation return isDeprecated ? rootNode.internalId() : rootNode.elementId(); } else { return this.getOptionalGraphPropertyName() @@ -118,14 +121,14 @@ public final class IdDescription { * @param nodeName use this name as the symbolic name of the node in the query * @return An expression that represents the right identifier type. */ + @SuppressWarnings("deprecation") public Expression asIdExpression(String nodeName) { final Node rootNode = Cypher.anyNode(nodeName); if (this.isInternallyGeneratedId()) { - //noinspection deprecation return isDeprecated ? rootNode.internalId() : rootNode.elementId(); } else { return this.getOptionalGraphPropertyName() - .map(propertyName -> Cypher.property(nodeName, propertyName)).get(); + .map(propertyName -> Cypher.property(nodeName, propertyName)).orElseThrow(); } } diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/IdentitySupport.java b/src/main/java/org/springframework/data/neo4j/core/mapping/IdentitySupport.java index 925dbd5d1..a469e6c03 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/IdentitySupport.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/IdentitySupport.java @@ -20,14 +20,13 @@ import static org.apiguardian.api.API.Status.INTERNAL; import java.util.function.Function; import org.apiguardian.api.API; +import org.jspecify.annotations.Nullable; import org.neo4j.driver.Value; import org.neo4j.driver.types.Entity; import org.neo4j.driver.types.MapAccessor; import org.neo4j.driver.types.Node; import org.neo4j.driver.types.Relationship; import org.neo4j.driver.types.TypeSystem; -import org.springframework.lang.NonNull; -import org.springframework.lang.Nullable; /** * This class is not part of any public API and will be changed without further notice as needed. It's @@ -61,7 +60,7 @@ public final class IdentitySupport { * @return An internal id */ @Nullable - public static String getElementId(@NonNull MapAccessor row) { + public static String getElementId(MapAccessor row) { if (row instanceof Entity entity) { return getElementId(entity); } @@ -77,9 +76,10 @@ public final class IdentitySupport { return value.asString(); } - @Nullable + @SuppressWarnings("DeprecatedIsStillUsed") @Deprecated - public static Long getInternalId(@NonNull MapAccessor row) { + @Nullable + public static Long getInternalId(MapAccessor row) { if (row instanceof Entity entity) { return entity.id(); } @@ -93,7 +93,7 @@ public final class IdentitySupport { } @Nullable - public static String getPrefixedElementId(@NonNull MapAccessor queryResult, @Nullable String seed) { + public static String getPrefixedElementId(MapAccessor queryResult, @Nullable String seed) { if (queryResult instanceof Node) { return "N" + getElementId(queryResult); } else if (queryResult instanceof Relationship) { diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/MappingSupport.java b/src/main/java/org/springframework/data/neo4j/core/mapping/MappingSupport.java index c9e096968..5ff442d67 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/MappingSupport.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/MappingSupport.java @@ -27,13 +27,13 @@ import java.util.function.Predicate; import java.util.stream.Collectors; import org.apiguardian.api.API; +import org.jspecify.annotations.Nullable; import org.neo4j.driver.Value; import org.neo4j.driver.types.Node; import org.neo4j.driver.types.Relationship; import org.neo4j.driver.types.Type; import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.neo4j.core.schema.TargetNode; -import org.springframework.lang.Nullable; /** * @author Michael J. Simons @@ -174,10 +174,10 @@ public final class MappingSupport { Object relationshipPropertiesValue = entityHolder.getRelationshipProperties(); Neo4jPersistentEntity persistentEntity = - neo4jMappingContext.getPersistentEntity(relationshipPropertiesValue.getClass()); + Objects.requireNonNull(neo4jMappingContext.getPersistentEntity(relationshipPropertiesValue.getClass())); PersistentPropertyAccessor relationshipPropertiesAccessor = persistentEntity.getPropertyAccessor(relationshipPropertiesValue); - relationshipPropertiesAccessor.setProperty(persistentEntity.getPersistentProperty(TargetNode.class), newRelationshipObject); + relationshipPropertiesAccessor.setProperty(Objects.requireNonNull(persistentEntity.getPersistentProperty(TargetNode.class)), newRelationshipObject); newRelationshipObject = relationshipPropertiesAccessor.getBean(); // If we recreate or manipulate the object including it's accessor, we must update it in the holder as well. diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContext.java b/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContext.java index 1fa28bc07..a2afbc946 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContext.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContext.java @@ -31,12 +31,14 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; import org.apiguardian.api.API; +import org.jspecify.annotations.Nullable; import org.neo4j.cypherdsl.core.Statement; import org.neo4j.driver.types.TypeSystem; import org.springframework.beans.BeanUtils; @@ -67,7 +69,6 @@ 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; /** @@ -109,11 +110,12 @@ public final class Neo4jMappingContext extends AbstractMappingContext> postLoadMethods = new ConcurrentHashMap<>(); + private final Map, Set> postLoadMethods = new ConcurrentHashMap<>(); private EventSupport eventSupport; - private @Nullable AutowireCapableBeanFactory beanFactory; + @Nullable + private AutowireCapableBeanFactory beanFactory; private boolean strict = false; @@ -127,7 +129,6 @@ public final class Neo4jMappingContext extends AbstractMappingContext existingEntity = (Neo4jPersistentEntity) this.nodeDescriptionStore.get( primaryLabel); - if (!existingEntity.getTypeInformation().getRawTypeInformation() + if (existingEntity != null && !existingEntity.getTypeInformation().getRawTypeInformation() .equals(typeInformation.getRawTypeInformation())) { String message = String.format(Locale.ENGLISH, "The schema already contains a node description under the primary label %s", primaryLabel); @@ -318,7 +305,7 @@ public final class Neo4jMappingContext extends AbstractMappingContext parentClass) { + private static boolean isValidParentNode(Class parentClass) { if (parentClass == null || parentClass.equals(Object.class)) { return false; } @@ -355,6 +342,7 @@ public final class Neo4jMappingContext extends AbstractMappingContext getNodeDescription(Class underlyingClass) { return doGetPersistentEntity(underlyingClass); } @@ -418,7 +406,12 @@ public final class Neo4jMappingContext extends AbstractMappingContext this.beanFactory.createBean(t)); + idGenerator = this.beanFactory.getBeanProvider(t).getIfUnique(() -> { + // The beanFactory can't actually be reassigned, so doing a whole double lock check is a bit overkill + @SuppressWarnings("NullAway") + var result = this.beanFactory.createBean(t); + return result; + }); } return idGenerator; } @@ -510,10 +503,10 @@ public final class Neo4jMappingContext extends AbstractMappingContext typeVariableMap = GenericTypeResolver.getTypeVariableMap(converterClass) + Map typeVariableMap = (converterClass != null) ? GenericTypeResolver.getTypeVariableMap(converterClass) .entrySet() .stream() - .collect(Collectors.toMap(e -> e.getKey().getName(), Map.Entry::getValue)); + .collect(Collectors.toMap(e -> e.getKey().getName(), Map.Entry::getValue)) : Map.of(); Type propertyType = null; if (typeVariableMap.containsKey("T")) { propertyType = typeVariableMap.get("T"); diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jPersistentEntity.java b/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jPersistentEntity.java index 20ede2504..c16bb97d4 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jPersistentEntity.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jPersistentEntity.java @@ -19,6 +19,7 @@ import java.util.Optional; import java.util.Set; import org.apiguardian.api.API; +import org.jspecify.annotations.Nullable; import org.springframework.data.mapping.AssociationHandler; import org.springframework.data.mapping.PropertyHandler; import org.springframework.data.mapping.model.MutablePersistentEntity; @@ -76,8 +77,21 @@ public interface Neo4jPersistentEntity return isUsingInternalIds() && Neo4jPersistentEntity.DEPRECATED_GENERATED_ID_TYPES.contains(getRequiredIdProperty().getType()); } + /** + * @return true if this entity spots a vector property + */ boolean hasVectorProperty(); + /** + * Will return the single supported vector property if {@link #hasVectorProperty()} returns {@literal true}, otherwise {@literal null}. + * @return an optional vector property on this entity + */ + @Nullable Neo4jPersistentProperty getVectorProperty(); + + /** + * Will return the single supported vector property if {@link #hasVectorProperty()} returns {@literal true}, otherwise it will throw an {@link IllegalStateException}. + * @return the vector property on this entity. + */ Neo4jPersistentProperty getRequiredVectorProperty(); } diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jPersistentProperty.java b/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jPersistentProperty.java index ca2571d56..0b9fff21f 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jPersistentProperty.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jPersistentProperty.java @@ -18,12 +18,12 @@ package org.springframework.data.neo4j.core.mapping; import java.util.Optional; import org.apiguardian.api.API; +import org.jspecify.annotations.Nullable; import org.springframework.data.domain.Vector; import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.neo4j.core.convert.Neo4jPersistentPropertyConverter; import org.springframework.data.neo4j.core.schema.CompositeProperty; import org.springframework.data.neo4j.core.schema.DynamicLabels; -import org.springframework.lang.Nullable; /** * A {@link org.springframework.data.mapping.PersistentProperty} interface with additional methods for metadata related @@ -45,7 +45,8 @@ public interface Neo4jPersistentProperty extends PersistentProperty componentType = getComponentType(); + return isRelationship() && isMap() && (componentType == String.class || (componentType != null && componentType.isEnum())); } /** diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/NestedRelationshipContext.java b/src/main/java/org/springframework/data/neo4j/core/mapping/NestedRelationshipContext.java index dd4fc55ee..f8236af3d 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/NestedRelationshipContext.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/NestedRelationshipContext.java @@ -20,14 +20,16 @@ import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import org.apiguardian.api.API; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; import org.springframework.data.annotation.ReadOnlyProperty; import org.springframework.data.mapping.Association; import org.springframework.data.mapping.MappingException; import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.neo4j.core.schema.TargetNode; -import org.springframework.lang.Nullable; /** * Working on nested relationships happens in a certain algorithmic context. This context enables a tight cohesion @@ -42,6 +44,7 @@ import org.springframework.lang.Nullable; @API(status = API.Status.INTERNAL, since = "6.0") public final class NestedRelationshipContext { private final Neo4jPersistentProperty inverse; + @Nullable private final Object value; private final RelationshipDescription relationship; @@ -82,10 +85,9 @@ public final class NestedRelationshipContext { public Object identifyAndExtractRelationshipTargetNode(Object relatedValue) { Object valueToBeSaved = relatedValue; - if (relatedValue instanceof Map.Entry) { - Map.Entry relatedValueMapEntry = (Map.Entry) relatedValue; + if (relatedValue instanceof Map.Entry relatedValueMapEntry) { if (this.hasRelationshipWithProperties()) { - Object mapValue = ((Map.Entry) relatedValue).getValue(); + Object mapValue = relatedValueMapEntry.getValue(); // it can be either a scalar entity holder or a list of it mapValue = mapValue instanceof List ? ((List) mapValue).get(0) : mapValue; valueToBeSaved = ((MappingSupport.RelationshipPropertiesWithEntityHolder) mapValue).getRelatedEntity(); @@ -100,7 +102,8 @@ public final class NestedRelationshipContext { return valueToBeSaved; } - public @Nullable PersistentPropertyAccessor getRelationshipPropertiesPropertyAccessor(@Nullable Object relatedValue) { + @Nullable + public PersistentPropertyAccessor getRelationshipPropertiesPropertyAccessor(Object relatedValue) { if (!this.hasRelationshipWithProperties() || relatedValue == null) { return null; @@ -115,7 +118,7 @@ public final class NestedRelationshipContext { } } - public static NestedRelationshipContext of(Association handler, + public static NestedRelationshipContext of(Association<@NonNull Neo4jPersistentProperty> handler, PersistentPropertyAccessor propertyAccessor, Neo4jPersistentEntity neo4jPersistentEntity) { Neo4jPersistentProperty inverse = handler.getInverse(); @@ -130,7 +133,7 @@ public final class NestedRelationshipContext { neo4jPersistentEntity.getName() + " does not define a relationship for " + inverse.getFieldName())); if (relationship.hasRelationshipProperties() && value != null) { - Neo4jPersistentEntity relationshipPropertiesEntity = (Neo4jPersistentEntity) relationship.getRelationshipPropertiesEntity(); + Neo4jPersistentEntity relationshipPropertiesEntity = (Neo4jPersistentEntity) relationship.getRequiredRelationshipPropertiesEntity(); // If this is dynamic relationship (Map), extract the keys as relationship names // and the map values as values. @@ -187,7 +190,8 @@ public final class NestedRelationshipContext { private static Object getTargetNode(Neo4jPersistentEntity relationshipPropertiesEntity, Object object) { PersistentPropertyAccessor propertyAccessor = relationshipPropertiesEntity.getPropertyAccessor(object); - return propertyAccessor.getProperty(relationshipPropertiesEntity.getPersistentProperty(TargetNode.class)); + var targetNodeProperty = Objects.requireNonNull(relationshipPropertiesEntity.getPersistentProperty(TargetNode.class), () -> "Could not get target node property on %s".formatted(relationshipPropertiesEntity.getType())); + return Objects.requireNonNull(propertyAccessor.getProperty(targetNodeProperty)); } } diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/NestedRelationshipProcessingStateMachine.java b/src/main/java/org/springframework/data/neo4j/core/mapping/NestedRelationshipProcessingStateMachine.java index 41c4a8eba..73427b6db 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/NestedRelationshipProcessingStateMachine.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/NestedRelationshipProcessingStateMachine.java @@ -24,9 +24,9 @@ import java.util.Set; import java.util.concurrent.locks.StampedLock; import org.apiguardian.api.API; +import org.jspecify.annotations.Nullable; import org.neo4j.cypherdsl.core.Statement; -import org.springframework.lang.NonNull; -import org.springframework.lang.Nullable; +import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.util.Assert; import reactor.core.publisher.Flux; @@ -101,7 +101,7 @@ public final class NestedRelationshipProcessingStateMachine { * @param valuesToStore Check whether all the values in the collection have been processed * @return The state of things processed */ - public ProcessState getStateOf(@Nullable Object fromId, RelationshipDescription relationshipDescription, @Nullable Collection valuesToStore) { + public ProcessState getStateOf(@Nullable Object fromId, RelationshipDescription relationshipDescription, Collection valuesToStore) { if (fromId == null) { return ProcessState.PROCESSED_BOTH; } @@ -139,7 +139,7 @@ public final class NestedRelationshipProcessingStateMachine { private record RelationshipIdUpdateContext(Statement cypher, Object fromId, Object toId, NestedRelationshipContext relationshipContext, - Object relatedValueToStore, Neo4jPersistentProperty idProperty) { + Object relatedValueToStore, @Nullable Neo4jPersistentProperty idProperty) { } /** @@ -147,7 +147,7 @@ public final class NestedRelationshipProcessingStateMachine { */ @FunctionalInterface public interface RelationshipIdSupplier { - Optional getId(Statement statement, Neo4jPersistentProperty idProperty, Object fromId, Object toId); + Optional getId(Statement statement, @Nullable Neo4jPersistentProperty idProperty, Object fromId, Object toId); } /** @@ -155,7 +155,7 @@ public final class NestedRelationshipProcessingStateMachine { */ @FunctionalInterface public interface ReactiveRelationshipIdSupplier { - Mono getId(Statement statement, Neo4jPersistentProperty idProperty, Object fromId, Object toId); + Mono getId(Statement statement, @Nullable Neo4jPersistentProperty idProperty, Object fromId, Object toId); } /** @@ -163,8 +163,8 @@ public final class NestedRelationshipProcessingStateMachine { * * @param relationshipDescription To be marked as processed */ - public void markRelationshipAsProcessed(Object fromId, @Nullable RelationshipDescription relationshipDescription) { - if (relationshipDescription == null) { + public void markRelationshipAsProcessed(@Nullable Object fromId, @Nullable RelationshipDescription relationshipDescription) { + if (fromId == null || relationshipDescription == null) { return; } @@ -218,16 +218,20 @@ public final class NestedRelationshipProcessingStateMachine { if (!processed && mappingContext.hasPersistentEntityFor(typeOfValue)) { Neo4jPersistentEntity entity = mappingContext.getRequiredPersistentEntity(typeOfValue); Neo4jPersistentProperty idProperty = entity.getIdProperty(); - Object id = idProperty == null ? null : entity.getPropertyAccessor(valueToCheck).getProperty(idProperty); + Object id; + Optional alreadyProcessedObject = Optional.empty(); + if (idProperty != null) { + // After the lookup by system.identityHashCode failed for a processed object alias, + // we must traverse or iterate over all value with the matching type and compare the domain ids + // to figure out if the logical object has already been processed through a different object instance. + // The type check is needed to avoid relationship ids <> node id conflicts. + id = entity.getPropertyAccessor(valueToCheck).getProperty(idProperty); + alreadyProcessedObject = processedObjectsAlias.values().stream() + .filter(typeOfValue::isInstance) + .filter(processedObject -> id != null && id.equals(entity.getPropertyAccessor(processedObject).getProperty(idProperty))) + .findAny(); + } - // After the lookup by system.identityHashCode failed for a processed object alias, - // we must traverse or iterate over all value with the matching type and compare the domain ids - // to figure out if the logical object has already been processed through a different object instance. - // The type check is needed to avoid relationship ids <> node id conflicts. - Optional alreadyProcessedObject = id == null ? Optional.empty() : processedObjectsAlias.values().stream() - .filter(typeOfValue::isInstance) - .filter(processedObject -> id.equals(entity.getPropertyAccessor(processedObject).getProperty(idProperty))) - .findAny(); if (alreadyProcessedObject.isPresent()) { // Skip the show the next time around. processed = true; Object internalId = getObjectId(alreadyProcessedObject.get()); @@ -249,16 +253,17 @@ public final class NestedRelationshipProcessingStateMachine { * @param relationshipDescription the relationship that should be looked for in the registry. * @return processed yes (true) / no (false) */ - public boolean hasProcessedRelationship(Object fromId, @Nullable RelationshipDescription relationshipDescription) { - if (relationshipDescription != null) { - final long stamp = lock.readLock(); - try { - return processedRelationshipDescriptions.contains(new RelationshipDescriptionWithSourceId(fromId, relationshipDescription)); - } finally { - lock.unlock(stamp); - } + public boolean hasProcessedRelationship(@Nullable Object fromId, @Nullable RelationshipDescription relationshipDescription) { + if (fromId == null || relationshipDescription == null) { + return false; + } + + final long stamp = lock.readLock(); + try { + return processedRelationshipDescriptions.contains(new RelationshipDescriptionWithSourceId(fromId, relationshipDescription)); + } finally { + lock.unlock(stamp); } - return false; } public void storeProcessRelationshipEntity(MappingSupport.RelationshipPropertiesWithEntityHolder id, Object source, Object target, RelationshipDescription type) { @@ -284,7 +289,7 @@ public final class NestedRelationshipProcessingStateMachine { } public void requireIdUpdate(Neo4jPersistentEntity sourceEntity, RelationshipDescription relationshipDescription, boolean canUseElementId, - Object fromId, Object toId, NestedRelationshipContext relationshipContext, Object relatedValueToStore, Neo4jPersistentProperty idProperty) { + Object fromId, Object toId, NestedRelationshipContext relationshipContext, Object relatedValueToStore, @Nullable Neo4jPersistentProperty idProperty) { Statement relationshipCreationQuery = CypherGenerator.INSTANCE.prepareSaveOfRelationshipWithProperties( sourceEntity, relationshipDescription, false, @@ -297,24 +302,6 @@ public final class NestedRelationshipProcessingStateMachine { } } - public Collection getRequiresIdUpdate() { - final long stamp = lock.readLock(); - try { - return Set.copyOf(requiresIdUpdate); - } finally { - lock.unlock(stamp); - } - } - - public void markAsUpdated(RelationshipIdUpdateContext context) { - final long stamp = lock.writeLock(); - try { - requiresIdUpdate.remove(context); - } finally { - lock.unlock(stamp); - } - } - public void updateRelationshipIds(RelationshipIdSupplier idSupplier) { final long stamp = lock.writeLock(); try { @@ -322,10 +309,12 @@ public final class NestedRelationshipProcessingStateMachine { while (it.hasNext()) { var requiredIdUpdate = it.next(); idSupplier.getId(requiredIdUpdate.cypher(), requiredIdUpdate.idProperty(), requiredIdUpdate.fromId(), requiredIdUpdate.toId()).ifPresent(anId -> { - requiredIdUpdate.relationshipContext() - .getRelationshipPropertiesPropertyAccessor(requiredIdUpdate.relatedValueToStore()) - .setProperty(requiredIdUpdate.idProperty(), anId); - it.remove(); + PersistentPropertyAccessor relationshipPropertiesPropertyAccessor = requiredIdUpdate.relationshipContext() + .getRelationshipPropertiesPropertyAccessor(requiredIdUpdate.relatedValueToStore()); + if (relationshipPropertiesPropertyAccessor != null && requiredIdUpdate.idProperty() != null) { + relationshipPropertiesPropertyAccessor.setProperty(requiredIdUpdate.idProperty(), anId); + it.remove(); + } }); } } finally { @@ -333,17 +322,19 @@ public final class NestedRelationshipProcessingStateMachine { } } - public Mono updateRelationshipIds(ReactiveRelationshipIdSupplier idSupplier) { + public Mono updateRelationshipIdsReactive(ReactiveRelationshipIdSupplier idSupplier) { return Flux.defer(() -> { final long stamp = lock.writeLock(); return Flux.fromIterable(requiresIdUpdate) .flatMap(requiredIdUpdate -> Mono.just(requiredIdUpdate).zipWith(idSupplier.getId(requiredIdUpdate.cypher(), requiredIdUpdate.idProperty(), requiredIdUpdate.fromId(), requiredIdUpdate.toId()))) .doOnNext(t -> { var requiredIdUpdate = t.getT1(); - requiredIdUpdate.relationshipContext() - .getRelationshipPropertiesPropertyAccessor(requiredIdUpdate.relatedValueToStore()) - .setProperty(requiredIdUpdate.idProperty(), t.getT2()); - requiresIdUpdate.remove(requiredIdUpdate); + PersistentPropertyAccessor relationshipPropertiesPropertyAccessor = requiredIdUpdate.relationshipContext() + .getRelationshipPropertiesPropertyAccessor(requiredIdUpdate.relatedValueToStore()); + if (relationshipPropertiesPropertyAccessor != null && requiredIdUpdate.idProperty() != null) { + relationshipPropertiesPropertyAccessor.setProperty(requiredIdUpdate.idProperty(), t.getT2()); + requiresIdUpdate.remove(requiredIdUpdate); + } }).doOnTerminate(() -> lock.unlock(stamp)); }).then(); } @@ -387,14 +378,13 @@ public final class NestedRelationshipProcessingStateMachine { } @Nullable - private Object getProcessedObjectIds(@Nullable Object entity) { + private Object getProcessedObjectIds(Object entity) { if (entity == null) { return null; } return processedObjectsIds.get(System.identityHashCode(entity)); } - @NonNull private Object extractRelatedValueFromRelationshipProperties(Object valueToStore) { Object value; if (valueToStore instanceof MappingSupport.RelationshipPropertiesWithEntityHolder) { @@ -424,8 +414,9 @@ public final class NestedRelationshipProcessingStateMachine { return processedObjectsAlias.containsKey(System.identityHashCode(entity)); } - private boolean hasProcessedAllOf(@Nullable Collection entities) { + private boolean hasProcessedAllOf(Collection entities) { // there can be null elements in the unified collection of values to store. + //noinspection ConstantValue if (entities == null) { return false; } diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/NoRootNodeMappingException.java b/src/main/java/org/springframework/data/neo4j/core/mapping/NoRootNodeMappingException.java index 17ca60f17..efa6dbde5 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/NoRootNodeMappingException.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/NoRootNodeMappingException.java @@ -15,11 +15,13 @@ */ package org.springframework.data.neo4j.core.mapping; +import java.io.Serial; import java.util.Formattable; import java.util.Formatter; import java.util.Locale; import org.apiguardian.api.API; +import org.jspecify.annotations.Nullable; import org.neo4j.driver.types.MapAccessor; import org.springframework.data.mapping.MappingException; @@ -36,9 +38,15 @@ import org.springframework.data.mapping.MappingException; @API(status = API.Status.INTERNAL, since = "6.0.2") public final class NoRootNodeMappingException extends MappingException implements Formattable { - private MapAccessor mapAccessor; - private Neo4jPersistentEntity entity; + @Serial + private static final long serialVersionUID = 5742846435191601546L; + @Nullable + private final transient MapAccessor mapAccessor; + @Nullable + private final transient Neo4jPersistentEntity entity; + + @SuppressWarnings("NullableProblems") public NoRootNodeMappingException(MapAccessor mapAccessor, Neo4jPersistentEntity entity) { super(String.format("Could not find mappable nodes or relationships inside %s for %s", mapAccessor, entity)); this.mapAccessor = mapAccessor; @@ -47,9 +55,13 @@ public final class NoRootNodeMappingException extends MappingException implement @Override public void formatTo(Formatter formatter, int flags, int width, int precision) { - String className = entity.getUnderlyingClass().getSimpleName(); - formatter.format("Could not find mappable nodes or relationships inside %s for %s:%s", mapAccessor, - className.substring(0, 1).toLowerCase( - Locale.ROOT), String.join(":", entity.getStaticLabels())); + if (mapAccessor != null && entity != null) { + String className = entity.getUnderlyingClass().getSimpleName(); + formatter.format("Could not find mappable nodes or relationships inside %s for %s:%s", mapAccessor, + className.substring(0, 1).toLowerCase( + Locale.ROOT), String.join(":", entity.getStaticLabels())); + } else { + formatter.format("Could not find mappable nodes or relationships inside a record"); + } } } diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/NodeDescription.java b/src/main/java/org/springframework/data/neo4j/core/mapping/NodeDescription.java index 6ab63eae8..9cbb18f9d 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/NodeDescription.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/NodeDescription.java @@ -18,12 +18,13 @@ package org.springframework.data.neo4j.core.mapping; import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.function.Predicate; import org.apiguardian.api.API; +import org.jspecify.annotations.Nullable; import org.neo4j.cypherdsl.core.Expression; -import org.springframework.lang.Nullable; /** * Describes how a class is mapped to a node inside the database. It provides navigable links to relationships and @@ -143,14 +144,15 @@ public interface NodeDescription { */ default Expression getIdExpression() { - if (this.getIdDescription().getOptionalGraphPropertyName() + var idDescription = Objects.requireNonNull(this.getIdDescription(), "No id description available, cannot compute a Cypher expression for retrieving or storing the id"); + if (idDescription.getOptionalGraphPropertyName() .flatMap(this::getGraphProperty) .filter(GraphPropertyDescription::isComposite) .isPresent()) { throw new IllegalStateException("A composite id property cannot be used as ID expression."); } - return this.getIdDescription().asIdExpression(); + return idDescription.asIdExpression(); } /** diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/NodeDescriptionStore.java b/src/main/java/org/springframework/data/neo4j/core/mapping/NodeDescriptionStore.java index 55c81d6e0..b847163c4 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/NodeDescriptionStore.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/NodeDescriptionStore.java @@ -23,12 +23,13 @@ import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.function.BiFunction; +import org.jspecify.annotations.Nullable; import org.springframework.data.mapping.context.AbstractMappingContext; -import org.springframework.lang.Nullable; /** * This class is more or less just a wrapper around the node description lookup map. It ensures that there is no cyclic @@ -102,7 +103,7 @@ final class NodeDescriptionStore { return nodeDescriptionAndLabels.apply(entityDescription, labels); } - private NodeDescriptionAndLabels computeConcreteNodeDescription(NodeDescription entityDescription, @Nullable List labels) { + private NodeDescriptionAndLabels computeConcreteNodeDescription(NodeDescription entityDescription, List labels) { boolean isConcreteClassThatFulfillsEverything = !Modifier.isAbstract(entityDescription.getUnderlyingClass().getModifiers()) && entityDescription.getStaticLabels().containsAll(labels); @@ -148,14 +149,19 @@ final class NodeDescriptionStore { } unmatchedLabelsCache.put(nd, unmatchedLabelsCount); - if (mostMatchingNodeDescription == null || unmatchedLabelsCount < unmatchedLabelsCache.get(mostMatchingNodeDescription)) { + if (mostMatchingNodeDescription == null || unmatchedLabelsCount < Objects.requireNonNullElse(unmatchedLabelsCache.get(mostMatchingNodeDescription), Integer.MAX_VALUE)) { mostMatchingNodeDescription = nd; mostMatchingStaticLabels = matchingLabels; } } Set surplusLabels = new HashSet<>(labels); - mostMatchingStaticLabels.forEach(surplusLabels::remove); + if (mostMatchingStaticLabels != null) { + mostMatchingStaticLabels.forEach(surplusLabels::remove); + } + if (mostMatchingNodeDescription == null) { + throw new IllegalStateException("Could not compute a concrete node description for entity %s and labels %s".formatted(entityDescription, labels)); + } return new NodeDescriptionAndLabels(mostMatchingNodeDescription, surplusLabels); } diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/NullSafeNeo4jPersistentPropertyConverter.java b/src/main/java/org/springframework/data/neo4j/core/mapping/NullSafeNeo4jPersistentPropertyConverter.java index 862c110fc..8ad11287a 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/NullSafeNeo4jPersistentPropertyConverter.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/NullSafeNeo4jPersistentPropertyConverter.java @@ -15,10 +15,10 @@ */ package org.springframework.data.neo4j.core.mapping; +import org.jspecify.annotations.Nullable; import org.neo4j.driver.Value; import org.neo4j.driver.Values; import org.springframework.data.neo4j.core.convert.Neo4jPersistentPropertyConverter; -import org.springframework.lang.Nullable; /** * All property converters will be wrapped by this class. It adds the information if a converter needs to be applied to @@ -60,7 +60,8 @@ final class NullSafeNeo4jPersistentPropertyConverter implements Neo4jPersiste return delegate.write(source); } - @Override @Nullable + @Override + @Nullable public T read(@Nullable Value source) { return source == null || source.isNull() ? null : delegate.read(source); } diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/PersistentPropertyCharacteristics.java b/src/main/java/org/springframework/data/neo4j/core/mapping/PersistentPropertyCharacteristics.java index 77862d047..c49dbb45f 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/PersistentPropertyCharacteristics.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/PersistentPropertyCharacteristics.java @@ -18,7 +18,7 @@ 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; +import org.jspecify.annotations.Nullable; /** * The characteristics of a {@link Neo4jPersistentProperty} can diverge from what is by default derived from the annotated diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/PropertyFilter.java b/src/main/java/org/springframework/data/neo4j/core/mapping/PropertyFilter.java index 94eee16b7..ae34227de 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/PropertyFilter.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/PropertyFilter.java @@ -22,8 +22,8 @@ import java.util.Set; import java.util.function.Predicate; import org.apiguardian.api.API; +import org.jspecify.annotations.Nullable; import org.springframework.data.neo4j.core.schema.Property; -import org.springframework.lang.Nullable; import org.springframework.util.StringUtils; /** @@ -48,7 +48,7 @@ public abstract class PropertyFilter { public abstract boolean isNotFiltering(); - static String toDotPath(RelaxedPropertyPath propertyPath, @Nullable String lastSegment) { + static String toDotPath(RelaxedPropertyPath propertyPath, String lastSegment) { if (lastSegment == null) { return propertyPath.toDotPath(); @@ -167,7 +167,7 @@ public abstract class PropertyFilter { return dotPath; } - public String toDotPath(@Nullable String lastSegment) { + public String toDotPath(String lastSegment) { if (lastSegment == null) { return this.toDotPath(); diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/PropertyTraverser.java b/src/main/java/org/springframework/data/neo4j/core/mapping/PropertyTraverser.java index 608bb8697..15db276d9 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/PropertyTraverser.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/PropertyTraverser.java @@ -23,10 +23,10 @@ import java.util.function.BiConsumer; import java.util.function.BiPredicate; import org.apiguardian.api.API; +import org.jspecify.annotations.Nullable; import org.springframework.data.mapping.Association; import org.springframework.data.mapping.PropertyPath; import org.springframework.data.neo4j.core.schema.TargetNode; -import org.springframework.lang.Nullable; /** * A strategy for traversing all properties (including association) once, without going in circles with cyclic mappings. diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/RelationshipDescription.java b/src/main/java/org/springframework/data/neo4j/core/mapping/RelationshipDescription.java index b1670cffd..9d2b6728c 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/RelationshipDescription.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/RelationshipDescription.java @@ -15,12 +15,12 @@ */ package org.springframework.data.neo4j.core.mapping; +import java.util.Objects; import java.util.Optional; import org.apiguardian.api.API; +import org.jspecify.annotations.Nullable; import org.springframework.data.neo4j.core.schema.Relationship; -import org.springframework.lang.NonNull; -import org.springframework.lang.Nullable; /** * Description of a relationship. Those descriptions always describe outgoing relationships. The inverse direction is @@ -92,6 +92,11 @@ public interface RelationshipDescription { @Nullable NodeDescription getRelationshipPropertiesEntity(); + default NodeDescription getRequiredRelationshipPropertiesEntity() { + + return Objects.requireNonNull(getRelationshipPropertiesEntity(), () -> "Relationship entity %s does not point to an entity holding the relationships' properties".formatted(this.getType())); + } + /** * Tells if this relationship is a relationship with additional properties. In such cases * {@code getRelationshipPropertiesClass} will return the type of the properties holding class. @@ -102,7 +107,7 @@ public interface RelationshipDescription { default boolean hasInternalIdProperty() { - return hasRelationshipProperties() && Optional.ofNullable(getRelationshipPropertiesEntity().getIdDescription()) + return hasRelationshipProperties() && Optional.ofNullable(getRelationshipPropertiesEntity()).map(NodeDescription::getIdDescription) .filter(IdDescription::isInternallyGeneratedId).isPresent(); } @@ -114,7 +119,6 @@ public interface RelationshipDescription { return Relationship.Direction.INCOMING.equals(this.getDirection()); } - @NonNull default String generateRelatedNodesCollectionName(NodeDescription mostAbstractNodeDescription) { return this.getSource().getMostAbstractParentLabel(mostAbstractNodeDescription) + "_" + this.getType() + "_" + this.getTarget().getPrimaryLabel() + "_" + this.isOutgoing(); @@ -125,11 +129,12 @@ public interface RelationshipDescription { * * @param relationshipObverse logically same relationship definition in the target entity */ - void setRelationshipObverse(RelationshipDescription relationshipObverse); + void setRelationshipObverse(@Nullable RelationshipDescription relationshipObverse); /** * @return logically same relationship definition in the target entity */ + @Nullable RelationshipDescription getRelationshipObverse(); /** diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/Schema.java b/src/main/java/org/springframework/data/neo4j/core/mapping/Schema.java index 8399959bb..86684805e 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/Schema.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/Schema.java @@ -22,11 +22,11 @@ import java.util.function.BiFunction; import java.util.function.Function; import org.apiguardian.api.API; +import org.jspecify.annotations.Nullable; import org.neo4j.driver.types.MapAccessor; import org.neo4j.driver.types.TypeSystem; import org.springframework.data.mapping.MappingException; import org.springframework.data.neo4j.core.schema.IdGenerator; -import org.springframework.lang.Nullable; /** * Contains the descriptions of all nodes, their properties and relationships known to SDN. @@ -93,7 +93,13 @@ public interface Schema { throw new UnknownEntityException(targetClass); } Neo4jEntityConverter entityConverter = getEntityConverter(); - return (typeSystem, record) -> entityConverter.read(targetClass, record); + return (typeSystem, record) -> { + try { + return entityConverter.read(targetClass, record); + } catch (IllegalStateException ex) { + return null; + } + }; } /** diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/UnknownEntityException.java b/src/main/java/org/springframework/data/neo4j/core/mapping/UnknownEntityException.java index 02d1536dc..a41857a24 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/UnknownEntityException.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/UnknownEntityException.java @@ -15,6 +15,8 @@ */ package org.springframework.data.neo4j.core.mapping; +import java.io.Serial; + import org.apiguardian.api.API; import org.springframework.dao.InvalidDataAccessApiUsageException; @@ -28,6 +30,8 @@ import org.springframework.dao.InvalidDataAccessApiUsageException; @API(status = API.Status.STABLE, since = "6.0") public final class UnknownEntityException extends InvalidDataAccessApiUsageException { + @Serial + private static final long serialVersionUID = -1769937352513022599L; private final Class targetClass; public UnknownEntityException(Class targetClass) { diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/callback/EventSupport.java b/src/main/java/org/springframework/data/neo4j/core/mapping/callback/EventSupport.java index 151c4e0bc..07ac938c7 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/callback/EventSupport.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/callback/EventSupport.java @@ -23,7 +23,6 @@ import org.springframework.beans.factory.BeanFactory; import org.springframework.data.mapping.callback.EntityCallbacks; import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity; -import org.springframework.lang.Nullable; /** * Utility class that orchestrates {@link EntityCallbacks}. Not to be used outside the framework. @@ -74,8 +73,7 @@ public final class EventSupport { this.entityCallbacks = entityCallbacks; } - @Nullable - public T maybeCallBeforeBind(@Nullable T object) { + public T maybeCallBeforeBind(T object) { if (object == null) { return object; @@ -90,8 +88,7 @@ public final class EventSupport { * @param Expected type * @return The instance to which the callback was applied to */ - @Nullable - public T maybeCallAfterConvert(@Nullable T object, Neo4jPersistentEntity entity, MapAccessor source) { + public T maybeCallAfterConvert(T object, Neo4jPersistentEntity entity, MapAccessor source) { if (object == null) { return object; diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/callback/package-info.java b/src/main/java/org/springframework/data/neo4j/core/mapping/callback/package-info.java index 4269926e3..1d787963f 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/callback/package-info.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/callback/package-info.java @@ -15,7 +15,4 @@ * * @author Michael J. Simons */ -@NonNullApi package org.springframework.data.neo4j.core.mapping.callback; - -import org.springframework.lang.NonNullApi; diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/package-info.java b/src/main/java/org/springframework/data/neo4j/core/mapping/package-info.java index ac8083eda..9835da9ce 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/package-info.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/package-info.java @@ -2,11 +2,11 @@ * The main mapping framework. This package orchestrates the reading and writing of entities and all tasks related to it. The only public API of this package is the subpackage {@literal callback}, containing the event support. - The core package itself has to be considered an internal api and we don't give any guarantees of API stability. + The core package itself has to be considered an internal api, and we don't give any guarantees of API stability. * * @author Michael J. Simons */ -@NonNullApi +@NullMarked package org.springframework.data.neo4j.core.mapping; -import org.springframework.lang.NonNullApi; +import org.jspecify.annotations.NullMarked; diff --git a/src/main/java/org/springframework/data/neo4j/core/package-info.java b/src/main/java/org/springframework/data/neo4j/core/package-info.java index 5264cec89..09f7b4887 100644 --- a/src/main/java/org/springframework/data/neo4j/core/package-info.java +++ b/src/main/java/org/springframework/data/neo4j/core/package-info.java @@ -5,7 +5,7 @@ the imperative and reactive variants of the client and the template. * */ -@NonNullApi +@NullMarked package org.springframework.data.neo4j.core; -import org.springframework.lang.NonNullApi; +import org.jspecify.annotations.NullMarked; diff --git a/src/main/java/org/springframework/data/neo4j/core/schema/CompositeProperty.java b/src/main/java/org/springframework/data/neo4j/core/schema/CompositeProperty.java index b963e7a5a..cde5bec5e 100644 --- a/src/main/java/org/springframework/data/neo4j/core/schema/CompositeProperty.java +++ b/src/main/java/org/springframework/data/neo4j/core/schema/CompositeProperty.java @@ -25,14 +25,18 @@ import java.lang.reflect.Type; import java.util.Collections; import java.util.HashMap; import java.util.Map; +import java.util.Objects; +import java.util.Optional; import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.UnaryOperator; import java.util.stream.Collectors; import org.apiguardian.api.API; +import org.jspecify.annotations.Nullable; import org.neo4j.driver.Value; import org.neo4j.driver.Values; +import org.neo4j.driver.types.TypeSystem; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.BeanFactory; import org.springframework.core.GenericTypeResolver; @@ -45,7 +49,6 @@ import org.springframework.data.neo4j.core.convert.Neo4jPersistentPropertyToMapC import org.springframework.data.neo4j.core.mapping.Neo4jPersistentProperty; import org.springframework.data.neo4j.core.schema.CompositeProperty.Phase; import org.springframework.data.util.TypeInformation; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -62,7 +65,7 @@ import org.springframework.util.StringUtils; * @since 6.0 */ @Retention(RetentionPolicy.RUNTIME) -@Target({ ElementType.FIELD }) +@Target({ElementType.FIELD}) @Inherited @ConvertWith(converterFactory = CompositePropertyConverterFactory.class) @API(status = API.Status.STABLE, since = "6.0") @@ -207,7 +210,7 @@ final class CompositePropertyConverter implements Neo4jPersistentPropertyC } @Override - public Value write(P property) { + public Value write(@Nullable P property) { Map source = delegate.decompose(property, neo4jConversionService); Map temp = new HashMap<>(); @@ -216,7 +219,12 @@ final class CompositePropertyConverter implements Neo4jPersistentPropertyC } @Override - public P read(Value source) { + @Nullable + public P read(@Nullable Value source) { + + if (source == null || TypeSystem.getDefault().NULL().isTypeOf(source)) { + return null; + } Map temp = new HashMap<>(); source.keys().forEach(k -> { @@ -230,6 +238,7 @@ final class CompositePropertyConverter implements Neo4jPersistentPropertyC /** * Internally used via reflection. + * * @return The type of the underlying delegate. */ @SuppressWarnings("unused") @@ -249,7 +258,7 @@ final class CompositePropertyConverterFactory implements Neo4jPersistentProperty private final BeanFactory beanFactory; private final Neo4jConversionService conversionServiceDelegate; - CompositePropertyConverterFactory(@Nullable BeanFactory beanFactory, @Nullable Neo4jConversionService conversionServiceDelegate) { + CompositePropertyConverterFactory(BeanFactory beanFactory, Neo4jConversionService conversionServiceDelegate) { this.beanFactory = beanFactory; this.conversionServiceDelegate = conversionServiceDelegate; } @@ -306,15 +315,16 @@ final class CompositePropertyConverterFactory implements Neo4jPersistentProperty } if (persistentProperty.getActualType() != type) { + var typeName = Optional.ofNullable(type).map(Type::getTypeName).orElse("n/a"); throw new IllegalArgumentException( - "The property type `" + typeVariableMap.get(PROPERTY_TYPE_KEY).getTypeName() + "` created by `" + "The property type `" + typeName + "` created by `" + delegateClass.getName() + "` " + generateLocation(persistentProperty) + " doesn't match the actual property type"); } componentType = (Class) typeVariableMap.get(KEY_TYPE_KEY); } - boolean isEnum = componentType.isEnum(); + boolean isEnum = componentType != null && componentType.isEnum(); if (!(componentType == String.class || isEnum)) { throw new IllegalArgumentException("@" + CompositeProperty.class.getSimpleName() + " can only be used on Map properties with a key type of String or enum. Was " + generateLocation( @@ -343,7 +353,7 @@ final class CompositePropertyConverterFactory implements Neo4jPersistentProperty String prefixWithDelimiter = persistentProperty.computePrefixWithDelimiter(); return new CompositePropertyConverter( - delegate, prefixWithDelimiter, conversionServiceDelegate, componentType, keyWriter, keyReader); + delegate, prefixWithDelimiter, conversionServiceDelegate, Objects.requireNonNull(componentType), keyWriter, keyReader); } private static String generateLocation(Neo4jPersistentProperty persistentProperty) { diff --git a/src/main/java/org/springframework/data/neo4j/core/schema/package-info.java b/src/main/java/org/springframework/data/neo4j/core/schema/package-info.java index 3a10722a2..49d5002cc 100644 --- a/src/main/java/org/springframework/data/neo4j/core/schema/package-info.java +++ b/src/main/java/org/springframework/data/neo4j/core/schema/package-info.java @@ -4,7 +4,7 @@ * * @author Michael J. Simons */ -@NonNullApi +@NullMarked package org.springframework.data.neo4j.core.schema; -import org.springframework.lang.NonNullApi; +import org.jspecify.annotations.NullMarked; diff --git a/src/main/java/org/springframework/data/neo4j/core/support/BookmarkManagerReference.java b/src/main/java/org/springframework/data/neo4j/core/support/BookmarkManagerReference.java index a25adf8a1..bab837461 100644 --- a/src/main/java/org/springframework/data/neo4j/core/support/BookmarkManagerReference.java +++ b/src/main/java/org/springframework/data/neo4j/core/support/BookmarkManagerReference.java @@ -15,8 +15,10 @@ */ package org.springframework.data.neo4j.core.support; +import java.util.Objects; import java.util.function.Supplier; +import org.jspecify.annotations.Nullable; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanCreationException; import org.springframework.beans.factory.ObjectProvider; @@ -24,7 +26,6 @@ import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.context.ApplicationEventPublisher; import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager; -import org.springframework.lang.Nullable; /** * Don't use outside SDN code. You have been warned. @@ -35,18 +36,20 @@ public final class BookmarkManagerReference implements ApplicationContextAware { private final Supplier defaultBookmarkManagerSupplier; - private ObjectProvider neo4jBookmarkManagers = new ObjectProvider() { + private ObjectProvider neo4jBookmarkManagers = new ObjectProvider<>() { @Override public Neo4jBookmarkManager getObject(Object... args) throws BeansException { throw new BeanCreationException("This provider can't create new beans"); } @Override + @Nullable public Neo4jBookmarkManager getIfAvailable() throws BeansException { return null; } @Override + @Nullable public Neo4jBookmarkManager getIfUnique() throws BeansException { return null; } @@ -60,6 +63,7 @@ public final class BookmarkManagerReference implements ApplicationContextAware { @Nullable private volatile Neo4jBookmarkManager bookmarkManager; + @Nullable private ApplicationEventPublisher applicationEventPublisher; public BookmarkManagerReference(Supplier defaultBookmarkManagerSupplier, @Nullable Neo4jBookmarkManager bookmarkManager) { @@ -73,7 +77,8 @@ public final class BookmarkManagerReference implements ApplicationContextAware { this.neo4jBookmarkManagers = applicationContext.getBeanProvider(Neo4jBookmarkManager.class); this.applicationEventPublisher = applicationContext; if (this.bookmarkManager != null) { - this.bookmarkManager.setApplicationEventPublisher(this.applicationEventPublisher); + Objects.requireNonNull(this.bookmarkManager) + .setApplicationEventPublisher(this.applicationEventPublisher); } } @@ -84,6 +89,7 @@ public final class BookmarkManagerReference implements ApplicationContextAware { result = this.bookmarkManager; if (result == null) { this.bookmarkManager = neo4jBookmarkManagers.getIfAvailable(this.defaultBookmarkManagerSupplier); + //noinspection DataFlowIssue this.bookmarkManager.setApplicationEventPublisher(this.applicationEventPublisher); result = this.bookmarkManager; } diff --git a/src/main/java/org/springframework/data/neo4j/core/support/DateLong.java b/src/main/java/org/springframework/data/neo4j/core/support/DateLong.java index 09196f784..26ed2b660 100644 --- a/src/main/java/org/springframework/data/neo4j/core/support/DateLong.java +++ b/src/main/java/org/springframework/data/neo4j/core/support/DateLong.java @@ -23,8 +23,10 @@ import java.lang.annotation.Target; import java.util.Date; import org.apiguardian.api.API; +import org.jspecify.annotations.Nullable; import org.neo4j.driver.Value; import org.neo4j.driver.Values; +import org.neo4j.driver.types.TypeSystem; import org.springframework.data.neo4j.core.convert.ConvertWith; import org.springframework.data.neo4j.core.convert.Neo4jPersistentPropertyConverter; @@ -47,12 +49,13 @@ public @interface DateLong { final class DateLongConverter implements Neo4jPersistentPropertyConverter { @Override - public Value write(Date source) { - return Values.value(source.getTime()); + public Value write(@Nullable Date source) { + return source == null ? Values.NULL : Values.value(source.getTime()); } @Override - public Date read(Value source) { - return new Date(source.asLong()); + @Nullable + public Date read(@Nullable Value source) { + return source == null || TypeSystem.getDefault().NULL().isTypeOf(source) ? null : new Date(source.asLong()); } } diff --git a/src/main/java/org/springframework/data/neo4j/core/support/DateString.java b/src/main/java/org/springframework/data/neo4j/core/support/DateString.java index 97a188926..a578a946f 100644 --- a/src/main/java/org/springframework/data/neo4j/core/support/DateString.java +++ b/src/main/java/org/springframework/data/neo4j/core/support/DateString.java @@ -26,8 +26,10 @@ import java.util.Date; import java.util.TimeZone; import org.apiguardian.api.API; +import org.jspecify.annotations.Nullable; import org.neo4j.driver.Value; import org.neo4j.driver.Values; +import org.neo4j.driver.types.TypeSystem; import org.springframework.core.annotation.AliasFor; import org.springframework.data.neo4j.core.convert.ConvertWith; import org.springframework.data.neo4j.core.convert.Neo4jPersistentPropertyConverterFactory; @@ -100,14 +102,15 @@ final class DateStringConverter implements Neo4jPersistentPropertyConverter */ -@NonNullApi +@NullMarked package org.springframework.data.neo4j.core.support; -import org.springframework.lang.NonNullApi; +import org.jspecify.annotations.NullMarked; diff --git a/src/main/java/org/springframework/data/neo4j/core/transaction/DefaultBookmarkManager.java b/src/main/java/org/springframework/data/neo4j/core/transaction/DefaultBookmarkManager.java index 5bd8d4fe6..a740152e7 100644 --- a/src/main/java/org/springframework/data/neo4j/core/transaction/DefaultBookmarkManager.java +++ b/src/main/java/org/springframework/data/neo4j/core/transaction/DefaultBookmarkManager.java @@ -24,9 +24,9 @@ import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.function.Supplier; +import org.jspecify.annotations.Nullable; import org.neo4j.driver.Bookmark; import org.springframework.context.ApplicationEventPublisher; -import org.springframework.lang.Nullable; /** * Default bookmark manager. diff --git a/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jBookmarkManager.java b/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jBookmarkManager.java index 4041a3c70..92203cd3e 100644 --- a/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jBookmarkManager.java +++ b/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jBookmarkManager.java @@ -20,9 +20,9 @@ import java.util.Set; import java.util.function.Supplier; import org.apiguardian.api.API; +import org.jspecify.annotations.Nullable; import org.neo4j.driver.Bookmark; import org.springframework.context.ApplicationEventPublisher; -import org.springframework.lang.Nullable; /** * Responsible for storing, updating and retrieving the bookmarks of Neo4j's transaction. @@ -58,7 +58,7 @@ public sealed interface Neo4jBookmarkManager permits AbstractBookmarkManager, No * bookmarks on each call. * @return A bookmark manager */ - static Neo4jBookmarkManager create(@Nullable Supplier> bookmarksSupplier) { + static Neo4jBookmarkManager create(Supplier> bookmarksSupplier) { return new DefaultBookmarkManager(bookmarksSupplier); } @@ -72,7 +72,7 @@ public sealed interface Neo4jBookmarkManager permits AbstractBookmarkManager, No * bookmarks on each call. * @return A reactive bookmark manager */ - static Neo4jBookmarkManager createReactive(@Nullable Supplier> bookmarksSupplier) { + static Neo4jBookmarkManager createReactive(Supplier> bookmarksSupplier) { return new ReactiveDefaultBookmarkManager(bookmarksSupplier); } diff --git a/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jBookmarksUpdatedEvent.java b/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jBookmarksUpdatedEvent.java index 4fd54c489..43264836c 100644 --- a/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jBookmarksUpdatedEvent.java +++ b/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jBookmarksUpdatedEvent.java @@ -15,10 +15,12 @@ */ package org.springframework.data.neo4j.core.transaction; +import java.io.Serial; import java.util.Collections; import java.util.Set; import org.apiguardian.api.API; +import org.jspecify.annotations.Nullable; import org.neo4j.driver.Bookmark; import org.springframework.context.ApplicationEvent; @@ -33,7 +35,10 @@ import org.springframework.context.ApplicationEvent; @API(status = API.Status.STABLE, since = "6.1.1") public final class Neo4jBookmarksUpdatedEvent extends ApplicationEvent { - private final Set bookmarks; + @Serial + private static final long serialVersionUID = 2143476552056698819L; + @Nullable + private transient final Set bookmarks; Neo4jBookmarksUpdatedEvent(Set bookmarks) { super(bookmarks); @@ -45,6 +50,6 @@ public final class Neo4jBookmarksUpdatedEvent extends ApplicationEvent { */ public Set getBookmarks() { - return Collections.unmodifiableSet(this.bookmarks); + return this.bookmarks == null ? Set.of() : Collections.unmodifiableSet(this.bookmarks); } } diff --git a/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionHolder.java b/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionHolder.java index 7d4cac155..c79e16a1f 100644 --- a/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionHolder.java +++ b/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionHolder.java @@ -17,13 +17,13 @@ package org.springframework.data.neo4j.core.transaction; import java.util.Collection; +import org.jspecify.annotations.Nullable; import org.neo4j.driver.Bookmark; import org.neo4j.driver.Session; import org.neo4j.driver.Transaction; import org.springframework.data.neo4j.core.DatabaseSelection; import org.springframework.data.neo4j.core.UserSelection; import org.springframework.data.neo4j.core.support.RetryExceptionPredicate; -import org.springframework.lang.Nullable; import org.springframework.transaction.support.ResourceHolderSupport; import org.springframework.util.Assert; diff --git a/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionManager.java b/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionManager.java index f8badf150..28367c4d4 100644 --- a/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionManager.java +++ b/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionManager.java @@ -15,9 +15,12 @@ */ package org.springframework.data.neo4j.core.transaction; +import java.io.Serial; import java.util.Collection; +import java.util.Objects; import org.apiguardian.api.API; +import org.jspecify.annotations.Nullable; import org.neo4j.driver.Bookmark; import org.neo4j.driver.Driver; import org.neo4j.driver.Session; @@ -33,7 +36,6 @@ import org.springframework.data.neo4j.core.DatabaseSelectionProvider; import org.springframework.data.neo4j.core.UserSelection; import org.springframework.data.neo4j.core.UserSelectionProvider; import org.springframework.data.neo4j.core.support.BookmarkManagerReference; -import org.springframework.lang.Nullable; import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.TransactionException; import org.springframework.transaction.TransactionSystemException; @@ -43,7 +45,6 @@ import org.springframework.transaction.support.SmartTransactionObject; import org.springframework.transaction.support.TransactionSynchronizationManager; import org.springframework.transaction.support.TransactionSynchronizationUtils; import org.springframework.util.Assert; -import org.springframework.util.StringUtils; /** * Dedicated {@link org.springframework.transaction.PlatformTransactionManager} for native Neo4j transactions. This @@ -55,6 +56,9 @@ import org.springframework.util.StringUtils; @API(status = API.Status.STABLE, since = "6.0") public final class Neo4jTransactionManager extends AbstractPlatformTransactionManager implements ApplicationContextAware { + @Serial + private static final long serialVersionUID = 7971369288503005574L; + /** * Start building a new transaction manager for the given driver instance. * @param driver A fixed driver instance. @@ -114,7 +118,7 @@ public final class Neo4jTransactionManager extends AbstractPlatformTransactionMa return this; } - public Builder withBookmarkManager(@Nullable Neo4jBookmarkManager bookmarkManager) { + public Builder withBookmarkManager(Neo4jBookmarkManager bookmarkManager) { this.bookmarkManager = bookmarkManager; return this; } @@ -127,19 +131,19 @@ public final class Neo4jTransactionManager extends AbstractPlatformTransactionMa /** * The underlying driver, which is also the synchronisation object. */ - private final Driver driver; + private transient final Driver driver; /** * Database name provider. */ - private final DatabaseSelectionProvider databaseSelectionProvider; + private transient final DatabaseSelectionProvider databaseSelectionProvider; /** * Provider for user impersonation. */ - private final UserSelectionProvider userSelectionProvider; + private transient final UserSelectionProvider userSelectionProvider; - private final BookmarkManagerReference bookmarkManager; + private transient final BookmarkManagerReference bookmarkManager; /** * This will create a transaction manager for the default database. @@ -193,20 +197,6 @@ public final class Neo4jTransactionManager extends AbstractPlatformTransactionMa this.bookmarkManager.setApplicationContext(applicationContext); } - /** - * @param driver The driver that has been used as a synchronization object. - * @param targetDatabase The target database - * @return An optional managed transaction or {@literal null} if the method hasn't been called inside an ongoing - * Spring transaction - * @see #retrieveTransaction(Driver, DatabaseSelection, UserSelection) - * @deprecated since 6.2, use #retrieveTransaction(Driver, DatabaseSelection, UserSelection) - */ - @Deprecated - public static @Nullable Transaction retrieveTransaction(final Driver driver, @Nullable final String targetDatabase) { - - return retrieveTransaction(driver, StringUtils.hasText(targetDatabase) ? DatabaseSelection.byName(targetDatabase) : DatabaseSelection.undecided(), UserSelection.connectedUser()); - } - /** * This method provides a native Neo4j transaction to be used from within a {@link org.springframework.data.neo4j.core.Neo4jClient}. * In most cases this the native transaction will be controlled from the Neo4j specific {@link org.springframework.transaction.PlatformTransactionManager}. @@ -219,7 +209,8 @@ public final class Neo4jTransactionManager extends AbstractPlatformTransactionMa * @return An optional managed transaction or {@literal null} if the method hasn't been called inside an ongoing * Spring transaction */ - public static @Nullable Transaction retrieveTransaction( + @Nullable + public static Transaction retrieveTransaction( final Driver driver, final DatabaseSelection targetDatabase, final UserSelection asUser @@ -257,7 +248,7 @@ public final class Neo4jTransactionManager extends AbstractPlatformTransactionMa .registerSynchronization(new Neo4jSessionSynchronization(connectionHolder, driver)); TransactionSynchronizationManager.bindResource(driver, connectionHolder); - return connectionHolder.getTransaction(targetDatabase, asUser); + return Objects.requireNonNull(connectionHolder.getTransaction(targetDatabase, asUser)); } private static Neo4jTransactionObject extractNeo4jTransaction(Object transaction) { @@ -285,7 +276,7 @@ public final class Neo4jTransactionManager extends AbstractPlatformTransactionMa @Override protected boolean isExistingTransaction(Object transaction) throws TransactionException { - return extractNeo4jTransaction(transaction).hasResourceHolder(); + return extractNeo4jTransaction(transaction).getResourceHolder() != null; } @Override @@ -331,7 +322,7 @@ public final class Neo4jTransactionManager extends AbstractPlatformTransactionMa @Override protected void doResume(@Nullable Object transaction, Object suspendedResources) { - Neo4jTransactionObject transactionObject = extractNeo4jTransaction(transaction); + Neo4jTransactionObject transactionObject = extractNeo4jTransaction(Objects.requireNonNull(transaction)); transactionObject.setResourceHolder((Neo4jTransactionHolder) suspendedResources); TransactionSynchronizationManager.bindResource(driver, suspendedResources); @@ -347,7 +338,7 @@ public final class Neo4jTransactionManager extends AbstractPlatformTransactionMa this.bookmarkManager.resolve().updateBookmarks(transactionHolder.getBookmarks(), newBookmarks); } catch (Neo4jException ex) { if (ex instanceof RetryableException) { - throw new TransactionSystemException(ex.getMessage(), ex); + throw new TransactionSystemException(Objects.requireNonNullElse(ex.getMessage(), "Caught a retryable exception"), ex); } throw ex; } @@ -383,7 +374,8 @@ public final class Neo4jTransactionManager extends AbstractPlatformTransactionMa // The resource holder is null when the call to TransactionSynchronizationManager.getResource // in Neo4jTransactionManager.doGetTransaction didn't return a corresponding resource holder. // If it is null, there's no existing session / transaction. - @Nullable private Neo4jTransactionHolder resourceHolder; + @Nullable + private Neo4jTransactionHolder resourceHolder; Neo4jTransactionObject(@Nullable Neo4jTransactionHolder resourceHolder) { this.resourceHolder = resourceHolder; @@ -393,23 +385,19 @@ public final class Neo4jTransactionManager extends AbstractPlatformTransactionMa * Usually called in {@link #doBegin(Object, TransactionDefinition)} which is called when there's no existing * transaction. * - * @param resourceHolder A newly created resource holder with a fresh drivers session, + * @param resourceHolder A newly created resource holder with a fresh drivers' session, */ void setResourceHolder(@Nullable Neo4jTransactionHolder resourceHolder) { this.resourceHolder = resourceHolder; } - /** - * @return {@literal true} if a {@link Neo4jTransactionHolder} is set. - */ - boolean hasResourceHolder() { - return resourceHolder != null; + @Nullable Neo4jTransactionHolder getResourceHolder() { + return resourceHolder; } Neo4jTransactionHolder getRequiredResourceHolder() { - Assert.state(hasResourceHolder(), RESOURCE_HOLDER_NOT_PRESENT_MESSAGE); - return resourceHolder; + return Objects.requireNonNull(resourceHolder, RESOURCE_HOLDER_NOT_PRESENT_MESSAGE); } void setRollbackOnly() { @@ -419,7 +407,7 @@ public final class Neo4jTransactionManager extends AbstractPlatformTransactionMa @Override public boolean isRollbackOnly() { - return this.hasResourceHolder() && this.resourceHolder.isRollbackOnly(); + return this.resourceHolder != null && this.resourceHolder.isRollbackOnly(); } @Override diff --git a/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionUtils.java b/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionUtils.java index d7190e1eb..b3abb74f8 100644 --- a/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionUtils.java +++ b/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionUtils.java @@ -20,6 +20,7 @@ import java.time.Duration; import java.util.Collection; import java.util.Collections; +import org.jspecify.annotations.Nullable; import org.neo4j.driver.AccessMode; import org.neo4j.driver.Bookmark; import org.neo4j.driver.SessionConfig; @@ -39,6 +40,7 @@ import org.springframework.util.ReflectionUtils; */ public final class Neo4jTransactionUtils { + @Nullable private static final Method WITH_IMPERSONATED_USER = ReflectionUtils.findMethod(SessionConfig.Builder.class, "withImpersonatedUser", String.class); @@ -46,7 +48,7 @@ public final class Neo4jTransactionUtils { return WITH_IMPERSONATED_USER != null; } - @SuppressWarnings("UnusedReturnValue") + @SuppressWarnings({"UnusedReturnValue", "NullAway"}) public static SessionConfig.Builder withImpersonatedUser(SessionConfig.Builder builder, String user) { if (driverSupportsImpersonation()) { diff --git a/src/main/java/org/springframework/data/neo4j/core/transaction/ReactiveDefaultBookmarkManager.java b/src/main/java/org/springframework/data/neo4j/core/transaction/ReactiveDefaultBookmarkManager.java index 2f9560389..6b859baaa 100644 --- a/src/main/java/org/springframework/data/neo4j/core/transaction/ReactiveDefaultBookmarkManager.java +++ b/src/main/java/org/springframework/data/neo4j/core/transaction/ReactiveDefaultBookmarkManager.java @@ -22,9 +22,9 @@ import java.util.Objects; import java.util.Set; import java.util.function.Supplier; +import org.jspecify.annotations.Nullable; import org.neo4j.driver.Bookmark; import org.springframework.context.ApplicationEventPublisher; -import org.springframework.lang.Nullable; /** * Default bookmark manager. diff --git a/src/main/java/org/springframework/data/neo4j/core/transaction/ReactiveNeo4jTransactionHolder.java b/src/main/java/org/springframework/data/neo4j/core/transaction/ReactiveNeo4jTransactionHolder.java index 6e1278175..3d3de85ca 100644 --- a/src/main/java/org/springframework/data/neo4j/core/transaction/ReactiveNeo4jTransactionHolder.java +++ b/src/main/java/org/springframework/data/neo4j/core/transaction/ReactiveNeo4jTransactionHolder.java @@ -20,12 +20,12 @@ import reactor.core.publisher.Mono; import java.util.Collection; import java.util.Set; +import org.jspecify.annotations.Nullable; import org.neo4j.driver.Bookmark; import org.neo4j.driver.reactivestreams.ReactiveSession; import org.neo4j.driver.reactivestreams.ReactiveTransaction; import org.springframework.data.neo4j.core.DatabaseSelection; import org.springframework.data.neo4j.core.UserSelection; -import org.springframework.lang.Nullable; import org.springframework.transaction.support.ResourceHolderSupport; /** diff --git a/src/main/java/org/springframework/data/neo4j/core/transaction/ReactiveNeo4jTransactionManager.java b/src/main/java/org/springframework/data/neo4j/core/transaction/ReactiveNeo4jTransactionManager.java index 458fdaf3a..ded60db71 100644 --- a/src/main/java/org/springframework/data/neo4j/core/transaction/ReactiveNeo4jTransactionManager.java +++ b/src/main/java/org/springframework/data/neo4j/core/transaction/ReactiveNeo4jTransactionManager.java @@ -15,10 +15,14 @@ */ package org.springframework.data.neo4j.core.transaction; +import java.io.Serial; +import java.util.Objects; + import reactor.core.publisher.Mono; import reactor.util.function.Tuples; import org.apiguardian.api.API; +import org.jspecify.annotations.Nullable; import org.neo4j.driver.Driver; import org.neo4j.driver.TransactionConfig; import org.neo4j.driver.exceptions.RetryableException; @@ -32,7 +36,6 @@ import org.springframework.data.neo4j.core.ReactiveDatabaseSelectionProvider; import org.springframework.data.neo4j.core.ReactiveUserSelectionProvider; import org.springframework.data.neo4j.core.UserSelection; import org.springframework.data.neo4j.core.support.BookmarkManagerReference; -import org.springframework.lang.Nullable; import org.springframework.transaction.NoTransactionException; import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.TransactionException; @@ -52,6 +55,9 @@ import org.springframework.util.Assert; @API(status = API.Status.STABLE, since = "6.0") public final class ReactiveNeo4jTransactionManager extends AbstractReactiveTransactionManager implements ApplicationContextAware { + @Serial + private static final long serialVersionUID = 204661696798919944L; + /** * Start building a new transaction manager for the given driver instance. * @param driver A fixed driver instance. @@ -124,19 +130,19 @@ public final class ReactiveNeo4jTransactionManager extends AbstractReactiveTrans /** * The underlying driver, which is also the synchronisation object. */ - private final Driver driver; + private transient final Driver driver; /** * Database name provider. */ - private final ReactiveDatabaseSelectionProvider databaseSelectionProvider; + private transient final ReactiveDatabaseSelectionProvider databaseSelectionProvider; /** * Provider for user impersonation. */ - private final ReactiveUserSelectionProvider userSelectionProvider; + private transient final ReactiveUserSelectionProvider userSelectionProvider; - private final BookmarkManagerReference bookmarkManager; + private transient final BookmarkManagerReference bookmarkManager; /** * This will create a transaction manager for the default database. @@ -272,7 +278,7 @@ public final class ReactiveNeo4jTransactionManager extends AbstractReactiveTrans @Override protected boolean isExistingTransaction(Object transaction) throws TransactionException { - return extractNeo4jTransaction(transaction).hasResourceHolder(); + return extractNeo4jTransaction(transaction).getResourceHolder() != null; } @Override @@ -328,7 +334,7 @@ public final class ReactiveNeo4jTransactionManager extends AbstractReactiveTrans .getRequiredResourceHolder(); return holder.commit() .doOnNext(bookmark -> bookmarkManager.resolve().updateBookmarks(holder.getBookmarks(), bookmark)) - .onErrorMap(e -> e instanceof RetryableException, e -> new TransactionSystemException(e.getMessage(), e)) + .onErrorMap(e -> e instanceof RetryableException, ex -> new TransactionSystemException(Objects.requireNonNullElse(ex.getMessage(), "Caught a retryable exception"), ex)) .then(); } @@ -350,10 +356,10 @@ public final class ReactiveNeo4jTransactionManager extends AbstractReactiveTrans } @Override - protected Mono doResume(TransactionSynchronizationManager synchronizationManager, Object transaction, + protected Mono doResume(TransactionSynchronizationManager synchronizationManager, @Nullable Object transaction, Object suspendedResources) throws TransactionException { - return Mono.just(extractNeo4jTransaction(transaction)) + return Mono.just(extractNeo4jTransaction(Objects.requireNonNull(transaction))) .doOnNext(r -> r.setResourceHolder((ReactiveNeo4jTransactionHolder) suspendedResources)) .then(Mono.fromRunnable(() -> synchronizationManager.bindResource(driver, suspendedResources))); } @@ -379,7 +385,8 @@ public final class ReactiveNeo4jTransactionManager extends AbstractReactiveTrans // The resource holder is null when the call to TransactionSynchronizationManager.getResource // in Neo4jTransactionManager.doGetTransaction didn't return a corresponding resource holder. // If it is null, there's no existing session / transaction. - @Nullable private ReactiveNeo4jTransactionHolder resourceHolder; + @Nullable + private ReactiveNeo4jTransactionHolder resourceHolder; ReactiveNeo4jTransactionObject(@Nullable ReactiveNeo4jTransactionHolder resourceHolder) { this.resourceHolder = resourceHolder; @@ -389,33 +396,24 @@ public final class ReactiveNeo4jTransactionManager extends AbstractReactiveTrans * Usually called in {@link #doBegin(TransactionSynchronizationManager, Object, TransactionDefinition)} which is * called when there's no existing transaction. * - * @param resourceHolder A newly created resource holder with a fresh drivers session, + * @param resourceHolder A newly created resource holder with a fresh drivers' session, */ void setResourceHolder(@Nullable ReactiveNeo4jTransactionHolder resourceHolder) { this.resourceHolder = resourceHolder; } - /** - * @return {@literal true} if a {@link Neo4jTransactionHolder} is set. - */ - boolean hasResourceHolder() { - return resourceHolder != null; - } - ReactiveNeo4jTransactionHolder getRequiredResourceHolder() { - Assert.state(hasResourceHolder(), RESOURCE_HOLDER_NOT_PRESENT_MESSAGE); - return resourceHolder; + return Objects.requireNonNull(this.resourceHolder, RESOURCE_HOLDER_NOT_PRESENT_MESSAGE); } - void setRollbackOnly() { - - getRequiredResourceHolder().setRollbackOnly(); + @Nullable ReactiveNeo4jTransactionHolder getResourceHolder() { + return resourceHolder; } @Override public boolean isRollbackOnly() { - return this.hasResourceHolder() && this.resourceHolder.isRollbackOnly(); + return this.resourceHolder != null && this.resourceHolder.isRollbackOnly(); } @Override diff --git a/src/main/java/org/springframework/data/neo4j/core/transaction/package-info.java b/src/main/java/org/springframework/data/neo4j/core/transaction/package-info.java index 2cf4112d2..800ad44b5 100644 --- a/src/main/java/org/springframework/data/neo4j/core/transaction/package-info.java +++ b/src/main/java/org/springframework/data/neo4j/core/transaction/package-info.java @@ -4,7 +4,7 @@ both the imperative and reactive `TransactionManager` as `Neo4jTransactionManager` and `ReactiveNeo4jTransactionManager`. * */ -@NonNullApi +@NullMarked package org.springframework.data.neo4j.core.transaction; -import org.springframework.lang.NonNullApi; +import org.jspecify.annotations.NullMarked; diff --git a/src/main/java/org/springframework/data/neo4j/repository/NoResultException.java b/src/main/java/org/springframework/data/neo4j/repository/NoResultException.java index 96ead1dcc..82dc6e350 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/NoResultException.java +++ b/src/main/java/org/springframework/data/neo4j/repository/NoResultException.java @@ -15,6 +15,8 @@ */ package org.springframework.data.neo4j.repository; +import java.io.Serial; + import org.apiguardian.api.API; import org.springframework.dao.EmptyResultDataAccessException; @@ -26,8 +28,10 @@ import org.springframework.dao.EmptyResultDataAccessException; * @since 6.0 */ @API(status = API.Status.STABLE, since = "6.0") -public class NoResultException extends EmptyResultDataAccessException { +public final class NoResultException extends EmptyResultDataAccessException { + @Serial + private static final long serialVersionUID = -1508370436250180391L; private final String query; public NoResultException(int expectedNumberOfResults, String query) { diff --git a/src/main/java/org/springframework/data/neo4j/repository/config/Neo4jRepositoryConfigurationExtension.java b/src/main/java/org/springframework/data/neo4j/repository/config/Neo4jRepositoryConfigurationExtension.java index f2bd84d60..38855006d 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/config/Neo4jRepositoryConfigurationExtension.java +++ b/src/main/java/org/springframework/data/neo4j/repository/config/Neo4jRepositoryConfigurationExtension.java @@ -132,7 +132,7 @@ public final class Neo4jRepositoryConfigurationExtension extends RepositoryConfi source.getAttribute("transactionManagerRef").orElse(DEFAULT_TRANSACTION_MANAGER_BEAN_NAME)); builder.addPropertyReference("neo4jOperations", source.getAttribute("neo4jTemplateRef").orElse(DEFAULT_NEO4J_TEMPLATE_BEAN_NAME)); - builder.addPropertyReference("neo4jMappingContext", + builder.addPropertyReference("mappingContext", source.getAttribute("neo4jMappingContextRef").orElse(DEFAULT_MAPPING_CONTEXT_BEAN_NAME)); } diff --git a/src/main/java/org/springframework/data/neo4j/repository/config/ReactiveNeo4jRepositoryConfigurationExtension.java b/src/main/java/org/springframework/data/neo4j/repository/config/ReactiveNeo4jRepositoryConfigurationExtension.java index c2d75b73e..3c5147b58 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/config/ReactiveNeo4jRepositoryConfigurationExtension.java +++ b/src/main/java/org/springframework/data/neo4j/repository/config/ReactiveNeo4jRepositoryConfigurationExtension.java @@ -130,7 +130,7 @@ public final class ReactiveNeo4jRepositoryConfigurationExtension extends Reposit source.getAttribute("transactionManagerRef").orElse(DEFAULT_TRANSACTION_MANAGER_BEAN_NAME)); builder.addPropertyReference("neo4jOperations", source.getAttribute("neo4jTemplateRef").orElse(DEFAULT_NEO4J_TEMPLATE_BEAN_NAME)); - builder.addPropertyReference("neo4jMappingContext", + builder.addPropertyReference("mappingContext", source.getAttribute("neo4jMappingContextRef").orElse(DEFAULT_MAPPING_CONTEXT_BEAN_NAME)); } } diff --git a/src/main/java/org/springframework/data/neo4j/repository/config/package-info.java b/src/main/java/org/springframework/data/neo4j/repository/config/package-info.java index 0660a187d..d5b5983c2 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/config/package-info.java +++ b/src/main/java/org/springframework/data/neo4j/repository/config/package-info.java @@ -4,7 +4,7 @@ and reactive Spring Data Neo4j repositories. * */ -@NonNullApi +@NullMarked package org.springframework.data.neo4j.repository.config; -import org.springframework.lang.NonNullApi; +import org.jspecify.annotations.NullMarked; diff --git a/src/main/java/org/springframework/data/neo4j/repository/package-info.java b/src/main/java/org/springframework/data/neo4j/repository/package-info.java index ce353f04e..e413b09f8 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/package-info.java +++ b/src/main/java/org/springframework/data/neo4j/repository/package-info.java @@ -3,7 +3,7 @@ This package provides the Neo4j imperative and reactive repository API. * */ -@NonNullApi +@NullMarked package org.springframework.data.neo4j.repository; -import org.springframework.lang.NonNullApi; +import org.jspecify.annotations.NullMarked; diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/AbstractNeo4jQuery.java b/src/main/java/org/springframework/data/neo4j/repository/query/AbstractNeo4jQuery.java index 3052b4e2e..f90207b7f 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/AbstractNeo4jQuery.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/AbstractNeo4jQuery.java @@ -24,6 +24,7 @@ import java.util.function.LongSupplier; import java.util.function.Supplier; import java.util.function.UnaryOperator; +import org.jspecify.annotations.Nullable; import org.neo4j.driver.types.MapAccessor; import org.neo4j.driver.types.TypeSystem; import org.springframework.core.convert.converter.Converter; @@ -48,7 +49,6 @@ import org.springframework.data.repository.query.ResultProcessor; import org.springframework.data.repository.query.ReturnedType; import org.springframework.data.support.PageableExecutionUtils; import org.springframework.data.util.TypeInformation; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -94,13 +94,14 @@ abstract class AbstractNeo4jQuery extends Neo4jQuerySupport implements Repositor if (Iterable.class.isAssignableFrom(returnType)) { TypeInformation from = TypeInformation.fromReturnTypeOf(repositoryMethod); - return GeoResult.class.equals(from.getComponentType().getType()); + return from.getComponentType() != null && GeoResult.class.equals(from.getComponentType().getType()); } return GeoPage.class.isAssignableFrom(returnType); } @Override + @Nullable public final Object execute(Object[] parameters) { boolean incrementLimit = queryMethod.incrementLimit(); @@ -122,8 +123,10 @@ abstract class AbstractNeo4jQuery extends Neo4jQuerySupport implements Repositor DtoInstantiatingConverter converter = new DtoInstantiatingConverter(returnedType.getReturnedType(), mappingContext); // Neo4jQuerySupport ensure we will get an EntityInstanceWithSource in the projecting case - preparingConverter = source -> converter.convert( - (EntityInstanceWithSource) OptionalUnwrappingConverter.INSTANCE.convert(source)); + preparingConverter = source -> { + var unwrapped = (EntityInstanceWithSource) OptionalUnwrappingConverter.INSTANCE.convert(source); + return (unwrapped == null) ? null : converter.convert(unwrapped); + }; } if (queryMethod.isPageQuery()) { @@ -180,11 +183,11 @@ abstract class AbstractNeo4jQuery extends Neo4jQuerySupport implements Repositor } } - protected abstract PreparedQuery prepareQuery(Class returnedType, + protected abstract PreparedQuery prepareQuery(Class returnedType, Collection includedProperties, Neo4jParameterAccessor parameterAccessor, @Nullable Neo4jQueryType queryType, @Nullable Supplier> mappingFunction, - @Nullable UnaryOperator limitModifier); + UnaryOperator limitModifier); protected Optional> getCountQuery(Neo4jParameterAccessor parameterAccessor) { return Optional.empty(); diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/AbstractReactiveNeo4jQuery.java b/src/main/java/org/springframework/data/neo4j/repository/query/AbstractReactiveNeo4jQuery.java index 534a9b1cf..846c7d44c 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/AbstractReactiveNeo4jQuery.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/AbstractReactiveNeo4jQuery.java @@ -20,6 +20,7 @@ import java.util.function.BiFunction; import java.util.function.Supplier; import java.util.function.UnaryOperator; +import org.jspecify.annotations.Nullable; import org.neo4j.driver.types.MapAccessor; import org.neo4j.driver.types.TypeSystem; import org.springframework.core.convert.converter.Converter; @@ -37,7 +38,6 @@ import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.query.ResultProcessor; import org.springframework.data.repository.query.ReturnedType; import org.springframework.data.util.TypeInformation; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import reactor.core.publisher.Flux; @@ -84,13 +84,15 @@ abstract class AbstractReactiveNeo4jQuery extends Neo4jQuerySupport implements R if (Flux.class.isAssignableFrom(returnType)) { TypeInformation from = TypeInformation.fromReturnTypeOf(repositoryMethod); - return GeoResult.class.equals(from.getComponentType().getType()); + TypeInformation componentType = from.getComponentType(); + return componentType != null && GeoResult.class.equals(componentType.getType()); } return false; } @Override + @Nullable public final Object execute(Object[] parameters) { boolean incrementLimit = queryMethod.incrementLimit(); @@ -111,8 +113,10 @@ abstract class AbstractReactiveNeo4jQuery extends Neo4jQuerySupport implements R DtoInstantiatingConverter converter = new DtoInstantiatingConverter(returnedType.getReturnedType(), mappingContext); // Neo4jQuerySupport ensure we will get an EntityInstanceWithSource in the projecting case - preparingConverter = source -> converter.convert( - (EntityInstanceWithSource) OptionalUnwrappingConverter.INSTANCE.convert(source)); + preparingConverter = source -> { + var intermediate = (EntityInstanceWithSource) OptionalUnwrappingConverter.INSTANCE.convert(source); + return (intermediate == null) ? null : converter.convert(intermediate); + }; } if (queryMethod.isScrollQuery()) { @@ -125,6 +129,6 @@ abstract class AbstractReactiveNeo4jQuery extends Neo4jQuerySupport implements R protected abstract PreparedQuery prepareQuery(Class returnedType, Collection includedProperties, Neo4jParameterAccessor parameterAccessor, - @Nullable Neo4jQueryType queryType, @Nullable Supplier> mappingFunction, - @Nullable UnaryOperator limitModifier); + @Nullable Neo4jQueryType queryType, Supplier> mappingFunction, + UnaryOperator limitModifier); } diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/CypherAdapterUtils.java b/src/main/java/org/springframework/data/neo4j/repository/query/CypherAdapterUtils.java index c6e3ed249..c8dae9997 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/CypherAdapterUtils.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/CypherAdapterUtils.java @@ -20,21 +20,21 @@ import static org.neo4j.cypherdsl.core.Cypher.property; import java.util.Collection; import java.util.HashMap; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.function.BiFunction; import java.util.function.Function; import java.util.stream.Collectors; import org.apiguardian.api.API; +import org.jspecify.annotations.Nullable; import org.neo4j.cypherdsl.core.Condition; import org.neo4j.cypherdsl.core.Cypher; import org.neo4j.cypherdsl.core.Expression; import org.neo4j.cypherdsl.core.SortItem; -import org.neo4j.cypherdsl.core.StatementBuilder; import org.neo4j.cypherdsl.core.SymbolicName; import org.neo4j.driver.Value; import org.springframework.data.domain.KeysetScrollPosition; -import org.springframework.data.domain.Pageable; import org.springframework.data.domain.ScrollPosition.Direction; import org.springframework.data.domain.Sort; import org.springframework.data.neo4j.core.convert.Neo4jConversionService; @@ -161,7 +161,7 @@ public final class CypherAdapterUtils { expression = entity.getIdExpression(); var comparatorFunction = getComparatorFunction(scrollPosition.scrollsForward() ? Sort.Direction.ASC : Sort.Direction.DESC, scrollDirection); allEqualsWithArtificialSort = allEqualsWithArtificialSort.and(comparatorFunction.apply(expression, parameter)); - } else { + } else if (propertyAndDirection.containsKey(k)) { var p = propertyAndDirection.get(k); expression = p.property.isIdProperty() ? entity.getIdExpression() : root.property(k); @@ -188,22 +188,14 @@ public final class CypherAdapterUtils { * @param sort The sort object to convert * @return An of sort items. It will be empty when sort is unsorted. */ - public static Collection toSortItems(NodeDescription nodeDescription, Sort sort) { + public static Collection toSortItems(@Nullable NodeDescription nodeDescription, Sort sort) { + + if (nodeDescription == null) { + return List.of(); + } return sort.stream().map(sortAdapterFor(nodeDescription)).collect(Collectors.toList()); } - public static StatementBuilder.BuildableStatement addPagingParameter(NodeDescription nodeDescription, - Pageable pageable, StatementBuilder.OngoingReadingAndReturn returning) { - - Sort sort = pageable.getSort(); - - long skip = pageable.getOffset(); - - int pageSize = pageable.getPageSize(); - - return returning.orderBy(toSortItems(nodeDescription, sort)).skip(skip).limit(pageSize); - } - private CypherAdapterUtils() {} } diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/CypherQueryCreator.java b/src/main/java/org/springframework/data/neo4j/repository/query/CypherQueryCreator.java index 1cb415090..7bace57b2 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/CypherQueryCreator.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/CypherQueryCreator.java @@ -32,6 +32,7 @@ import java.util.function.Supplier; import java.util.function.UnaryOperator; import java.util.stream.Collectors; +import org.jspecify.annotations.Nullable; import org.neo4j.cypherdsl.core.Condition; import org.neo4j.cypherdsl.core.Cypher; import org.neo4j.cypherdsl.core.Expression; @@ -64,8 +65,6 @@ import org.springframework.data.repository.query.QueryMethod; import org.springframework.data.repository.query.parser.AbstractQueryCreator; import org.springframework.data.repository.query.parser.Part; import org.springframework.data.repository.query.parser.PartTree; -import org.springframework.lang.NonNull; -import org.springframework.lang.Nullable; /** * A Cypher-DSL based implementation of the {@link AbstractQueryCreator} that eventually creates Cypher queries as @@ -94,11 +93,13 @@ final class CypherQueryCreator extends AbstractQueryCreator(this.sortItems); @@ -580,7 +583,8 @@ final class CypherQueryCreator extends AbstractQueryCreator conversionOverride; + @Nullable + final Neo4jPersistentPropertyConverter conversionOverride; Parameter(String nameOrIndex, Object value, @Nullable Neo4jPersistentPropertyConverter conversionOverride) { this.nameOrIndex = nameOrIndex; diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/CypherdslBasedQuery.java b/src/main/java/org/springframework/data/neo4j/repository/query/CypherdslBasedQuery.java index 88be8aa21..7966223ec 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/CypherdslBasedQuery.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/CypherdslBasedQuery.java @@ -23,6 +23,7 @@ import java.util.function.Function; import java.util.function.Supplier; import java.util.function.UnaryOperator; +import org.jspecify.annotations.Nullable; import org.neo4j.cypherdsl.core.Statement; import org.neo4j.cypherdsl.core.StatementBuilder.OngoingReadingAndReturn; import org.neo4j.driver.types.MapAccessor; @@ -64,8 +65,8 @@ final class CypherdslBasedQuery extends AbstractNeo4jQuery { @Override protected PreparedQuery prepareQuery(Class returnedType, Collection includedProperties, - Neo4jParameterAccessor parameterAccessor, Neo4jQueryType queryType, - Supplier> mappingFunction, + Neo4jParameterAccessor parameterAccessor, @Nullable Neo4jQueryType queryType, + @Nullable Supplier> mappingFunction, UnaryOperator limitModifier) { Object[] parameters = parameterAccessor.getValues(); diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/FetchableFluentQueryByExample.java b/src/main/java/org/springframework/data/neo4j/repository/query/FetchableFluentQueryByExample.java index 4d003b310..219218ba9 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/FetchableFluentQueryByExample.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/FetchableFluentQueryByExample.java @@ -15,7 +15,14 @@ */ package org.springframework.data.neo4j.repository.query; +import java.util.Collection; +import java.util.List; +import java.util.function.Function; +import java.util.function.LongSupplier; +import java.util.stream.Stream; + import org.apiguardian.api.API; +import org.jspecify.annotations.Nullable; import org.neo4j.cypherdsl.core.Condition; import org.springframework.data.domain.Example; import org.springframework.data.domain.KeysetScrollPosition; @@ -30,13 +37,6 @@ import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity; import org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery; import org.springframework.data.support.PageableExecutionUtils; -import org.springframework.lang.Nullable; - -import java.util.Collection; -import java.util.List; -import java.util.function.Function; -import java.util.function.LongSupplier; -import java.util.stream.Stream; /** * Immutable implementation of a {@link FetchableFluentQuery}. All @@ -124,6 +124,7 @@ final class FetchableFluentQueryByExample extends FluentQuerySupport im } @Override + @Nullable public R oneValue() { return findOperation.find(example.getProbeType()) @@ -134,6 +135,7 @@ final class FetchableFluentQueryByExample extends FluentQuerySupport im } @Override + @Nullable public R firstValue() { List all = all(); @@ -166,7 +168,7 @@ final class FetchableFluentQueryByExample extends FluentQuerySupport im @Override public Window scroll(ScrollPosition scrollPosition) { Class domainType = this.example.getProbeType(); - Neo4jPersistentEntity entity = mappingContext.getPersistentEntity(domainType); + Neo4jPersistentEntity entity = mappingContext.getRequiredPersistentEntity(domainType); var skip = scrollPosition.isInitial() ? 0 @@ -174,7 +176,7 @@ final class FetchableFluentQueryByExample extends FluentQuerySupport im : 0; Condition condition = scrollPosition instanceof KeysetScrollPosition keysetScrollPosition - ? CypherAdapterUtils.combineKeysetIntoCondition(mappingContext.getPersistentEntity(example.getProbeType()), keysetScrollPosition, sort, mappingContext.getConversionService()) + ? CypherAdapterUtils.combineKeysetIntoCondition(mappingContext.getRequiredPersistentEntity(example.getProbeType()), keysetScrollPosition, sort, mappingContext.getConversionService()) : null; List rawResult = findOperation.find(domainType) diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/FetchableFluentQueryByPredicate.java b/src/main/java/org/springframework/data/neo4j/repository/query/FetchableFluentQueryByPredicate.java index f7d9c7000..c0b546d4a 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/FetchableFluentQueryByPredicate.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/FetchableFluentQueryByPredicate.java @@ -22,6 +22,7 @@ import java.util.function.LongSupplier; import java.util.stream.Stream; import org.apiguardian.api.API; +import org.jspecify.annotations.Nullable; import org.neo4j.cypherdsl.core.Cypher; import org.springframework.data.domain.KeysetScrollPosition; import org.springframework.data.domain.Page; @@ -34,7 +35,6 @@ import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity; import org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery; import org.springframework.data.support.PageableExecutionUtils; -import org.springframework.lang.Nullable; import com.querydsl.core.types.Predicate; @@ -129,6 +129,7 @@ final class FetchableFluentQueryByPredicate extends FluentQuerySupport } @Override + @Nullable public R oneValue() { return findOperation.find(metaData.getType()) @@ -143,6 +144,7 @@ final class FetchableFluentQueryByPredicate extends FluentQuerySupport } @Override + @Nullable public R firstValue() { List all = all(); diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/FluentQuerySupport.java b/src/main/java/org/springframework/data/neo4j/repository/query/FluentQuerySupport.java index da955ce45..babc59371 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/FluentQuerySupport.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/FluentQuerySupport.java @@ -24,6 +24,7 @@ import java.util.Set; import java.util.function.IntFunction; import java.util.function.Predicate; +import org.jspecify.annotations.Nullable; import org.springframework.data.domain.KeysetScrollPosition; import org.springframework.data.domain.OffsetScrollPosition; import org.springframework.data.domain.ScrollPosition; @@ -32,7 +33,6 @@ import org.springframework.data.domain.Window; import org.springframework.data.neo4j.core.mapping.Constants; import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity; import org.springframework.data.neo4j.core.mapping.PropertyFilter; -import org.springframework.lang.Nullable; /** * Supporting class containing some state and convenience methods for building fluent queries (both imperative and reactive). @@ -47,9 +47,9 @@ abstract class FluentQuerySupport { protected final Sort sort; + @Nullable protected final Integer limit; - @Nullable protected final Set properties; FluentQuerySupport( @@ -64,7 +64,7 @@ abstract class FluentQuerySupport { if (properties != null) { this.properties = new HashSet<>(properties); } else { - this.properties = null; + this.properties = Set.of(); } } diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jNestedMapEntityWriter.java b/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jNestedMapEntityWriter.java index 2f12fdbdd..308f8c400 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jNestedMapEntityWriter.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jNestedMapEntityWriter.java @@ -31,6 +31,7 @@ import java.util.stream.Collectors; import java.util.stream.Stream; import org.apiguardian.api.API; +import org.jspecify.annotations.Nullable; import org.neo4j.driver.Value; import org.neo4j.driver.Values; import org.springframework.data.convert.EntityWriter; @@ -49,7 +50,6 @@ import org.springframework.data.neo4j.core.mapping.PropertyHandlerSupport; import org.springframework.data.neo4j.core.mapping.RelationshipDescription; import org.springframework.data.neo4j.core.schema.TargetNode; import org.springframework.data.util.TypeInformation; -import org.springframework.lang.Nullable; /** * A specialized version of an {@link EntityWriter} for Neo4j that traverses the entity and maps the entity, @@ -77,7 +77,7 @@ final class Neo4jNestedMapEntityWriter implements EntityWriter sink) { + public void write(Object source, Map sink) { if (source == null) { return; @@ -132,7 +132,9 @@ final class Neo4jNestedMapEntityWriter implements EntityWriter propertyMap = (Map) sink.get(Constants.NAME_OF_PROPERTIES_PARAM); - propertyMap.remove(idProperty.getPropertyName()); + if (propertyMap != null) { + propertyMap.remove(idProperty.getPropertyName()); + } } // Param not needed @@ -181,7 +183,7 @@ final class Neo4jNestedMapEntityWriter implements EntityWriter createParameters(ParametersSource parametersSource) { - return new Neo4jParameters(parametersSource); - } - - static class Neo4jParameters extends Parameters { + static class Neo4jParameters extends Parameters<@NonNull Neo4jParameters, @NonNull Neo4jParameter> { Neo4jParameters(ParametersSource parametersSource) { super(parametersSource, it -> new Neo4jParameter(it, parametersSource.getDomainTypeInformation())); diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQuerySupport.java b/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQuerySupport.java index ceec1629d..7c4528d29 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQuerySupport.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQuerySupport.java @@ -26,6 +26,7 @@ import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.function.BiFunction; import java.util.function.Supplier; @@ -33,6 +34,7 @@ import java.util.stream.Collectors; import java.util.stream.StreamSupport; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.neo4j.driver.Values; import org.neo4j.driver.types.MapAccessor; import org.neo4j.driver.types.TypeSystem; @@ -61,7 +63,6 @@ import org.springframework.data.repository.query.QueryMethod; import org.springframework.data.repository.query.ResultProcessor; import org.springframework.data.repository.query.ReturnedType; import org.springframework.data.util.TypeInformation; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -156,7 +157,7 @@ abstract class Neo4jQuerySupport { return VALID_RETURN_TYPES_FOR_DELETE.contains(queryMethod.getResultProcessor().getReturnedType().getReturnedType()); } - static void logParameterIfNull(String name, Object value) { + static void logParameterIfNull(String name, @Nullable Object value) { if (value != null || !REPOSITORY_QUERY_LOG.isDebugEnabled()) { return; @@ -177,7 +178,7 @@ abstract class Neo4jQuerySupport { * @param parameter The parameter to fit into the generated query. * @return A parameter that fits the placeholders of a generated query */ - final Object convertParameter(Object parameter) { + final Object convertParameter(@Nullable Object parameter) { return this.convertParameter(parameter, null); } @@ -188,11 +189,11 @@ abstract class Neo4jQuerySupport { * @param conversionOverride Passed to the entity converter if present. * @return A parameter that fits the placeholders of a generated query */ - final Object convertParameter(Object parameter, @Nullable Neo4jPersistentPropertyConverter conversionOverride) { + final Object convertParameter(@Nullable Object parameter, @Nullable Neo4jPersistentPropertyConverter conversionOverride) { if (parameter == null) { return Values.NULL; - } else if (parameter instanceof Range v) { + } else if (parameter instanceof Range v) { return convertRange(v); } else if (parameter instanceof Distance v) { return calculateDistanceInMeter(v); @@ -301,8 +302,8 @@ abstract class Neo4jQuerySupport { final Window createWindow(ResultProcessor resultProcessor, boolean incrementLimit, Neo4jParameterAccessor parameterAccessor, List rawResult, QueryFragmentsAndParameters orderBy) { var domainType = resultProcessor.getReturnedType().getDomainType(); - var neo4jPersistentEntity = mappingContext.getPersistentEntity(domainType); - var limit = orderBy.getQueryFragments().getLimit().intValue() - (incrementLimit ? 1 : 0); + var neo4jPersistentEntity = mappingContext.getRequiredPersistentEntity(domainType); + var limit = Objects.requireNonNull(orderBy.getQueryFragments().getLimit(), "Can't create a result window without a size (limit)").intValue() - (incrementLimit ? 1 : 0); var scrollPosition = parameterAccessor.getScrollPosition(); var scrollDirection = scrollPosition instanceof KeysetScrollPosition keysetScrollPosition ? keysetScrollPosition.getDirection() : Direction.FORWARD; diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jSpelSupport.java b/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jSpelSupport.java index 3d2e05e61..e83e7f30d 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jSpelSupport.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jSpelSupport.java @@ -24,7 +24,9 @@ import java.util.regex.Pattern; import java.util.stream.Collectors; import org.apiguardian.api.API; +import org.jspecify.annotations.Nullable; import org.neo4j.cypherdsl.support.schema_name.SchemaNames; +import org.springframework.core.env.StandardEnvironment; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.expression.ValueEvaluationContext; @@ -35,7 +37,6 @@ import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity; import org.springframework.data.repository.core.EntityMetadata; import org.springframework.expression.spel.support.StandardEvaluationContext; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -62,7 +63,8 @@ public final class Neo4jSpelSupport { * @param arg The {@link Sort sort object} to order the result set of the final query. * @return A literal replacement for a SpEL placeholder */ - public static LiteralReplacement orderBy(@Nullable Object arg) { + @Nullable + public static LiteralReplacement orderBy(Object arg) { Sort sort = null; if (arg instanceof Pageable v) { @@ -83,21 +85,21 @@ public final class Neo4jSpelSupport { * @param arg The object that will be inserted as a literal String into the query. It's {@code toString()} method will be used. * @return A literal replacement for a SpEL placeholder */ - public static LiteralReplacement literal(@Nullable Object arg) { + public static LiteralReplacement literal(Object arg) { return StringBasedLiteralReplacement .withTargetAndValue(LiteralReplacement.Target.UNSPECIFIED, arg == null ? "" : arg.toString()); } - public static LiteralReplacement anyOf(@Nullable Object arg) { + public static LiteralReplacement anyOf(Object arg) { return labels(arg, "|"); } - public static LiteralReplacement allOf(@Nullable Object arg) { + public static LiteralReplacement allOf(Object arg) { return labels(arg, "&"); } - private static LiteralReplacement labels(@Nullable Object arg, String joinOn) { + private static LiteralReplacement labels(Object arg, String joinOn) { return StringBasedLiteralReplacement .withTargetAndValue(LiteralReplacement.Target.UNSPECIFIED, arg == null ? "" : joinStrings(arg, joinOn) @@ -213,7 +215,6 @@ public final class Neo4jSpelSupport { } } - private static final Pattern LABEL_AND_TYPE_QUOTATION = Pattern.compile("`"); private static final String EXPRESSION_PARAMETER = "$1#{"; private static final String QUOTED_EXPRESSION_PARAMETER = "$1__HASH__{"; @@ -241,7 +242,7 @@ public final class Neo4jSpelSupport { return query; } - ValueEvaluationContext evalContext = ValueEvaluationContext.of(null, new StandardEvaluationContext()); + ValueEvaluationContext evalContext = ValueEvaluationContext.of(new StandardEnvironment(), new StandardEvaluationContext()); Neo4jPersistentEntity requiredPersistentEntity = mappingContext .getRequiredPersistentEntity(metadata.getJavaType()); evalContext.getEvaluationContext().setVariable(ENTITY_NAME, requiredPersistentEntity.getStaticLabels() diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/OptionalUnwrappingConverter.java b/src/main/java/org/springframework/data/neo4j/repository/query/OptionalUnwrappingConverter.java index 9b1981955..5a5cec347 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/OptionalUnwrappingConverter.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/OptionalUnwrappingConverter.java @@ -17,6 +17,7 @@ package org.springframework.data.neo4j.repository.query; import java.util.Optional; +import org.jspecify.annotations.Nullable; import org.springframework.core.convert.converter.Converter; /** @@ -29,6 +30,7 @@ enum OptionalUnwrappingConverter implements Converter { INSTANCE; @Override + @Nullable public Object convert(Object source) { if (source instanceof Optional v) { return v.orElse(null); diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/PartTreeNeo4jQuery.java b/src/main/java/org/springframework/data/neo4j/repository/query/PartTreeNeo4jQuery.java index f37f784e9..db05d4cd1 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/PartTreeNeo4jQuery.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/PartTreeNeo4jQuery.java @@ -21,6 +21,7 @@ import java.util.function.BiFunction; import java.util.function.Supplier; import java.util.function.UnaryOperator; +import org.jspecify.annotations.Nullable; import org.neo4j.driver.types.MapAccessor; import org.neo4j.driver.types.TypeSystem; import org.springframework.data.neo4j.core.Neo4jOperations; @@ -31,7 +32,6 @@ import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.query.parser.PartTree; import org.springframework.data.repository.query.parser.PartTree.OrPart; -import org.springframework.lang.Nullable; /** * Implementation of {@link RepositoryQuery} for derived finder methods. @@ -61,7 +61,7 @@ final class PartTreeNeo4jQuery extends AbstractNeo4jQuery { } @Override - protected PreparedQuery prepareQuery(Class returnedType, Collection includedProperties, + protected PreparedQuery prepareQuery(Class returnedType, Collection includedProperties, Neo4jParameterAccessor parameterAccessor, @Nullable Neo4jQueryType queryType, @Nullable Supplier> mappingFunction, UnaryOperator limitModifier) { diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/Predicate.java b/src/main/java/org/springframework/data/neo4j/repository/query/Predicate.java index 7e8b0c7d3..f8f337373 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/Predicate.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/Predicate.java @@ -24,11 +24,13 @@ import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.BiFunction; +import org.jspecify.annotations.Nullable; import org.neo4j.cypherdsl.core.Condition; import org.neo4j.cypherdsl.core.Cypher; import org.neo4j.cypherdsl.core.Expression; @@ -47,7 +49,6 @@ import org.springframework.data.neo4j.core.mapping.NodeDescription; import org.springframework.data.neo4j.core.mapping.RelationshipDescription; import org.springframework.data.support.ExampleMatcherAccessor; import org.springframework.data.util.DirectFieldAccessFallbackBeanWrapper; -import org.springframework.lang.Nullable; /** * Support class for "query by example" executors. @@ -84,9 +85,13 @@ final class Predicate { return predicate; } - private static void processRelationships(Neo4jMappingContext mappingContext, Example example, NodeDescription currentNodeDescription, - DirectFieldAccessFallbackBeanWrapper beanWrapper, ExampleMatcher.MatchMode mode, AtomicInteger relationshipPatternCount, - @Nullable PropertyPath propertyPath, Predicate predicate) { + private static void processRelationships(Neo4jMappingContext mappingContext, Example example, @Nullable NodeDescription currentNodeDescription, + DirectFieldAccessFallbackBeanWrapper beanWrapper, ExampleMatcher.MatchMode mode, AtomicInteger relationshipPatternCount, + @Nullable PropertyPath propertyPath, Predicate predicate) { + + if (currentNodeDescription == null) { + return; + } for (RelationshipDescription relationship : currentNodeDescription.getRelationships()) { String relationshipFieldName = relationship.getFieldName(); @@ -98,7 +103,7 @@ final class Predicate { // Right now we are only accepting the first element of a collection as a filter entry. // Maybe combining multiple entities with AND might make sense. - if (relationshipObject instanceof Collection collection) { + if (relationshipObject instanceof Collection collection) { int collectionSize = collection.size(); if (collectionSize > 1) { throw new IllegalArgumentException("Cannot have more than one related node per collection."); @@ -120,10 +125,12 @@ final class Predicate { PropertyPathWrapper nestedPropertyPathWrapper = new PropertyPathWrapper(relationshipPatternCount.incrementAndGet(), mappingContext.getPersistentPropertyPath(nestedPropertyPath), false); predicate.addRelationship(nestedPropertyPathWrapper); - for (GraphPropertyDescription graphProperty : relatedNodeDescription.getGraphProperties()) { - addConditionAndParameters(mappingContext, (Neo4jPersistentEntity) relatedNodeDescription, new DirectFieldAccessFallbackBeanWrapper(relationshipObject), mode, - new ExampleMatcherAccessor(example.getMatcher()), predicate, - graphProperty, nestedPropertyPathWrapper); + if (relatedNodeDescription != null) { + for (GraphPropertyDescription graphProperty : relatedNodeDescription.getGraphProperties()) { + addConditionAndParameters(mappingContext, (Neo4jPersistentEntity) relatedNodeDescription, new DirectFieldAccessFallbackBeanWrapper(relationshipObject), mode, + new ExampleMatcherAccessor(example.getMatcher()), predicate, + graphProperty, nestedPropertyPathWrapper); + } } processRelationships(mappingContext, example, relatedNodeDescription, new DirectFieldAccessFallbackBeanWrapper(relationshipObject), mode, relationshipPatternCount, @@ -166,7 +173,7 @@ final class Predicate { if (isRootNode) { condition = predicate.neo4jPersistentEntity.getIdExpression().isEqualTo(literalOf(theValue)); } else { - condition = nodeDescription.getIdDescription().asIdExpression(wrapper.getNodeName()).isEqualTo(literalOf(theValue)); + condition = Objects.requireNonNull(nodeDescription.getIdDescription(), "No id description available, cannot compute a Cypher expression for retrieving or storing the id").asIdExpression(wrapper.getNodeName()).isEqualTo(literalOf(theValue)); } } else { Expression property = !isRootNode ? property(wrapper.getNodeName(), propertyName) : property(Constants.NAME_OF_TYPED_ROOT_NODE.apply(nodeDescription), propertyName); @@ -203,7 +210,7 @@ final class Predicate { return condition; } - private final Neo4jPersistentEntity neo4jPersistentEntity; + private final Neo4jPersistentEntity neo4jPersistentEntity; private Condition condition = Cypher.noCondition(); @@ -211,7 +218,7 @@ final class Predicate { private final Set relationshipFields = new HashSet<>(); - private Predicate(Neo4jPersistentEntity neo4jPersistentEntity) { + private Predicate(Neo4jPersistentEntity neo4jPersistentEntity) { this.neo4jPersistentEntity = neo4jPersistentEntity; } diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/PropertyPathWrapper.java b/src/main/java/org/springframework/data/neo4j/repository/query/PropertyPathWrapper.java index a021e7374..6c08cab67 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/PropertyPathWrapper.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/PropertyPathWrapper.java @@ -15,6 +15,7 @@ */ package org.springframework.data.neo4j.repository.query; +import org.jspecify.annotations.Nullable; import org.neo4j.cypherdsl.core.Cypher; import org.neo4j.cypherdsl.core.ExposesRelationships; import org.neo4j.cypherdsl.core.Node; @@ -25,7 +26,6 @@ import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity; import org.springframework.data.neo4j.core.mapping.NodeDescription; import org.springframework.data.neo4j.core.mapping.RelationshipDescription; import org.springframework.data.neo4j.core.schema.TargetNode; -import org.springframework.lang.Nullable; class PropertyPathWrapper { private static final String NAME_OF_RELATED_FILTER_ENTITY = "m"; diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/QueryFragments.java b/src/main/java/org/springframework/data/neo4j/repository/query/QueryFragments.java index 594ace996..5d4d1b43e 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/QueryFragments.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/QueryFragments.java @@ -24,6 +24,7 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.function.Predicate; import org.apiguardian.api.API; +import org.jspecify.annotations.Nullable; import org.neo4j.cypherdsl.core.Condition; import org.neo4j.cypherdsl.core.Cypher; import org.neo4j.cypherdsl.core.Expression; @@ -37,7 +38,6 @@ import org.springframework.data.neo4j.core.mapping.PropertyFilter; import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity; import org.springframework.data.neo4j.core.mapping.NodeDescription; import org.springframework.data.neo4j.core.schema.Property; -import org.springframework.lang.Nullable; /** * Collects the parts of a Cypher query to be handed over to the Cypher generator. @@ -48,18 +48,25 @@ import org.springframework.lang.Nullable; @API(status = API.Status.INTERNAL, since = "6.0.4") public final class QueryFragments { private List matchOn = new ArrayList<>(); + @Nullable private Condition condition; private Collection returnExpressions = new ArrayList<>(); + @Nullable private Collection orderBy; + @Nullable private Number limit; + @Nullable private Long skip; + @Nullable private ReturnTuple returnTuple; private boolean scalarValueReturn = false; + @Nullable private Expression deleteExpression; /** * This flag becomes {@literal true} for backward scrolling keyset pagination. Any {@code AbstractNeo4jQuery} will in turn reverse the result list. */ private boolean requiresReverseSort = false; + @Nullable private Predicate projectingPropertyFilter; public void addMatchOn(PatternElement match) { @@ -78,6 +85,7 @@ public final class QueryFragments { this.condition = Optional.ofNullable(condition).orElse(Cypher.noCondition()); } + @Nullable public Condition getCondition() { return condition; } @@ -86,16 +94,20 @@ public final class QueryFragments { this.returnExpressions = expression; } - public void setDeleteExpression(Expression expression) { + public void setDeleteExpression(@Nullable Expression expression) { this.deleteExpression = expression; } - public void setReturnExpression(Expression returnExpression, boolean isScalarValue) { - this.returnExpressions = Collections.singletonList(returnExpression); - this.scalarValueReturn = isScalarValue; + public void setReturnExpression(@Nullable Expression returnExpression, boolean isScalarValue) { + if (returnExpression != null) { + this.returnExpressions = Collections.singletonList(returnExpression); + this.scalarValueReturn = isScalarValue; + } else { + this.returnExpressions = List.of(); + } } - public void setProjectingPropertyFilter(Predicate projectingPropertyFilter) { + public void setProjectingPropertyFilter(@Nullable Predicate projectingPropertyFilter) { this.projectingPropertyFilter = projectingPropertyFilter; } @@ -104,7 +116,7 @@ public final class QueryFragments { && (this.returnTuple == null || this.returnTuple.include(fieldName)); } - public void setOrderBy(Collection orderBy) { + public void setOrderBy(@Nullable Collection orderBy) { this.orderBy = orderBy; } @@ -131,14 +143,14 @@ public final class QueryFragments { public Statement toStatement() { - StatementBuilder.OngoingReadingWithoutWhere match = null; + if (this.matchOn.isEmpty()) { + throw new IllegalStateException("No pattern to match on"); + } + + StatementBuilder.OngoingReadingWithoutWhere match = Cypher.match(matchOn.get(0)); for (PatternElement patternElement : matchOn) { - if (match == null) { - match = Cypher.match(matchOn.get(0)); - } else { - match = match.match(patternElement); - } + match = match.match(patternElement); } StatementBuilder.OngoingReadingWithWhere matchWithWhere = match.where(condition); @@ -161,7 +173,7 @@ public final class QueryFragments { } private Collection getReturnExpressions() { - return returnExpressions.isEmpty() ? CypherGenerator.INSTANCE.createReturnStatementForMatch((Neo4jPersistentEntity) returnTuple.nodeDescription, + return returnExpressions.isEmpty() && returnTuple != null ? CypherGenerator.INSTANCE.createReturnStatementForMatch((Neo4jPersistentEntity) returnTuple.nodeDescription, this::includeField, returnTuple.additionalExpressions.toArray(Expression[]::new)) : returnExpressions; } @@ -170,7 +182,7 @@ public final class QueryFragments { } private boolean isDistinctReturn() { - return returnExpressions.isEmpty() && returnTuple.isDistinct; + return returnExpressions.isEmpty() && returnTuple != null && returnTuple.isDistinct; } public Collection getOrderBy() { @@ -203,10 +215,12 @@ public final class QueryFragments { return Cypher.sort(sortedExpression.get(), sortDirection.get()); } + @Nullable public Number getLimit() { return limit; } + @Nullable public Long getSkip() { return skip; } diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/QueryFragmentsAndParameters.java b/src/main/java/org/springframework/data/neo4j/repository/query/QueryFragmentsAndParameters.java index 736d72cac..4044de008 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/QueryFragmentsAndParameters.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/QueryFragmentsAndParameters.java @@ -16,6 +16,8 @@ package org.springframework.data.neo4j.repository.query; import org.apiguardian.api.API; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; import org.neo4j.cypherdsl.core.Condition; import org.neo4j.cypherdsl.core.Cypher; import org.neo4j.cypherdsl.core.Node; @@ -34,14 +36,15 @@ import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity; import org.springframework.data.neo4j.core.mapping.NodeDescription; import org.springframework.data.neo4j.core.mapping.PropertyFilter; -import org.springframework.lang.Nullable; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; +import java.util.function.Predicate; import static org.neo4j.cypherdsl.core.Cypher.parameter; @@ -55,13 +58,14 @@ import static org.neo4j.cypherdsl.core.Cypher.parameter; public final class QueryFragmentsAndParameters { private final static CypherGenerator cypherGenerator = CypherGenerator.INSTANCE; private Map parameters; + @Nullable private NodeDescription nodeDescription; private final QueryFragments queryFragments; + @Nullable private final String cypherQuery; private final Sort sort; - public QueryFragmentsAndParameters(NodeDescription nodeDescription, QueryFragments queryFragments, - @Nullable Map parameters, @Nullable Sort sort) { + public QueryFragmentsAndParameters(@Nullable NodeDescription nodeDescription, QueryFragments queryFragments, Map parameters, @Nullable Sort sort) { this.nodeDescription = nodeDescription; this.queryFragments = queryFragments; this.parameters = parameters; @@ -69,11 +73,11 @@ public final class QueryFragmentsAndParameters { this.sort = sort == null ? Sort.unsorted() : sort; } - public QueryFragmentsAndParameters(String cypherQuery) { - this(cypherQuery, null); + public QueryFragmentsAndParameters(@NonNull String cypherQuery) { + this(cypherQuery, Map.of()); } - public QueryFragmentsAndParameters(String cypherQuery, @Nullable Map parameters) { + public QueryFragmentsAndParameters(@NonNull String cypherQuery, Map parameters) { this.cypherQuery = cypherQuery; this.queryFragments = new QueryFragments(); this.parameters = parameters; @@ -88,10 +92,12 @@ public final class QueryFragmentsAndParameters { return queryFragments; } + @Nullable public String getCypherQuery() { return cypherQuery; } + @Nullable public NodeDescription getNodeDescription() { return nodeDescription; } @@ -110,10 +116,17 @@ public final class QueryFragmentsAndParameters { public static QueryFragmentsAndParameters forFindById(Neo4jPersistentEntity entityMetaData, Object idValues) { Map parameters = Collections.singletonMap(Constants.NAME_OF_ID, idValues); + QueryFragments queryFragments = forFindOrExistsById(entityMetaData); + queryFragments.setReturnExpressions(cypherGenerator.createReturnStatementForMatch(entityMetaData)); + return new QueryFragmentsAndParameters(entityMetaData, queryFragments, parameters, null); + } + + private static QueryFragments forFindOrExistsById(Neo4jPersistentEntity entityMetaData) { Node container = cypherGenerator.createRootNode(entityMetaData); Condition condition; - if (entityMetaData.getIdProperty().isComposite()) { - condition = CypherGenerator.INSTANCE.createCompositePropertyCondition(entityMetaData.getIdProperty(), container.getRequiredSymbolicName(), parameter(Constants.NAME_OF_ID)); + var idProperty = entityMetaData.getIdProperty(); + if (idProperty != null && idProperty.isComposite()) { + condition = CypherGenerator.INSTANCE.createCompositePropertyCondition(idProperty, container.getRequiredSymbolicName(), parameter(Constants.NAME_OF_ID)); } else { condition = entityMetaData.getIdExpression().isEqualTo(parameter(Constants.NAME_OF_ID)); } @@ -121,8 +134,7 @@ public final class QueryFragmentsAndParameters { QueryFragments queryFragments = new QueryFragments(); queryFragments.addMatchOn(container); queryFragments.setCondition(condition); - queryFragments.setReturnExpressions(cypherGenerator.createReturnStatementForMatch(entityMetaData)); - return new QueryFragmentsAndParameters(entityMetaData, queryFragments, parameters, null); + return queryFragments; } public static QueryFragmentsAndParameters forFindByAllId(Neo4jPersistentEntity entityMetaData, Object idValues) { @@ -130,9 +142,10 @@ public final class QueryFragmentsAndParameters { Node container = cypherGenerator.createRootNode(entityMetaData); Condition condition; - if (entityMetaData.getIdProperty().isComposite()) { + var idProperty = entityMetaData.getIdProperty(); + if (idProperty != null && idProperty.isComposite()) { List args = new ArrayList<>(); - for (String key : entityMetaData.getIdProperty().getOptionalConverter().write(null).keys()) { + for (String key : Objects.requireNonNull(idProperty.getOptionalConverter()).write(null).keys()) { args.add(key); args.add(container.property(key)); } @@ -153,29 +166,18 @@ public final class QueryFragmentsAndParameters { queryFragments.addMatchOn(cypherGenerator.createRootNode(entityMetaData)); queryFragments.setCondition(Cypher.noCondition()); queryFragments.setReturnExpressions(cypherGenerator.createReturnStatementForMatch(entityMetaData)); - return new QueryFragmentsAndParameters(entityMetaData, queryFragments, Collections.emptyMap(), null); + return new QueryFragmentsAndParameters(entityMetaData, queryFragments, Map.of(), null); } public static QueryFragmentsAndParameters forExistsById(Neo4jPersistentEntity entityMetaData, Object idValues) { Map parameters = Collections.singletonMap(Constants.NAME_OF_ID, idValues); - Node container = cypherGenerator.createRootNode(entityMetaData); - Condition condition; - if (entityMetaData.getIdProperty().isComposite()) { - condition = CypherGenerator.INSTANCE.createCompositePropertyCondition(entityMetaData.getIdProperty(), container.getRequiredSymbolicName(), parameter(Constants.NAME_OF_ID)); - } else { - condition = entityMetaData.getIdExpression().isEqualTo(parameter(Constants.NAME_OF_ID)); - } - - QueryFragments queryFragments = new QueryFragments(); - queryFragments.addMatchOn(container); - queryFragments.setCondition(condition); + QueryFragments queryFragments = forFindOrExistsById(entityMetaData); queryFragments.setReturnExpressions(cypherGenerator.createReturnStatementForExists(entityMetaData)); - return new QueryFragmentsAndParameters(entityMetaData, queryFragments, parameters, null); + return new QueryFragmentsAndParameters(entityMetaData, queryFragments, Objects.requireNonNullElseGet(parameters, Map::of), null); } - public static QueryFragmentsAndParameters forPageableAndSort(Neo4jPersistentEntity neo4jPersistentEntity, - @Nullable Pageable pageable, @Nullable Sort sort) { + public static QueryFragmentsAndParameters forPageableAndSort(Neo4jPersistentEntity neo4jPersistentEntity, @Nullable Pageable pageable, @Nullable Sort sort) { return getQueryFragmentsAndParameters(neo4jPersistentEntity, pageable, sort, null, null, null, Collections.emptyMap(), null, null, null); } @@ -187,32 +189,34 @@ public final class QueryFragmentsAndParameters { return forExample(mappingContext, example, null, null, null, null, null, null, null); } - static QueryFragmentsAndParameters forExampleWithPageable(Neo4jMappingContext mappingContext, Example example, Pageable pageable, @Nullable java.util.function.Predicate includeField) { + static QueryFragmentsAndParameters forExampleWithPageable(Neo4jMappingContext mappingContext, Example example, Pageable pageable, java.util.function.Predicate includeField) { return forExample(mappingContext, example, null, pageable, null, null, null, null, includeField); } - static QueryFragmentsAndParameters forExampleWithSort(Neo4jMappingContext mappingContext, Example example, Sort sort, @Nullable Integer limit, @Nullable java.util.function.Predicate includeField) { + static QueryFragmentsAndParameters forExampleWithSort(Neo4jMappingContext mappingContext, Example example, Sort sort, @Nullable Integer limit, java.util.function.Predicate includeField) { return forExample(mappingContext, example, null, null, sort, limit, null, null, includeField); } - static QueryFragmentsAndParameters forExampleWithScrollPosition(Neo4jMappingContext mappingContext, Example example, @Nullable Condition keysetScrollPositionCondition, Sort sort, @Nullable Integer limit, @Nullable Long skip, ScrollPosition scrollPosition, @Nullable java.util.function.Predicate includeField) { + static QueryFragmentsAndParameters forExampleWithScrollPosition(Neo4jMappingContext mappingContext, Example example, @Nullable Condition keysetScrollPositionCondition, Sort sort, Integer limit, Long skip, ScrollPosition scrollPosition, java.util.function.Predicate includeField) { return forExample(mappingContext, example, keysetScrollPositionCondition, null, sort, limit, skip, scrollPosition, includeField); } private static QueryFragmentsAndParameters forExample(Neo4jMappingContext mappingContext, Example example, - @Nullable Condition keysetScrollPositionCondition, - @Nullable Pageable pageable, - @Nullable Sort sort, - @Nullable Integer limit, - @Nullable Long skip, - @Nullable ScrollPosition scrollPosition, - @Nullable java.util.function.Predicate includeField) { + @Nullable Condition keysetScrollPositionCondition, + @Nullable Pageable pageable, + @Nullable Sort sort, + @Nullable Integer limit, + @Nullable Long skip, + @Nullable ScrollPosition scrollPosition, + @Nullable Predicate includeField + ) { - Predicate predicate = Predicate.create(mappingContext, example); + var predicate = org.springframework.data.neo4j.repository.query.Predicate.create(mappingContext, example); Map parameters = predicate.getParameters(); Set propertyPathWrappers = predicate.getPropertyPathWrappers(); Condition condition = predicate.getCondition(); - Neo4jPersistentEntity persistentEntity = mappingContext.getPersistentEntity(example.getProbeType()); + Neo4jPersistentEntity persistentEntity = Objects + .requireNonNull(mappingContext.getPersistentEntity(example.getProbeType()), () -> "Could not load persistent entity for probe type %s".formatted(example.getProbeType())); if (scrollPosition instanceof KeysetScrollPosition keysetScrollPosition) { if (!keysetScrollPosition.isInitial()) { @@ -244,13 +248,13 @@ public final class QueryFragmentsAndParameters { static QueryFragmentsAndParameters forConditionAndPageable(Neo4jPersistentEntity entityMetaData, Condition condition, Pageable pageable, - java.util.function.Predicate includeField) { + @Nullable Predicate includeField) { return forCondition(entityMetaData, condition, pageable, null, null, null, null, includeField); } static QueryFragmentsAndParameters forConditionAndSort(Neo4jPersistentEntity entityMetaData, Condition condition, Sort sort, @Nullable Integer limit, - @Nullable java.util.function.Predicate includeField) { + @Nullable Predicate includeField) { return forCondition(entityMetaData, condition, null, sort, null, limit, null, includeField); } @@ -263,9 +267,9 @@ public final class QueryFragmentsAndParameters { Condition condition, @Nullable Condition keysetCondition, ScrollPosition scrollPosition, - @Nullable Sort sort, + Sort sort, @Nullable Integer limit, - @Nullable java.util.function.Predicate includeField) { + @Nullable Predicate includeField) { long skip = 0L; @@ -278,7 +282,7 @@ public final class QueryFragmentsAndParameters { } if (scrollPosition instanceof KeysetScrollPosition keysetScrollPosition) { - if (!scrollPosition.isInitial()) { + if (!scrollPosition.isInitial() && keysetCondition != null) { condition = condition.and(keysetCondition); } QueryFragmentsAndParameters queryFragmentsAndParameters = getQueryFragmentsAndParameters(entityMetaData, null, @@ -293,16 +297,15 @@ public final class QueryFragmentsAndParameters { // Parameter re-ordering helper private static QueryFragmentsAndParameters forCondition(Neo4jPersistentEntity entityMetaData, - Condition condition, - @Nullable Pageable pageable, - @Nullable Sort sort, - @Nullable Collection sortItems, - @Nullable Integer limit, - @Nullable Long skip, - @Nullable java.util.function.Predicate includeField + @Nullable Condition condition, + @Nullable Pageable pageable, + @Nullable Sort sort, + @Nullable Collection sortItems, + @Nullable Integer limit, + @Nullable Long skip, + @Nullable Predicate includeField ) { - return getQueryFragmentsAndParameters(entityMetaData, pageable, sort, sortItems, limit, skip, Collections.emptyMap(), condition, includeField, null); } @@ -315,8 +318,9 @@ public final class QueryFragmentsAndParameters { @Nullable Long skip, @Nullable Map parameters, @Nullable Condition condition, - @Nullable java.util.function.Predicate includeField, - @Nullable Set propertyPathWrappers) { + @Nullable Predicate includeField, + @Nullable Set propertyPathWrappers + ) { QueryFragments queryFragments = new QueryFragments(); @@ -358,7 +362,7 @@ public final class QueryFragmentsAndParameters { } } - return new QueryFragmentsAndParameters(entityMetaData, queryFragments, parameters, sort); + return new QueryFragmentsAndParameters(entityMetaData, queryFragments, Objects.requireNonNullElseGet(parameters, Map::of), sort); } private static void adaptPageable( diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveCypherdslBasedQuery.java b/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveCypherdslBasedQuery.java index ac5ae6864..38d8b1c95 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveCypherdslBasedQuery.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveCypherdslBasedQuery.java @@ -22,6 +22,7 @@ import java.util.function.Function; import java.util.function.Supplier; import java.util.function.UnaryOperator; +import org.jspecify.annotations.Nullable; import org.neo4j.cypherdsl.core.Statement; import org.neo4j.driver.types.MapAccessor; import org.neo4j.driver.types.TypeSystem; @@ -30,7 +31,6 @@ import org.springframework.data.neo4j.core.ReactiveNeo4jOperations; import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; import org.springframework.data.neo4j.core.mapping.PropertyFilter; import org.springframework.data.projection.ProjectionFactory; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -60,9 +60,9 @@ final class ReactiveCypherdslBasedQuery extends AbstractReactiveNeo4jQuery { @Override protected PreparedQuery prepareQuery(Class returnedType, Collection includedProperties, - @Nullable Neo4jParameterAccessor parameterAccessor, Neo4jQueryType queryType, - @Nullable Supplier> mappingFunction, - @Nullable UnaryOperator limitModifier) { + Neo4jParameterAccessor parameterAccessor, @Nullable Neo4jQueryType queryType, + Supplier> mappingFunction, + UnaryOperator limitModifier) { Object[] parameters = parameterAccessor.getValues(); diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveFluentQueryByExample.java b/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveFluentQueryByExample.java index 708b4e02f..5a023c52e 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveFluentQueryByExample.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveFluentQueryByExample.java @@ -15,28 +15,28 @@ */ package org.springframework.data.neo4j.repository.query; -import org.neo4j.cypherdsl.core.Condition; -import org.springframework.data.domain.KeysetScrollPosition; -import org.springframework.data.domain.OffsetScrollPosition; -import org.springframework.data.domain.ScrollPosition; -import org.springframework.data.domain.Window; -import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - import java.util.Collection; import java.util.function.Function; import org.apiguardian.api.API; +import org.jspecify.annotations.Nullable; +import org.neo4j.cypherdsl.core.Condition; import org.springframework.data.domain.Example; +import org.springframework.data.domain.KeysetScrollPosition; +import org.springframework.data.domain.OffsetScrollPosition; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.ScrollPosition; import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Window; import org.springframework.data.neo4j.core.ReactiveFluentFindOperation; import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; +import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity; import org.springframework.data.repository.query.FluentQuery.ReactiveFluentQuery; import org.springframework.data.support.PageableExecutionUtils; -import org.springframework.lang.Nullable; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; /** * Immutable implementation of a {@link ReactiveFluentQuery}. All @@ -167,7 +167,7 @@ final class ReactiveFluentQueryByExample extends FluentQuerySupport imp @Override public Mono> scroll(ScrollPosition scrollPosition) { Class domainType = this.example.getProbeType(); - Neo4jPersistentEntity entity = mappingContext.getPersistentEntity(domainType); + Neo4jPersistentEntity entity = mappingContext.getRequiredPersistentEntity(domainType); var skip = scrollPosition.isInitial() ? 0 @@ -175,7 +175,7 @@ final class ReactiveFluentQueryByExample extends FluentQuerySupport imp : 0; Condition condition = scrollPosition instanceof KeysetScrollPosition keysetScrollPosition - ? CypherAdapterUtils.combineKeysetIntoCondition(mappingContext.getPersistentEntity(example.getProbeType()), keysetScrollPosition, sort, mappingContext.getConversionService()) + ? CypherAdapterUtils.combineKeysetIntoCondition(mappingContext.getRequiredPersistentEntity(example.getProbeType()), keysetScrollPosition, sort, mappingContext.getConversionService()) : null; return findOperation.find(domainType) diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveFluentQueryByPredicate.java b/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveFluentQueryByPredicate.java index 0a3ce6fe8..3d1828d7e 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveFluentQueryByPredicate.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveFluentQueryByPredicate.java @@ -15,6 +15,7 @@ */ package org.springframework.data.neo4j.repository.query; +import org.jspecify.annotations.Nullable; import org.springframework.data.domain.KeysetScrollPosition; import org.springframework.data.domain.ScrollPosition; import org.springframework.data.domain.Window; @@ -34,7 +35,6 @@ import org.springframework.data.neo4j.core.ReactiveFluentFindOperation; import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity; import org.springframework.data.repository.query.FluentQuery.ReactiveFluentQuery; import org.springframework.data.support.PageableExecutionUtils; -import org.springframework.lang.Nullable; import com.querydsl.core.types.Predicate; diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveNeo4jQueryMethod.java b/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveNeo4jQueryMethod.java index a1c8763fd..0b1be5f3a 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveNeo4jQueryMethod.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveNeo4jQueryMethod.java @@ -42,7 +42,9 @@ import org.springframework.util.ClassUtils; */ final class ReactiveNeo4jQueryMethod extends Neo4jQueryMethod { + @SuppressWarnings("rawtypes") private static final TypeInformation PAGE_TYPE = TypeInformation.of(Page.class); + @SuppressWarnings("rawtypes") private static final TypeInformation SLICE_TYPE = TypeInformation.of(Slice.class); private final Lazy isCollectionQuery; diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/ReactivePartTreeNeo4jQuery.java b/src/main/java/org/springframework/data/neo4j/repository/query/ReactivePartTreeNeo4jQuery.java index 87d3efadd..62f16cedc 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/ReactivePartTreeNeo4jQuery.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/ReactivePartTreeNeo4jQuery.java @@ -21,6 +21,7 @@ import java.util.function.BiFunction; import java.util.function.Supplier; import java.util.function.UnaryOperator; +import org.jspecify.annotations.Nullable; import org.neo4j.driver.types.MapAccessor; import org.neo4j.driver.types.TypeSystem; import org.springframework.data.neo4j.core.PreparedQuery; @@ -31,7 +32,6 @@ import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.query.parser.PartTree; import org.springframework.data.repository.query.parser.PartTree.OrPart; -import org.springframework.lang.Nullable; /** * Implementation of {@link RepositoryQuery} for derived finder methods. @@ -63,7 +63,7 @@ final class ReactivePartTreeNeo4jQuery extends AbstractReactiveNeo4jQuery { @Override protected PreparedQuery prepareQuery(Class returnedType, Collection includedProperties, Neo4jParameterAccessor parameterAccessor, @Nullable Neo4jQueryType queryType, - @Nullable Supplier> mappingFunction, @Nullable UnaryOperator limitModifier) { + Supplier> mappingFunction, UnaryOperator limitModifier) { CypherQueryCreator queryCreator = new CypherQueryCreator(mappingContext, queryMethod, getDomainType(queryMethod), Optional.ofNullable(queryType).orElseGet(() -> Neo4jQueryType.fromPartTree(tree)), tree, parameterAccessor, diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveQuerydslNeo4jPredicateExecutor.java b/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveQuerydslNeo4jPredicateExecutor.java index 4a0360de7..345decda7 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveQuerydslNeo4jPredicateExecutor.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveQuerydslNeo4jPredicateExecutor.java @@ -17,6 +17,7 @@ package org.springframework.data.neo4j.repository.query; import static org.neo4j.cypherdsl.core.Cypher.asterisk; +import org.jspecify.annotations.Nullable; import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -109,7 +110,7 @@ public final class ReactiveQuerydslNeo4jPredicateExecutor implements Reactive return doFindAll(Cypher.noCondition(), Arrays.asList(QuerydslNeo4jPredicateExecutor.toSortItems(orders))); } - private Flux doFindAll(Condition condition, Collection sortItems) { + private Flux doFindAll(Condition condition, @Nullable Collection sortItems) { return this.neo4jOperations.toExecutableQuery( this.metaData.getType(), QueryFragmentsAndParameters.forConditionAndSortItems(this.metaData, condition, diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveStringBasedNeo4jQuery.java b/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveStringBasedNeo4jQuery.java index 81a2b6c32..0d4978bd8 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveStringBasedNeo4jQuery.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveStringBasedNeo4jQuery.java @@ -23,6 +23,7 @@ import java.util.function.BiFunction; import java.util.function.Supplier; import java.util.function.UnaryOperator; +import org.jspecify.annotations.Nullable; import org.neo4j.driver.types.MapAccessor; import org.neo4j.driver.types.TypeSystem; import org.springframework.data.mapping.MappingException; @@ -36,7 +37,6 @@ import org.springframework.data.repository.query.Parameters; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.query.ValueExpressionDelegate; import org.springframework.data.repository.query.ValueExpressionQueryRewriter; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -123,8 +123,8 @@ final class ReactiveStringBasedNeo4jQuery extends AbstractReactiveNeo4jQuery { @Override protected PreparedQuery prepareQuery(Class returnedType, Collection includedProperties, Neo4jParameterAccessor parameterAccessor, - @Nullable Neo4jQueryType queryType, @Nullable Supplier> mappingFunction, - @Nullable UnaryOperator limitModifier) { + @Nullable Neo4jQueryType queryType, Supplier> mappingFunction, + UnaryOperator limitModifier) { Map boundParameters = bindParameters(parameterAccessor); QueryContext queryContext = new QueryContext(queryMethod.getRepositoryName() + "." + queryMethod.getName(), @@ -166,23 +166,4 @@ final class ReactiveStringBasedNeo4jQuery extends AbstractReactiveNeo4jQuery { return resolvedParameters; } - - /** - * @param index position of this parameter placeholder - * @param originalSpelExpression Not used for configuring parameter names atm. - * @return A new parameter name for the given index. - */ - private static String parameterNameSource(int index, @SuppressWarnings("unused") String originalSpelExpression) { - return "__SpEL__" + index; - } - - /** - * @param originalPrefix The prefix passed to the replacement source is either ':' or '?', so that isn't usable for - * Cypher templates and therefore ignored. - * @param parameterName name of the parameter - * @return The name of the parameter in its native Cypher form. - */ - private static String replacementSource(@SuppressWarnings("unused") String originalPrefix, String parameterName) { - return "$" + parameterName; - } } diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/StringBasedNeo4jQuery.java b/src/main/java/org/springframework/data/neo4j/repository/query/StringBasedNeo4jQuery.java index cb92e6518..652f6d9f3 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/StringBasedNeo4jQuery.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/StringBasedNeo4jQuery.java @@ -25,6 +25,7 @@ import java.util.function.Supplier; import java.util.function.UnaryOperator; import java.util.regex.Pattern; +import org.jspecify.annotations.Nullable; import org.neo4j.driver.types.MapAccessor; import org.neo4j.driver.types.TypeSystem; import org.springframework.data.domain.Pageable; @@ -39,7 +40,6 @@ import org.springframework.data.repository.query.Parameters; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.query.ValueExpressionDelegate; import org.springframework.data.repository.query.ValueExpressionQueryRewriter; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StringUtils; diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/package-info.java b/src/main/java/org/springframework/data/neo4j/repository/query/package-info.java index c70426963..47ba55d7f 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/package-info.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/package-info.java @@ -1,7 +1,7 @@ /** * A set of annotations for providing custom queries to repositories. */ -@NonNullApi +@NullMarked package org.springframework.data.neo4j.repository.query; -import org.springframework.lang.NonNullApi; +import org.jspecify.annotations.NullMarked; diff --git a/src/main/java/org/springframework/data/neo4j/repository/support/EntityAndGraphPropertyAccessingMethodInterceptor.java b/src/main/java/org/springframework/data/neo4j/repository/support/EntityAndGraphPropertyAccessingMethodInterceptor.java index ca0932616..9fbf51e81 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/support/EntityAndGraphPropertyAccessingMethodInterceptor.java +++ b/src/main/java/org/springframework/data/neo4j/repository/support/EntityAndGraphPropertyAccessingMethodInterceptor.java @@ -21,6 +21,7 @@ import java.util.concurrent.atomic.AtomicReference; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; +import org.jspecify.annotations.Nullable; import org.springframework.beans.BeanUtils; import org.springframework.beans.BeanWrapper; import org.springframework.beans.NotReadablePropertyException; @@ -30,7 +31,6 @@ import org.springframework.data.neo4j.core.mapping.PropertyHandlerSupport; import org.springframework.data.neo4j.core.schema.Property; import org.springframework.data.projection.MethodInterceptorFactory; import org.springframework.data.util.DirectFieldAccessFallbackBeanWrapper; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ReflectionUtils; @@ -63,9 +63,9 @@ final class EntityAndGraphPropertyAccessingMethodInterceptor implements MethodIn this.target = new GraphPropertyAndDirectFieldAccessFallbackBeanWrapper(target, ctx); } - @Nullable @Override - public Object invoke(@SuppressWarnings("null") MethodInvocation invocation) throws Throwable { + @Nullable + public Object invoke(MethodInvocation invocation) throws Throwable { Method method = invocation.getMethod(); @@ -113,6 +113,7 @@ final class EntityAndGraphPropertyAccessingMethodInterceptor implements MethodIn } @Override + @Nullable public Object getPropertyValue(String propertyName) { try { return super.getPropertyValue(propertyName); diff --git a/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jEvaluationContextExtension.java b/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jEvaluationContextExtension.java index 2644ae3ae..2be48094e 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jEvaluationContextExtension.java +++ b/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jEvaluationContextExtension.java @@ -47,13 +47,13 @@ public final class Neo4jEvaluationContextExtension implements EvaluationContextE public Map getFunctions() { Map functions = new HashMap<>(); functions.put(Neo4jSpelSupport.FUNCTION_ORDER_BY, new Function(ReflectionUtils - .findRequiredMethod(Neo4jSpelSupport.class, Neo4jSpelSupport.FUNCTION_ORDER_BY, Object.class))); + .getRequiredMethod(Neo4jSpelSupport.class, Neo4jSpelSupport.FUNCTION_ORDER_BY, Object.class))); functions.put(Neo4jSpelSupport.FUNCTION_LITERAL, new Function(ReflectionUtils - .findRequiredMethod(Neo4jSpelSupport.class, Neo4jSpelSupport.FUNCTION_LITERAL, Object.class))); + .getRequiredMethod(Neo4jSpelSupport.class, Neo4jSpelSupport.FUNCTION_LITERAL, Object.class))); functions.put(Neo4jSpelSupport.FUNCTION_ANY_OF, new Function(ReflectionUtils - .findRequiredMethod(Neo4jSpelSupport.class, Neo4jSpelSupport.FUNCTION_ANY_OF, Object.class))); + .getRequiredMethod(Neo4jSpelSupport.class, Neo4jSpelSupport.FUNCTION_ANY_OF, Object.class))); functions.put(Neo4jSpelSupport.FUNCTION_ALL_OF, new Function(ReflectionUtils - .findRequiredMethod(Neo4jSpelSupport.class, Neo4jSpelSupport.FUNCTION_ALL_OF, Object.class))); + .getRequiredMethod(Neo4jSpelSupport.class, Neo4jSpelSupport.FUNCTION_ALL_OF, Object.class))); return functions; } diff --git a/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactory.java b/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactory.java index 41243f6a6..bd7ff31a5 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactory.java +++ b/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactory.java @@ -17,6 +17,7 @@ package org.springframework.data.neo4j.repository.support; import java.util.Optional; +import org.jspecify.annotations.Nullable; import org.neo4j.cypherdsl.core.renderer.Configuration; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; @@ -133,7 +134,7 @@ final class Neo4jRepositoryFactory extends RepositoryFactorySupport { } @Override - protected Optional getQueryLookupStrategy(Key key, + protected Optional getQueryLookupStrategy(@Nullable Key key, ValueExpressionDelegate valueExpressionDelegate) { return Optional.of(new Neo4jQueryLookupStrategy(neo4jOperations, mappingContext, valueExpressionDelegate, cypherDSLConfiguration)); } diff --git a/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactoryBean.java b/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactoryBean.java index 73f01686c..0eb9290b7 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactoryBean.java +++ b/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactoryBean.java @@ -18,6 +18,8 @@ package org.springframework.data.neo4j.repository.support; import java.io.Serializable; import org.apiguardian.api.API; +import org.jspecify.annotations.Nullable; +import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.neo4j.core.Neo4jOperations; import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; import org.springframework.data.repository.Repository; @@ -30,8 +32,8 @@ import org.springframework.data.repository.core.support.TransactionalRepositoryF * * @author Michael J. Simons * @author Gerrit Meier - * @param the type of the repository - * @param type of the domain class to map + * @param the type of the repository + * @param type of the domain class to map * @param identifier type in the domain class * @since 6.0 */ @@ -39,8 +41,10 @@ import org.springframework.data.repository.core.support.TransactionalRepositoryF public final class Neo4jRepositoryFactoryBean, S, ID extends Serializable> extends TransactionalRepositoryFactoryBeanSupport { + @Nullable private Neo4jOperations neo4jOperations; + @Nullable private Neo4jMappingContext neo4jMappingContext; /** @@ -48,21 +52,25 @@ public final class Neo4jRepositoryFactoryBean, S, ID * * @param repositoryInterface must not be {@literal null}. */ - protected Neo4jRepositoryFactoryBean(Class repositoryInterface) { + Neo4jRepositoryFactoryBean(Class repositoryInterface) { super(repositoryInterface); } - public void setNeo4jOperations(Neo4jOperations neo4jOperations) { + public void setNeo4jOperations(@Nullable Neo4jOperations neo4jOperations) { this.neo4jOperations = neo4jOperations; } - public void setNeo4jMappingContext(Neo4jMappingContext neo4jMappingContext) { - super.setMappingContext(neo4jMappingContext); - this.neo4jMappingContext = neo4jMappingContext; + @Override + public void setMappingContext(MappingContext mappingContext) { + super.setMappingContext(mappingContext); + this.neo4jMappingContext = (Neo4jMappingContext) mappingContext; } @Override protected RepositoryFactorySupport doCreateRepositoryFactory() { + if (this.neo4jOperations == null || this.neo4jMappingContext == null) { + throw new IllegalStateException("Repository factory bean has not been configured properly, both Neo4j operations and mapping context are required"); + } return new Neo4jRepositoryFactory(neo4jOperations, neo4jMappingContext); } } diff --git a/src/main/java/org/springframework/data/neo4j/repository/support/ReactiveNeo4jRepositoryFactory.java b/src/main/java/org/springframework/data/neo4j/repository/support/ReactiveNeo4jRepositoryFactory.java index f56e70185..07430e456 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/support/ReactiveNeo4jRepositoryFactory.java +++ b/src/main/java/org/springframework/data/neo4j/repository/support/ReactiveNeo4jRepositoryFactory.java @@ -17,6 +17,7 @@ package org.springframework.data.neo4j.repository.support; import java.util.Optional; +import org.jspecify.annotations.Nullable; import org.neo4j.cypherdsl.core.renderer.Configuration; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; @@ -127,7 +128,7 @@ final class ReactiveNeo4jRepositoryFactory extends ReactiveRepositoryFactorySupp } - @Override protected Optional getQueryLookupStrategy(Key key, + @Override protected Optional getQueryLookupStrategy(@Nullable Key key, ValueExpressionDelegate valueExpressionDelegate) { return Optional .of(new ReactiveNeo4jQueryLookupStrategy(neo4jOperations, mappingContext, valueExpressionDelegate, cypherDSLConfiguration)); diff --git a/src/main/java/org/springframework/data/neo4j/repository/support/ReactiveNeo4jRepositoryFactoryBean.java b/src/main/java/org/springframework/data/neo4j/repository/support/ReactiveNeo4jRepositoryFactoryBean.java index ba503cf58..30d0f21ce 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/support/ReactiveNeo4jRepositoryFactoryBean.java +++ b/src/main/java/org/springframework/data/neo4j/repository/support/ReactiveNeo4jRepositoryFactoryBean.java @@ -18,6 +18,8 @@ package org.springframework.data.neo4j.repository.support; import java.io.Serializable; import org.apiguardian.api.API; +import org.jspecify.annotations.Nullable; +import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.neo4j.core.ReactiveNeo4jOperations; import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; import org.springframework.data.repository.Repository; @@ -39,8 +41,10 @@ import org.springframework.data.repository.core.support.TransactionalRepositoryF public final class ReactiveNeo4jRepositoryFactoryBean, S, ID extends Serializable> extends TransactionalRepositoryFactoryBeanSupport { + @Nullable private ReactiveNeo4jOperations neo4jOperations; + @Nullable private Neo4jMappingContext neo4jMappingContext; /** @@ -48,21 +52,25 @@ public final class ReactiveNeo4jRepositoryFactoryBean repositoryInterface) { + ReactiveNeo4jRepositoryFactoryBean(Class repositoryInterface) { super(repositoryInterface); } - public void setNeo4jOperations(ReactiveNeo4jOperations neo4jOperations) { + public void setNeo4jOperations(@Nullable ReactiveNeo4jOperations neo4jOperations) { this.neo4jOperations = neo4jOperations; } - public void setNeo4jMappingContext(Neo4jMappingContext neo4jMappingContext) { - super.setMappingContext(neo4jMappingContext); - this.neo4jMappingContext = neo4jMappingContext; + @Override + public void setMappingContext(MappingContext mappingContext) { + super.setMappingContext(mappingContext); + this.neo4jMappingContext = (Neo4jMappingContext) mappingContext; } @Override protected RepositoryFactorySupport doCreateRepositoryFactory() { + if (this.neo4jOperations == null || this.neo4jMappingContext == null) { + throw new IllegalStateException("Repository factory bean has not been configured properly, both Neo4j operations and mapping context are required"); + } return new ReactiveNeo4jRepositoryFactory(neo4jOperations, neo4jMappingContext); } } diff --git a/src/main/java/org/springframework/data/neo4j/repository/support/ReactivePersistenceExceptionTranslationInterceptor.java b/src/main/java/org/springframework/data/neo4j/repository/support/ReactivePersistenceExceptionTranslationInterceptor.java index f04016c6d..b03a7e535 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/support/ReactivePersistenceExceptionTranslationInterceptor.java +++ b/src/main/java/org/springframework/data/neo4j/repository/support/ReactivePersistenceExceptionTranslationInterceptor.java @@ -23,6 +23,7 @@ import java.util.function.Function; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.BeanFactoryUtils; import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.dao.DataAccessException; @@ -59,12 +60,14 @@ final class ReactivePersistenceExceptionTranslationInterceptor implements Method * * @param beanFactory the ListableBeanFactory to obtaining all PersistenceExceptionTranslators from */ + @SuppressWarnings("NullAway") ReactivePersistenceExceptionTranslationInterceptor(ListableBeanFactory beanFactory) { Assert.notNull(beanFactory, "ListableBeanFactory must not be null"); this.beanFactory = beanFactory; } @Override + @Nullable public Object invoke(MethodInvocation mi) throws Throwable { // Invoke the method potentially returning a reactive type diff --git a/src/main/java/org/springframework/data/neo4j/repository/support/ReactivePersistenceExceptionTranslationPostProcessor.java b/src/main/java/org/springframework/data/neo4j/repository/support/ReactivePersistenceExceptionTranslationPostProcessor.java index 39f7788ad..0c169cb09 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/support/ReactivePersistenceExceptionTranslationPostProcessor.java +++ b/src/main/java/org/springframework/data/neo4j/repository/support/ReactivePersistenceExceptionTranslationPostProcessor.java @@ -18,8 +18,10 @@ package org.springframework.data.neo4j.repository.support; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import java.io.Serial; import java.lang.annotation.Annotation; import java.lang.reflect.Method; +import java.util.Objects; import org.aopalliance.aop.Advice; import org.apiguardian.api.API; @@ -57,7 +59,9 @@ import org.springframework.util.Assert; public final class ReactivePersistenceExceptionTranslationPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessor { - private final Class repositoryAnnotationType; + @Serial + private static final long serialVersionUID = -8597336297033105680L; + private transient final Class repositoryAnnotationType; public ReactivePersistenceExceptionTranslationPostProcessor() { @@ -79,7 +83,7 @@ public final class ReactivePersistenceExceptionTranslationPostProcessor "Cannot use PersistenceExceptionTranslator autodetection without ListableBeanFactory"); } this.advisor = new ReactivePersistenceExceptionTranslationAdvisor((ListableBeanFactory) beanFactory, - this.repositoryAnnotationType); + Objects.requireNonNullElse(this.repositoryAnnotationType, Repository.class)); } /** @@ -88,9 +92,11 @@ public final class ReactivePersistenceExceptionTranslationPostProcessor */ static final class ReactivePersistenceExceptionTranslationAdvisor extends AbstractPointcutAdvisor { - private final ReactivePersistenceExceptionTranslationInterceptor advice; + @Serial + private static final long serialVersionUID = 849460320459940956L; + private transient final ReactivePersistenceExceptionTranslationInterceptor advice; - private final AnnotationMatchingPointcut pointcut; + private transient final AnnotationMatchingPointcut pointcut; /** * Create a new PersistenceExceptionTranslationAdvisor. diff --git a/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java b/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java index 295dc9efe..0f12bb30d 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java +++ b/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java @@ -16,6 +16,7 @@ package org.springframework.data.neo4j.repository.support; import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.function.LongSupplier; import java.util.stream.Collectors; @@ -34,7 +35,6 @@ import org.springframework.data.repository.PagingAndSortingRepository; import org.springframework.data.support.PageableExecutionUtils; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; -import org.springframework.util.Assert; /** * Repository base implementation for Neo4j. @@ -138,8 +138,7 @@ public class SimpleNeo4jRepository implements PagingAndSortingRepository< @Transactional public void delete(T entity) { - ID id = this.entityInformation.getId(entity); - Assert.notNull(id, "Cannot delete individual nodes without an id"); + ID id = Objects.requireNonNull(this.entityInformation.getId(entity), "Cannot delete individual nodes without an id"); if (entityMetaData.hasVersionProperty()) { Neo4jPersistentProperty versionProperty = entityMetaData.getRequiredVersionProperty(); Object versionValue = entityMetaData.getPropertyAccessor(entity).getProperty(versionProperty); diff --git a/src/main/java/org/springframework/data/neo4j/repository/support/SimpleReactiveNeo4jRepository.java b/src/main/java/org/springframework/data/neo4j/repository/support/SimpleReactiveNeo4jRepository.java index d2fa5f93c..acc884446 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/support/SimpleReactiveNeo4jRepository.java +++ b/src/main/java/org/springframework/data/neo4j/repository/support/SimpleReactiveNeo4jRepository.java @@ -20,6 +20,7 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.util.List; +import java.util.Objects; import java.util.stream.Collectors; import java.util.stream.StreamSupport; @@ -168,10 +169,9 @@ public class SimpleReactiveNeo4jRepository implements ReactiveSortingRepo @Override @Transactional public Mono delete(T entity) { - Assert.notNull(entity, "The given entity must not be null"); + Objects.requireNonNull(entity, "The given entity must not be null"); - ID id = this.entityInformation.getId(entity); - Assert.notNull(id, "Cannot delete individual nodes without an id"); + ID id = Objects.requireNonNull(this.entityInformation.getId(entity), "Cannot delete individual nodes without an id"); if (entityMetaData.hasVersionProperty()) { Neo4jPersistentProperty versionProperty = entityMetaData.getRequiredVersionProperty(); Object versionValue = entityMetaData.getPropertyAccessor(entity).getProperty(versionProperty); diff --git a/src/main/java/org/springframework/data/neo4j/repository/support/package-info.java b/src/main/java/org/springframework/data/neo4j/repository/support/package-info.java index edf18c9d4..a818be1ec 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/support/package-info.java +++ b/src/main/java/org/springframework/data/neo4j/repository/support/package-info.java @@ -4,7 +4,7 @@ repository base classes. The support classes are the same classes used by SDN itself. * */ -@NonNullApi +@NullMarked package org.springframework.data.neo4j.repository.support; -import org.springframework.lang.NonNullApi; +import org.jspecify.annotations.NullMarked; diff --git a/src/main/java/org/springframework/data/neo4j/types/CartesianPoint3d.java b/src/main/java/org/springframework/data/neo4j/types/CartesianPoint3d.java index 524c9e331..df28351ad 100644 --- a/src/main/java/org/springframework/data/neo4j/types/CartesianPoint3d.java +++ b/src/main/java/org/springframework/data/neo4j/types/CartesianPoint3d.java @@ -15,6 +15,8 @@ */ package org.springframework.data.neo4j.types; +import java.util.Objects; + import org.apiguardian.api.API; /** @@ -43,7 +45,7 @@ public final class CartesianPoint3d extends AbstractPoint { } public Double getZ() { - return coordinate.getZ(); + return Objects.requireNonNull(coordinate.getZ(), "The underlying coordinate does not have a z-value (height)"); } @Override diff --git a/src/main/java/org/springframework/data/neo4j/types/Coordinate.java b/src/main/java/org/springframework/data/neo4j/types/Coordinate.java index 2b8e76aca..56dadb9f6 100644 --- a/src/main/java/org/springframework/data/neo4j/types/Coordinate.java +++ b/src/main/java/org/springframework/data/neo4j/types/Coordinate.java @@ -17,7 +17,8 @@ package org.springframework.data.neo4j.types; import java.util.Objects; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + /** * @author Michael J. Simons @@ -27,6 +28,7 @@ public final class Coordinate { private final double y; + @Nullable private final Double z; public Coordinate(double x, double y) { @@ -47,7 +49,8 @@ public final class Coordinate { return y; } - @Nullable Double getZ() { + @Nullable + Double getZ() { return z; } diff --git a/src/main/java/org/springframework/data/neo4j/types/GeographicPoint3d.java b/src/main/java/org/springframework/data/neo4j/types/GeographicPoint3d.java index b80d4d64c..419419cff 100644 --- a/src/main/java/org/springframework/data/neo4j/types/GeographicPoint3d.java +++ b/src/main/java/org/springframework/data/neo4j/types/GeographicPoint3d.java @@ -15,6 +15,8 @@ */ package org.springframework.data.neo4j.types; +import java.util.Objects; + import org.apiguardian.api.API; /** @@ -41,7 +43,7 @@ public final class GeographicPoint3d extends AbstractPoint { } public double getHeight() { - return coordinate.getZ(); + return Objects.requireNonNull(coordinate.getZ(), "The underlying coordinate does not have a z-value (height)"); } @Override diff --git a/src/main/java/org/springframework/data/neo4j/types/package-info.java b/src/main/java/org/springframework/data/neo4j/types/package-info.java index 1bf1f8051..bf5280e9d 100644 --- a/src/main/java/org/springframework/data/neo4j/types/package-info.java +++ b/src/main/java/org/springframework/data/neo4j/types/package-info.java @@ -1,7 +1,7 @@ /** * Additional types provided by SDN. */ -@NonNullApi +@NullMarked package org.springframework.data.neo4j.types; -import org.springframework.lang.NonNullApi; +import org.jspecify.annotations.NullMarked; diff --git a/src/main/kotlin/org/springframework/data/neo4j/core/Neo4jClientExtensions.kt b/src/main/kotlin/org/springframework/data/neo4j/core/Neo4jClientExtensions.kt index f35cd8f23..8b9e7264e 100644 --- a/src/main/kotlin/org/springframework/data/neo4j/core/Neo4jClientExtensions.kt +++ b/src/main/kotlin/org/springframework/data/neo4j/core/Neo4jClientExtensions.kt @@ -34,7 +34,7 @@ fun Neo4jClient.UnboundRunnableSpec.inDatabase(targetDatabase: String): Neo4jCli * @author Michael J. Simons * @since 6.0 */ -fun Neo4jClient.OngoingDelegation.inDatabase(targetDatabase: String): Neo4jClient.RunnableDelegation = +fun Neo4jClient.OngoingDelegation.inDatabase(targetDatabase: String): Neo4jClient.RunnableDelegation = `in`(targetDatabase) /** diff --git a/src/main/kotlin/org/springframework/data/neo4j/core/ReactiveNeo4jClientExtensions.kt b/src/main/kotlin/org/springframework/data/neo4j/core/ReactiveNeo4jClientExtensions.kt index 4ff86259a..6f6abfc82 100644 --- a/src/main/kotlin/org/springframework/data/neo4j/core/ReactiveNeo4jClientExtensions.kt +++ b/src/main/kotlin/org/springframework/data/neo4j/core/ReactiveNeo4jClientExtensions.kt @@ -38,7 +38,7 @@ fun ReactiveNeo4jClient.UnboundRunnableSpec.inDatabase(targetDatabase: String): * @author Michael J. Simons * @since 6.0 */ -fun ReactiveNeo4jClient.OngoingDelegation.inDatabase(targetDatabase: String): +fun ReactiveNeo4jClient.OngoingDelegation.inDatabase(targetDatabase: String): ReactiveNeo4jClient.RunnableDelegation = `in`(targetDatabase) /** diff --git a/src/test/java/org/springframework/data/neo4j/core/Neo4jClientTest.java b/src/test/java/org/springframework/data/neo4j/core/Neo4jClientTest.java index fc8bb80ca..12bd446df 100644 --- a/src/test/java/org/springframework/data/neo4j/core/Neo4jClientTest.java +++ b/src/test/java/org/springframework/data/neo4j/core/Neo4jClientTest.java @@ -62,7 +62,6 @@ import org.neo4j.driver.Values; import org.neo4j.driver.summary.ResultSummary; import org.neo4j.driver.types.TypeSystem; import org.springframework.data.neo4j.core.transaction.Neo4jTransactionUtils; -import org.springframework.lang.Nullable; import org.springframework.util.ReflectionUtils; /** @@ -602,7 +601,7 @@ class Neo4jClientTest { } } - void verifyDatabaseSelection(@Nullable String targetDatabase) { + void verifyDatabaseSelection(String targetDatabase) { verify(driver).session(configArgumentCaptor.capture()); SessionConfig config = configArgumentCaptor.getValue(); @@ -614,7 +613,7 @@ class Neo4jClientTest { } } - void verifyUserSelection(@Nullable String aUser) { + void verifyUserSelection(String aUser) { verify(driver).session(configArgumentCaptor.capture()); SessionConfig config = configArgumentCaptor.getValue(); diff --git a/src/test/java/org/springframework/data/neo4j/core/ReactiveNeo4jClientTest.java b/src/test/java/org/springframework/data/neo4j/core/ReactiveNeo4jClientTest.java index a17963899..466d86e7a 100644 --- a/src/test/java/org/springframework/data/neo4j/core/ReactiveNeo4jClientTest.java +++ b/src/test/java/org/springframework/data/neo4j/core/ReactiveNeo4jClientTest.java @@ -34,7 +34,6 @@ import org.neo4j.driver.reactivestreams.ReactiveResult; import org.neo4j.driver.reactivestreams.ReactiveSession; import org.neo4j.driver.summary.ResultSummary; import org.springframework.data.neo4j.core.transaction.Neo4jTransactionUtils; -import org.springframework.lang.Nullable; import org.springframework.util.ReflectionUtils; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -549,7 +548,7 @@ class ReactiveNeo4jClientTest { verify(session).close(); } - void verifyDatabaseSelection(@Nullable String targetDatabase) { + void verifyDatabaseSelection(String targetDatabase) { verify(driver).session(eq(ReactiveSession.class), configArgumentCaptor.capture()); SessionConfig config = configArgumentCaptor.getValue(); @@ -561,7 +560,7 @@ class ReactiveNeo4jClientTest { } } - void verifyUserSelection(@Nullable String aUser) { + void verifyUserSelection(String aUser) { verify(driver).session(eq(ReactiveSession.class), configArgumentCaptor.capture()); SessionConfig config = configArgumentCaptor.getValue(); diff --git a/src/test/java/org/springframework/data/neo4j/integration/conversion_imperative/compose_as_ids/CompositeValue.java b/src/test/java/org/springframework/data/neo4j/integration/conversion_imperative/compose_as_ids/CompositeValue.java index 50c3b3e6f..c4bc56daa 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/conversion_imperative/compose_as_ids/CompositeValue.java +++ b/src/test/java/org/springframework/data/neo4j/integration/conversion_imperative/compose_as_ids/CompositeValue.java @@ -22,8 +22,6 @@ import org.neo4j.driver.Value; import org.neo4j.driver.Values; import org.springframework.data.neo4j.core.convert.Neo4jConversionService; import org.springframework.data.neo4j.core.convert.Neo4jPersistentPropertyToMapConverter; -import org.springframework.lang.NonNull; -import org.springframework.lang.Nullable; /** * @author Michael J. Simons @@ -34,9 +32,8 @@ public record CompositeValue(String value1, Integer value2) { static class Converter implements Neo4jPersistentPropertyToMapConverter { - @NonNull @Override - public Map decompose(@Nullable CompositeValue property, Neo4jConversionService conversionService) { + public Map decompose(CompositeValue property, Neo4jConversionService conversionService) { final HashMap decomposed = new HashMap<>(); if (property == null) { diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/SimpleDisplayNameGeneratorWithTags.java b/src/test/java/org/springframework/data/neo4j/integration/issues/SimpleDisplayNameGeneratorWithTags.java index 3d71c20f0..98df773ed 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/SimpleDisplayNameGeneratorWithTags.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/SimpleDisplayNameGeneratorWithTags.java @@ -16,6 +16,7 @@ package org.springframework.data.neo4j.integration.issues; import java.lang.reflect.Method; +import java.util.List; import java.util.stream.Collectors; import org.junit.jupiter.api.DisplayNameGenerator; @@ -31,7 +32,7 @@ import org.junit.platform.commons.util.AnnotationUtils; final class SimpleDisplayNameGeneratorWithTags extends DisplayNameGenerator.Simple { @Override - public String generateDisplayNameForMethod(Class testClass, Method testMethod) { + public String generateDisplayNameForMethod(List> enclosingInstanceTypes, Class testClass, Method testMethod) { var displayNameForMethod = testMethod.getName(); var tags = AnnotationUtils.findRepeatableAnnotations(testMethod, Tag.class); diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2168/UnrelatedObjectPropertyConverterAsBean.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2168/UnrelatedObjectPropertyConverterAsBean.java index 29b0d354c..c928a29a2 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2168/UnrelatedObjectPropertyConverterAsBean.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2168/UnrelatedObjectPropertyConverterAsBean.java @@ -15,10 +15,9 @@ */ package org.springframework.data.neo4j.integration.issues.gh2168; -import org.neo4j.driver.Driver; import org.neo4j.driver.Value; import org.neo4j.driver.Values; -import org.springframework.beans.factory.annotation.Autowired; +import org.neo4j.driver.types.TypeSystem; import org.springframework.data.neo4j.core.convert.Neo4jPersistentPropertyConverter; /** @@ -26,9 +25,6 @@ import org.springframework.data.neo4j.core.convert.Neo4jPersistentPropertyConver */ public class UnrelatedObjectPropertyConverterAsBean implements Neo4jPersistentPropertyConverter { - @Autowired - private Driver driver; - @Override public Value write(UnrelatedObject source) { @@ -38,7 +34,7 @@ public class UnrelatedObjectPropertyConverterAsBean implements Neo4jPersistentPr @Override public UnrelatedObject read(Value source) { - if (!driver.defaultTypeSystem().STRING().isTypeOf(source)) { + if (!TypeSystem.getDefault().STRING().isTypeOf(source)) { throw new IllegalArgumentException("Unsupported value"); } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2500/Group.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2500/Group.java index e248af032..ead722e4b 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2500/Group.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2500/Group.java @@ -21,7 +21,6 @@ import org.springframework.data.neo4j.core.schema.Id; import org.springframework.data.neo4j.core.schema.Node; import org.springframework.data.neo4j.core.schema.Relationship; import org.springframework.data.neo4j.core.support.UUIDStringGenerator; -import org.springframework.lang.NonNull; import java.util.LinkedHashSet; import java.util.Set; @@ -39,7 +38,6 @@ public class Group { @Version private Long version; - @NonNull private String name; @Relationship(type = "BELONGS_TO", direction = Relationship.Direction.INCOMING) @@ -81,7 +79,6 @@ public class Group { return this.version; } - @NonNull public String getName() { return this.name; } @@ -102,7 +99,7 @@ public class Group { this.version = version; } - public void setName(@NonNull String name) { + public void setName(String name) { this.name = name; } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/pure_element_id/ImperativeElementIdIT.java b/src/test/java/org/springframework/data/neo4j/integration/issues/pure_element_id/ImperativeElementIdIT.java index d5a35ecec..df041e247 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/pure_element_id/ImperativeElementIdIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/pure_element_id/ImperativeElementIdIT.java @@ -40,7 +40,6 @@ import org.springframework.data.neo4j.test.LogbackCapture; import org.springframework.data.neo4j.test.LogbackCapturingExtension; import org.springframework.data.neo4j.test.Neo4jImperativeTestConfiguration; import org.springframework.data.neo4j.test.Neo4jIntegrationTest; -import org.springframework.lang.NonNull; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; @@ -388,8 +387,7 @@ public class ImperativeElementIdIT extends AbstractElementIdTestBase { } @Override - @NonNull - public PlatformTransactionManager transactionManager(@NonNull Driver driver, @NonNull DatabaseSelectionProvider databaseNameProvider) { + public PlatformTransactionManager transactionManager(Driver driver, DatabaseSelectionProvider databaseNameProvider) { BookmarkCapture bookmarkCapture = bookmarkCapture(); return new Neo4jTransactionManager(driver, databaseNameProvider, @@ -397,7 +395,6 @@ public class ImperativeElementIdIT extends AbstractElementIdTestBase { } @Bean - @NonNull public Driver driver() { return neo4jConnectionSupport.getDriver(); diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/pure_element_id/ReactiveElementIdIT.java b/src/test/java/org/springframework/data/neo4j/integration/issues/pure_element_id/ReactiveElementIdIT.java index accc4e82e..d4e225a0c 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/pure_element_id/ReactiveElementIdIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/pure_element_id/ReactiveElementIdIT.java @@ -39,7 +39,6 @@ import org.springframework.data.neo4j.test.LogbackCapture; import org.springframework.data.neo4j.test.LogbackCapturingExtension; import org.springframework.data.neo4j.test.Neo4jIntegrationTest; import org.springframework.data.neo4j.test.Neo4jReactiveTestConfiguration; -import org.springframework.lang.NonNull; import org.springframework.transaction.ReactiveTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; import reactor.core.publisher.Mono; @@ -402,8 +401,7 @@ public class ReactiveElementIdIT extends AbstractElementIdTestBase { } @Override - @NonNull - public ReactiveTransactionManager reactiveTransactionManager(@NonNull Driver driver, @NonNull ReactiveDatabaseSelectionProvider databaseSelectionProvider) { + public ReactiveTransactionManager reactiveTransactionManager(Driver driver, ReactiveDatabaseSelectionProvider databaseSelectionProvider) { BookmarkCapture bookmarkCapture = bookmarkCapture(); return new ReactiveNeo4jTransactionManager(driver, databaseSelectionProvider, @@ -411,7 +409,6 @@ public class ReactiveElementIdIT extends AbstractElementIdTestBase { } @Bean - @NonNull public Driver driver() { return neo4jConnectionSupport.getDriver(); diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/conversion/ThingWithCompositeProperties.java b/src/test/java/org/springframework/data/neo4j/integration/shared/conversion/ThingWithCompositeProperties.java index 436c51ec7..6ff313e9d 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/conversion/ThingWithCompositeProperties.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/conversion/ThingWithCompositeProperties.java @@ -31,8 +31,6 @@ import org.springframework.data.neo4j.core.schema.GeneratedValue; import org.springframework.data.neo4j.core.schema.Id; import org.springframework.data.neo4j.core.schema.Node; import org.springframework.data.neo4j.core.schema.Relationship; -import org.springframework.lang.NonNull; -import org.springframework.lang.Nullable; /** * Test class verifying composite properties behaviour. @@ -234,8 +232,8 @@ public class ThingWithCompositeProperties { */ static class SomeOtherDTOToMapConverter implements Neo4jPersistentPropertyToMapConverter { - @NonNull @Override - public Map decompose(@Nullable SomeOtherDTO property, Neo4jConversionService conversionService) { + @Override + public Map decompose(SomeOtherDTO property, Neo4jConversionService conversionService) { final HashMap decomposed = new HashMap<>(); if (property == null) { diff --git a/src/test/java/org/springframework/data/neo4j/test/Neo4jExtension.java b/src/test/java/org/springframework/data/neo4j/test/Neo4jExtension.java index f1710f460..8db270a01 100644 --- a/src/test/java/org/springframework/data/neo4j/test/Neo4jExtension.java +++ b/src/test/java/org/springframework/data/neo4j/test/Neo4jExtension.java @@ -39,7 +39,6 @@ import org.neo4j.driver.internal.SecuritySettings; import org.neo4j.driver.internal.security.SecurityPlan; import org.neo4j.driver.internal.security.SecurityPlans; import org.springframework.core.log.LogMessage; -import org.springframework.lang.Nullable; import org.testcontainers.containers.Neo4jContainer; import org.testcontainers.utility.TestcontainersConfiguration; @@ -234,7 +233,7 @@ public class Neo4jExtension implements BeforeAllCallback, BeforeEachCallback { * @param driver The driver that should be checked for usability * @return true if the driver is currently usable. */ - private static boolean isUsable(@Nullable Driver driver) { + private static boolean isUsable(Driver driver) { if (driver == null) { return false; diff --git a/src/test/kotlin/org/springframework/data/neo4j/integration/imperative/ImmutableRelationshipsIT.kt b/src/test/kotlin/org/springframework/data/neo4j/integration/imperative/ImmutableRelationshipsIT.kt index 23def98b7..09876ede2 100644 --- a/src/test/kotlin/org/springframework/data/neo4j/integration/imperative/ImmutableRelationshipsIT.kt +++ b/src/test/kotlin/org/springframework/data/neo4j/integration/imperative/ImmutableRelationshipsIT.kt @@ -132,9 +132,9 @@ class ImmutableRelationshipsIT @Autowired constructor( } @Bean - override fun transactionManager(driver: Driver, databaseNameProvider: DatabaseSelectionProvider): PlatformTransactionManager { + override fun transactionManager(driver: Driver, databaseNameProvider: DatabaseSelectionProvider?): PlatformTransactionManager { val bookmarkCapture = bookmarkCapture() - return Neo4jTransactionManager(driver, databaseNameProvider, Neo4jBookmarkManager.create(bookmarkCapture)) + return Neo4jTransactionManager(driver, databaseNameProvider!!, Neo4jBookmarkManager.create(bookmarkCapture)) } override fun isCypher5Compatible(): Boolean { diff --git a/src/test/kotlin/org/springframework/data/neo4j/integration/imperative/KotlinInheritanceIT.kt b/src/test/kotlin/org/springframework/data/neo4j/integration/imperative/KotlinInheritanceIT.kt index 96b208d4b..232662196 100644 --- a/src/test/kotlin/org/springframework/data/neo4j/integration/imperative/KotlinInheritanceIT.kt +++ b/src/test/kotlin/org/springframework/data/neo4j/integration/imperative/KotlinInheritanceIT.kt @@ -234,9 +234,9 @@ class KotlinInheritanceIT @Autowired constructor( } @Bean - override fun transactionManager(driver: Driver, databaseNameProvider: DatabaseSelectionProvider): PlatformTransactionManager { + override fun transactionManager(driver: Driver, databaseNameProvider: DatabaseSelectionProvider?): PlatformTransactionManager { val bookmarkCapture = bookmarkCapture() - return Neo4jTransactionManager(driver, databaseNameProvider, Neo4jBookmarkManager.create(bookmarkCapture)) + return Neo4jTransactionManager(driver, databaseNameProvider!!, Neo4jBookmarkManager.create(bookmarkCapture)) } @Bean diff --git a/src/test/kotlin/org/springframework/data/neo4j/integration/imperative/KotlinProjectionIT.kt b/src/test/kotlin/org/springframework/data/neo4j/integration/imperative/KotlinProjectionIT.kt index 414850560..df7c86fcf 100644 --- a/src/test/kotlin/org/springframework/data/neo4j/integration/imperative/KotlinProjectionIT.kt +++ b/src/test/kotlin/org/springframework/data/neo4j/integration/imperative/KotlinProjectionIT.kt @@ -119,9 +119,9 @@ internal class KotlinProjectionIT { } @Bean - override fun transactionManager(driver: Driver, databaseNameProvider: DatabaseSelectionProvider): PlatformTransactionManager { + override fun transactionManager(driver: Driver, databaseNameProvider: DatabaseSelectionProvider?): PlatformTransactionManager { val bookmarkCapture = bookmarkCapture() - return Neo4jTransactionManager(driver, databaseNameProvider, Neo4jBookmarkManager.create(bookmarkCapture)) + return Neo4jTransactionManager(driver, databaseNameProvider!!, Neo4jBookmarkManager.create(bookmarkCapture)) } /** diff --git a/src/test/kotlin/org/springframework/data/neo4j/integration/imperative/Neo4jListContainsTest.kt b/src/test/kotlin/org/springframework/data/neo4j/integration/imperative/Neo4jListContainsTest.kt index 8c679a328..fd96bd28b 100644 --- a/src/test/kotlin/org/springframework/data/neo4j/integration/imperative/Neo4jListContainsTest.kt +++ b/src/test/kotlin/org/springframework/data/neo4j/integration/imperative/Neo4jListContainsTest.kt @@ -90,9 +90,9 @@ class Neo4jListContainsTest { } @Bean - override fun transactionManager(driver: Driver, databaseNameProvider: DatabaseSelectionProvider): PlatformTransactionManager { + override fun transactionManager(driver: Driver, databaseNameProvider: DatabaseSelectionProvider?): PlatformTransactionManager { val bookmarkCapture = bookmarkCapture() - return Neo4jTransactionManager(driver, databaseNameProvider, Neo4jBookmarkManager.create(bookmarkCapture)) + return Neo4jTransactionManager(driver, databaseNameProvider!!, Neo4jBookmarkManager.create(bookmarkCapture)) } } } diff --git a/src/test/kotlin/org/springframework/data/neo4j/integration/k/KotlinIssuesIT.kt b/src/test/kotlin/org/springframework/data/neo4j/integration/k/KotlinIssuesIT.kt index 4cf492aad..857a47f21 100644 --- a/src/test/kotlin/org/springframework/data/neo4j/integration/k/KotlinIssuesIT.kt +++ b/src/test/kotlin/org/springframework/data/neo4j/integration/k/KotlinIssuesIT.kt @@ -102,9 +102,9 @@ internal class KotlinIssuesIT { } @Bean - override fun transactionManager(driver: Driver, databaseNameProvider: DatabaseSelectionProvider): PlatformTransactionManager { + override fun transactionManager(driver: Driver, databaseNameProvider: DatabaseSelectionProvider?): PlatformTransactionManager { val bookmarkCapture = bookmarkCapture() - return Neo4jTransactionManager(driver, databaseNameProvider, Neo4jBookmarkManager.create(bookmarkCapture)) + return Neo4jTransactionManager(driver, databaseNameProvider!!, Neo4jBookmarkManager.create(bookmarkCapture)) } @Bean