Skip to content

Commit b54fb73

Browse files
GH-2544 - Check for empty collections in saveAllAs methods.
When an empty collection is passed to imperative and reactive `saveAllAs` variants a common element type can't be computed. We did not check for this scenario correctly and failed with a NPE. The introduction of a marker class fixes #2544 Co-authored-by: Michael Simons <[email protected]>
1 parent bc54125 commit b54fb73

File tree

6 files changed

+79
-7
lines changed

6 files changed

+79
-7
lines changed

src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@
9999
* @author Michael J. Simons
100100
* @author Philipp Tölle
101101
* @author Gerrit Meier
102+
* @author Corey Beres
102103
* @soundtrack Motörhead - We Are Motörhead
103104
* @since 6.0
104105
*/
@@ -550,6 +551,14 @@ public <T, R> List<R> saveAllAs(Iterable<T> instances, Class<R> resultType) {
550551

551552
Class<?> commonElementType = TemplateSupport.findCommonElementType(instances);
552553

554+
if (commonElementType == null) {
555+
throw new IllegalArgumentException("Could not determine a common element of an heterogeneous collection.");
556+
}
557+
558+
if (commonElementType == TemplateSupport.EmptyIterable.class) {
559+
return Collections.emptyList();
560+
}
561+
553562
if (resultType.isAssignableFrom(commonElementType)) {
554563
@SuppressWarnings("unchecked") // Nicer to live with this than streaming, mapping and collecting to avoid the cast. It's easier on the reactive side.
555564
List<R> saveElements = (List<R>) saveAll(instances);

src/main/java/org/springframework/data/neo4j/core/ReactiveNeo4jTemplate.java

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,6 @@
1919
import static org.neo4j.cypherdsl.core.Cypher.asterisk;
2020
import static org.neo4j.cypherdsl.core.Cypher.parameter;
2121

22-
import org.neo4j.cypherdsl.core.renderer.Configuration;
23-
import org.springframework.data.mapping.Association;
24-
import org.springframework.data.neo4j.core.TemplateSupport.FilteredBinderFunction;
25-
import org.springframework.data.neo4j.core.mapping.AssociationHandlerSupport;
26-
import org.springframework.data.neo4j.core.schema.TargetNode;
2722
import reactor.core.publisher.Flux;
2823
import reactor.core.publisher.Mono;
2924
import reactor.util.function.Tuple2;
@@ -55,6 +50,7 @@
5550
import org.neo4j.cypherdsl.core.Functions;
5651
import org.neo4j.cypherdsl.core.Node;
5752
import org.neo4j.cypherdsl.core.Statement;
53+
import org.neo4j.cypherdsl.core.renderer.Configuration;
5854
import org.neo4j.cypherdsl.core.renderer.Renderer;
5955
import org.neo4j.driver.Value;
6056
import org.neo4j.driver.types.Entity;
@@ -68,10 +64,13 @@
6864
import org.springframework.core.log.LogAccessor;
6965
import org.springframework.dao.IncorrectResultSizeDataAccessException;
7066
import org.springframework.dao.OptimisticLockingFailureException;
67+
import org.springframework.data.mapping.Association;
7168
import org.springframework.data.mapping.PersistentPropertyAccessor;
7269
import org.springframework.data.mapping.PropertyPath;
7370
import org.springframework.data.mapping.callback.ReactiveEntityCallbacks;
71+
import org.springframework.data.neo4j.core.TemplateSupport.FilteredBinderFunction;
7472
import org.springframework.data.neo4j.core.TemplateSupport.NodesAndRelationshipsByIdStatementProvider;
73+
import org.springframework.data.neo4j.core.mapping.AssociationHandlerSupport;
7574
import org.springframework.data.neo4j.core.mapping.Constants;
7675
import org.springframework.data.neo4j.core.mapping.CreateRelationshipStatementHolder;
7776
import org.springframework.data.neo4j.core.mapping.CypherGenerator;
@@ -89,6 +88,7 @@
8988
import org.springframework.data.neo4j.core.mapping.PropertyFilter;
9089
import org.springframework.data.neo4j.core.mapping.RelationshipDescription;
9190
import org.springframework.data.neo4j.core.mapping.callback.ReactiveEventSupport;
91+
import org.springframework.data.neo4j.core.schema.TargetNode;
9292
import org.springframework.data.neo4j.repository.query.QueryFragments;
9393
import org.springframework.data.neo4j.repository.query.QueryFragmentsAndParameters;
9494
import org.springframework.data.projection.ProjectionFactory;
@@ -493,6 +493,15 @@ public <T, R> Flux<R> saveAllAs(Iterable<T> instances, Class<R> resultType) {
493493

494494
Class<?> commonElementType = TemplateSupport.findCommonElementType(instances);
495495

496+
if (commonElementType == null) {
497+
return Flux.error(() -> new IllegalArgumentException(
498+
"Could not determine a common element of an heterogeneous collection."));
499+
}
500+
501+
if (commonElementType == TemplateSupport.EmptyIterable.class) {
502+
return Flux.empty();
503+
}
504+
496505
if (resultType.isAssignableFrom(commonElementType)) {
497506
return saveAll(instances).map(resultType::cast);
498507
}

src/main/java/org/springframework/data/neo4j/core/TemplateSupport.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,14 @@
6464
@API(status = API.Status.INTERNAL, since = "6.0.9")
6565
public final class TemplateSupport {
6666

67+
/**
68+
* Indicator for an empty collection
69+
*/
70+
public static final class EmptyIterable {
71+
private EmptyIterable() {
72+
}
73+
}
74+
6775
enum FetchType {
6876

6977
ONE,
@@ -81,6 +89,10 @@ public static Class<?> findCommonElementType(Iterable<?> collection) {
8189
.filter(o -> o != null)
8290
.map(Object::getClass).collect(Collectors.toSet());
8391

92+
if (allClasses.isEmpty()) {
93+
return EmptyIterable.class;
94+
}
95+
8496
Class<?> candidate = null;
8597
for (Class<?> type : allClasses) {
8698
if (candidate == null) {

src/test/java/org/springframework/data/neo4j/core/TemplateSupportTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ void shouldNotFailWithNullInput() {
8888
void shouldNotFailWithEmptyInput() {
8989

9090
Class<?> type = TemplateSupport.findCommonElementType(Collections.emptyList());
91-
assertThat(type).isNull();
91+
assertThat(type).isEqualTo(TemplateSupport.EmptyIterable.class);
9292
}
9393

9494
@Test

src/test/java/org/springframework/data/neo4j/integration/imperative/Neo4jTemplateIT.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,11 @@
1717

1818
import static org.assertj.core.api.Assertions.assertThat;
1919
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
20+
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
2021

2122
import lombok.Data;
2223

24+
import java.util.ArrayList;
2325
import java.util.Arrays;
2426
import java.util.Collection;
2527
import java.util.Collections;
@@ -76,6 +78,7 @@
7678
* @author Gerrit Meier
7779
* @author Michael J. Simons
7880
* @author Rosetta Roberts
81+
* @author Corey Beres
7982
*/
8083
@Neo4jIntegrationTest
8184
class Neo4jTemplateIT {
@@ -795,6 +798,24 @@ void saveAllAsWithClosedProjectionShouldWork() {
795798
assertThat(people).allMatch(p -> p.getAddress() != null);
796799
}
797800

801+
@Test // GH-2544
802+
void saveAllAsWithEmptyList() {
803+
List<ClosedProjection> projections = neo4jTemplate.saveAllAs(Collections.emptyList(), ClosedProjection.class);
804+
805+
assertThat(projections).isEmpty();
806+
}
807+
808+
@Test // GH-2544
809+
void saveWeirdHierarchy() {
810+
811+
List<Object> things = new ArrayList<>();
812+
things.add(1);
813+
things.add("eins");
814+
815+
assertThatIllegalArgumentException().isThrownBy(() -> neo4jTemplate.saveAllAs(things, ClosedProjection.class))
816+
.withMessage("Could not determine a common element of an heterogeneous collection.");
817+
}
818+
798819
@Test
799820
void updatingFindShouldWork() {
800821
Map<String, Object> params = new HashMap<>();

src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveNeo4jTemplateIT.java

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,10 @@
1919
import static org.neo4j.cypherdsl.core.Cypher.parameter;
2020

2121
import lombok.Data;
22-
import org.springframework.data.neo4j.test.Neo4jReactiveTestConfiguration;
2322
import reactor.core.publisher.Flux;
2423
import reactor.test.StepVerifier;
2524

25+
import java.util.ArrayList;
2626
import java.util.Arrays;
2727
import java.util.Collection;
2828
import java.util.Collections;
@@ -71,6 +71,7 @@
7171
import org.springframework.data.neo4j.test.Neo4jExtension;
7272
import org.springframework.data.neo4j.test.Neo4jExtension.Neo4jConnectionSupport;
7373
import org.springframework.data.neo4j.test.Neo4jIntegrationTest;
74+
import org.springframework.data.neo4j.test.Neo4jReactiveTestConfiguration;
7475
import org.springframework.transaction.ReactiveTransactionManager;
7576
import org.springframework.transaction.annotation.EnableTransactionManagement;
7677

@@ -688,6 +689,26 @@ void saveAsWithClosedProjectionOnThreeLevelShouldWork(@Autowired ReactiveNeo4jTe
688689
.verifyComplete();
689690
}
690691

692+
@Test // GH-2544
693+
void saveAllAsWithEmptyList(@Autowired ReactiveNeo4jTemplate template) {
694+
695+
template.saveAllAs(Collections.emptyList(), ClosedProjection.class)
696+
.as(StepVerifier::create)
697+
.verifyComplete();
698+
}
699+
700+
@Test // GH-2544
701+
void saveWeirdHierarchy(@Autowired ReactiveNeo4jTemplate template) {
702+
703+
List<Object> things = new ArrayList<>();
704+
things.add(1);
705+
things.add("eins");
706+
707+
template.saveAllAs(things, ClosedProjection.class)
708+
.as(StepVerifier::create)
709+
.verifyErrorMatches(t -> t instanceof IllegalArgumentException && t.getMessage().equals("Could not determine a common element of an heterogeneous collection."));
710+
}
711+
691712
@Test
692713
void saveAllAsWithClosedProjectionShouldWork(@Autowired ReactiveNeo4jTemplate template) {
693714

0 commit comments

Comments
 (0)