Skip to content

Commit 97bae7a

Browse files
committed
DATAREST-976 - Embedded properties are now considered in sort property paths.
We now allow sorting by properties of embedded objects. An embedded object is not linkable to a root object but embedded in the resource itself. If any part of the sort property path points to a linkable association, the whole sort property path is discarded silently and not used for sorting any further.
1 parent 3c6f311 commit 97bae7a

File tree

7 files changed

+70
-20
lines changed

7 files changed

+70
-20
lines changed

spring-data-rest-tests/spring-data-rest-tests-jpa/src/main/java/org/springframework/data/rest/webmvc/jpa/Book.java

+21-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2013-2016 the original author or authors.
2+
* Copyright 2013-2017 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -15,10 +15,15 @@
1515
*/
1616
package org.springframework.data.rest.webmvc.jpa;
1717

18+
import lombok.AllArgsConstructor;
19+
import lombok.Getter;
20+
import lombok.NoArgsConstructor;
21+
1822
import java.util.HashSet;
1923
import java.util.Set;
2024

2125
import javax.persistence.CascadeType;
26+
import javax.persistence.Embeddable;
2227
import javax.persistence.Entity;
2328
import javax.persistence.GeneratedValue;
2429
import javax.persistence.Id;
@@ -45,9 +50,11 @@ public class Book {
4550
@RestResource(path = "creators") //
4651
public Set<Author> authors;
4752

53+
public Offer offer;
54+
4855
protected Book() {}
4956

50-
public Book(String isbn, String title, long soldUnits, Iterable<Author> authors) {
57+
public Book(String isbn, String title, long soldUnits, Iterable<Author> authors, Offer offer) {
5158

5259
this.isbn = isbn;
5360
this.title = title;
@@ -59,5 +66,17 @@ public Book(String isbn, String title, long soldUnits, Iterable<Author> authors)
5966
author.books.add(this);
6067
this.authors.add(author);
6168
}
69+
70+
this.offer = offer;
71+
}
72+
73+
@Getter
74+
@Embeddable
75+
@AllArgsConstructor
76+
@NoArgsConstructor
77+
static class Offer {
78+
79+
double price;
80+
String currency;
6281
}
6382
}

spring-data-rest-tests/spring-data-rest-tests-jpa/src/test/java/org/springframework/data/rest/webmvc/jpa/JpaWebTests.java

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2013-2016 the original author or authors.
2+
* Copyright 2013-2017 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -558,12 +558,12 @@ public void exectuesSearchThatTakesASort() throws Exception {
558558
assertThat(findBySortedLink.getVariableNames(), hasItems("sort", "projection"));
559559

560560
// Assert results returned as specified
561-
client.follow(findBySortedLink.expand("title,desc")).//
561+
client.follow(findBySortedLink.expand("offer.price,desc")).//
562562
andExpect(jsonPath("$._embedded.books[0].title").value("Spring Data (Second Edition)")).//
563563
andExpect(jsonPath("$._embedded.books[1].title").value("Spring Data")).//
564564
andExpect(client.hasLinkWithRel("self"));
565565

566-
client.follow(findBySortedLink.expand("title,asc")).//
566+
client.follow(findBySortedLink.expand("offer.price,asc")).//
567567
andExpect(jsonPath("$._embedded.books[0].title").value("Spring Data")).//
568568
andExpect(jsonPath("$._embedded.books[1].title").value("Spring Data (Second Edition)")).//
569569
andExpect(client.hasLinkWithRel("self"));

spring-data-rest-tests/spring-data-rest-tests-jpa/src/test/java/org/springframework/data/rest/webmvc/jpa/TestDataPopulator.java

+4-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2013-2016 the original author or authors.
2+
* Copyright 2013-2017 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -18,6 +18,7 @@
1818
import java.util.Arrays;
1919

2020
import org.springframework.beans.factory.annotation.Autowired;
21+
import org.springframework.data.rest.webmvc.jpa.Book.Offer;
2122

2223
/**
2324
* @author Jon Brisbin
@@ -54,8 +55,8 @@ private void populateAuthorsAndBooks() {
5455

5556
Iterable<Author> authors = this.authors.save(Arrays.asList(ollie, mark, michael, david, john, thomas));
5657

57-
books.save(new Book("1449323952", "Spring Data", 1000, authors));
58-
books.save(new Book("1449323953", "Spring Data (Second Edition)", 2000, authors));
58+
books.save(new Book("1449323952", "Spring Data", 1000, authors, new Offer(21.21, "EUR")));
59+
books.save(new Book("1449323953", "Spring Data (Second Edition)", 2000, authors, new Offer(30.99, "EUR")));
5960
}
6061

6162
private void populateOrders() {

spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvcConfiguration.java

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2016 the original author or authors.
2+
* Copyright 2012-2017 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -805,7 +805,8 @@ protected List<HandlerMethodArgumentResolver> defaultMethodArgumentResolvers() {
805805
PageableHandlerMethodArgumentResolver pageableResolver = pageableResolver();
806806

807807
JacksonMappingAwareSortTranslator sortTranslator = new JacksonMappingAwareSortTranslator(objectMapper(),
808-
repositories(), DomainClassResolver.of(repositories(), resourceMappings(), baseUri()), persistentEntities());
808+
repositories(), DomainClassResolver.of(repositories(), resourceMappings(), baseUri()), persistentEntities(),
809+
associationLinks());
809810

810811
HandlerMethodArgumentResolver sortResolver = new MappingAwareSortArgumentResolver(sortTranslator, sortResolver());
811812
HandlerMethodArgumentResolver jacksonPageableResolver = new MappingAwarePageableArgumentResolver(sortTranslator,

spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/JacksonMappingAwareSortTranslator.java

+8-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2016 the original author or authors.
2+
* Copyright 2016-2017 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -31,6 +31,7 @@
3131
import org.springframework.data.mapping.PersistentProperty;
3232
import org.springframework.data.mapping.context.PersistentEntities;
3333
import org.springframework.data.repository.support.Repositories;
34+
import org.springframework.data.rest.webmvc.mapping.Associations;
3435
import org.springframework.data.rest.webmvc.support.DomainClassResolver;
3536
import org.springframework.util.Assert;
3637
import org.springframework.util.StringUtils;
@@ -62,16 +63,18 @@ public class JacksonMappingAwareSortTranslator {
6263
* @param repositories must not be {@literal null}.
6364
* @param domainClassResolver must not be {@literal null}.
6465
* @param persistentEntities must not be {@literal null}.
66+
* @param associations must not be {@literal null}.
6567
*/
6668
public JacksonMappingAwareSortTranslator(ObjectMapper objectMapper, Repositories repositories,
67-
DomainClassResolver domainClassResolver, PersistentEntities persistentEntities) {
69+
DomainClassResolver domainClassResolver, PersistentEntities persistentEntities, Associations associations) {
6870

6971
Assert.notNull(repositories, "Repositories must not be null!");
7072
Assert.notNull(domainClassResolver, "DomainClassResolver must not be null!");
73+
Assert.notNull(associations, "Associations must not be null!");
7174

7275
this.repositories = repositories;
7376
this.domainClassResolver = domainClassResolver;
74-
this.sortTranslator = new SortTranslator(persistentEntities, objectMapper);
77+
this.sortTranslator = new SortTranslator(persistentEntities, objectMapper, associations);
7578
}
7679

7780
/**
@@ -117,6 +120,7 @@ public static class SortTranslator {
117120

118121
private final @NonNull PersistentEntities persistentEntities;
119122
private final @NonNull ObjectMapper objectMapper;
123+
private final @NonNull Associations associations;
120124

121125
/**
122126
* Translates {@link Sort} orders from Jackson-mapped field names to {@link PersistentProperty} names. Properties
@@ -181,7 +185,7 @@ private List<String> mapPropertyPath(PersistentEntity<?, ?> rootEntity, List<Str
181185

182186
for (PersistentProperty<?> persistentProperty : persistentProperties) {
183187

184-
if (persistentProperty.isAssociation()) {
188+
if (associations.isLinkableAssociation(persistentProperty)) {
185189
return Collections.emptyList();
186190
}
187191

spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/SortTranslatorUnitTests.java

+30-5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2016 the original author or authors.
2+
* Copyright 2016-2017 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -17,6 +17,7 @@
1717

1818
import static org.hamcrest.Matchers.*;
1919
import static org.junit.Assert.*;
20+
import static org.mockito.Mockito.*;
2021

2122
import java.util.Collections;
2223
import java.util.List;
@@ -27,7 +28,11 @@
2728
import org.springframework.data.domain.Sort;
2829
import org.springframework.data.keyvalue.core.mapping.context.KeyValueMappingContext;
2930
import org.springframework.data.mapping.context.PersistentEntities;
31+
import org.springframework.data.rest.core.annotation.RestResource;
32+
import org.springframework.data.rest.core.config.RepositoryRestConfiguration;
33+
import org.springframework.data.rest.core.mapping.PersistentEntitiesResourceMappings;
3034
import org.springframework.data.rest.webmvc.json.JacksonMappingAwareSortTranslator.SortTranslator;
35+
import org.springframework.data.rest.webmvc.mapping.Associations;
3136

3237
import com.fasterxml.jackson.annotation.JsonProperty;
3338
import com.fasterxml.jackson.annotation.JsonUnwrapped;
@@ -57,7 +62,9 @@ public void setUp() {
5762
mappingContext.getPersistentEntity(MultiUnwrapped.class);
5863

5964
persistentEntities = new PersistentEntities(Collections.singleton(mappingContext));
60-
sortTranslator = new SortTranslator(persistentEntities, objectMapper);
65+
66+
sortTranslator = new SortTranslator(persistentEntities, objectMapper, new Associations(
67+
new PersistentEntitiesResourceMappings(persistentEntities), mock(RepositoryRestConfiguration.class)));
6168
}
6269

6370
/**
@@ -138,17 +145,29 @@ public void shouldSkipWrongNestedProperties() {
138145
}
139146

140147
/**
141-
* @see DATAREST-910
148+
* @see DATAREST-910, DATAREST-976
142149
*/
143150
@Test
144-
public void shouldSkipKnownAssociationProperties() {
151+
public void shouldSkipLinkableAssociationProperties() {
145152

146-
Sort translatedSort = sortTranslator.translateSort(new Sort("refEmbedded.name"),
153+
Sort translatedSort = sortTranslator.translateSort(new Sort("association.name"),
147154
mappingContext.getPersistentEntity(Plain.class));
148155

149156
assertThat(translatedSort, is(nullValue()));
150157
}
151158

159+
/**
160+
* @see DATAREST-976
161+
*/
162+
@Test
163+
public void shouldMapEmbeddableAssociationProperties() {
164+
165+
Sort translatedSort = sortTranslator.translateSort(new Sort("refEmbedded.name"),
166+
mappingContext.getPersistentEntity(Plain.class));
167+
168+
assertThat(translatedSort.getOrderFor("refEmbedded.name"), is(notNullValue()));
169+
}
170+
152171
/**
153172
* @see DATAREST-910
154173
*/
@@ -191,6 +210,7 @@ static class Plain {
191210
public String name;
192211
public Embedded embedded;
193212
@Reference public Embedded refEmbedded;
213+
@Reference public AnotherRootEntity association;
194214
}
195215

196216
static class UnwrapEmbedded {
@@ -222,4 +242,9 @@ static class EmbeddedWithJsonProperty {
222242
}
223243

224244
static interface SomeInterface {}
245+
246+
@RestResource
247+
static class AnotherRootEntity {
248+
public String name;
249+
}
225250
}

src/main/asciidoc/paging-and-sorting.adoc

+1-1
Original file line numberDiff line numberDiff line change
@@ -127,4 +127,4 @@ To have your results sorted on a particular property, add a `sort` URL parameter
127127
curl -v "http://localhost:8080/people/search/nameStartsWith?name=K&sort=name,desc"
128128
----
129129

130-
To sort the results by more than one property, keep adding as many `sort=PROPERTY` parameters as you need. They will be added to the `Pageable` in the order they appear in the query string.
130+
To sort the results by more than one property, keep adding as many `sort=PROPERTY` parameters as you need. They will be added to the `Pageable` in the order they appear in the query string. Results can be sorted by top-level and nested properties. Use property path notation to express a nested sort property. Sorting by linkable associations (i.e. resources to top-level resources) is not supported.

0 commit comments

Comments
 (0)