From c48050bfa1f96809fc41334ebbe5252cb8469563 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 5 Sep 2016 12:39:45 +0200 Subject: [PATCH] DATAREST-883 - Consider Jackson field names in Sort mapping. We now consider Jackson field names when resolving Sort arguments. Domain model properties annotated with @JsonProperty("sales") can be specified by their Jackson-mapped field name in sort arguments. Field names are mapped to their according persistent property names to be used in repository query method sorting. Unknown field names are silently dropped. Original pull request: #222. --- .../RepositoryRestMvcConfiguration.java | 26 +++- .../rest/webmvc/json/DomainObjectReader.java | 74 +--------- .../JacksonMappingAwareSortTranslator.java | 136 ++++++++++++++++++ .../rest/webmvc/json/MappedProperties.java | 111 ++++++++++++++ ...wareDefaultedPageableArgumentResolver.java | 92 ++++++++++++ .../MappingAwarePageableArgumentResolver.java | 89 ++++++++++++ .../MappingAwareSortArgumentResolver.java | 84 +++++++++++ .../webmvc/support/DomainClassResolver.java | 101 +++++++++++++ .../data/rest/webmvc/jpa/Book.java | 9 +- .../data/rest/webmvc/jpa/BookRepository.java | 7 + .../data/rest/webmvc/jpa/JpaWebTests.java | 55 ++++++- .../rest/webmvc/jpa/TestDataPopulator.java | 4 +- .../webmvc/json/SortTranslatorUnitTests.java | 112 +++++++++++++++ 13 files changed, 823 insertions(+), 77 deletions(-) create mode 100644 spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/JacksonMappingAwareSortTranslator.java create mode 100644 spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/MappedProperties.java create mode 100644 spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/MappingAwareDefaultedPageableArgumentResolver.java create mode 100644 spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/MappingAwarePageableArgumentResolver.java create mode 100644 spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/MappingAwareSortArgumentResolver.java create mode 100644 spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/DomainClassResolver.java create mode 100644 spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/SortTranslatorUnitTests.java diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvcConfiguration.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvcConfiguration.java index 4297137cc..f3a2133e7 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvcConfiguration.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvcConfiguration.java @@ -33,10 +33,10 @@ import org.springframework.beans.factory.config.PropertiesFactoryBean; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; -import org.springframework.context.annotation.ComponentScan.Filter; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.context.annotation.ImportResource; +import org.springframework.context.annotation.ComponentScan.Filter; import org.springframework.context.support.MessageSourceAccessor; import org.springframework.context.support.ReloadableResourceBundleMessageSource; import org.springframework.core.Ordered; @@ -86,7 +86,11 @@ import org.springframework.data.rest.webmvc.convert.UriListHttpMessageConverter; import org.springframework.data.rest.webmvc.json.DomainObjectReader; import org.springframework.data.rest.webmvc.json.EnumTranslator; import org.springframework.data.rest.webmvc.json.Jackson2DatatypeHelper; +import org.springframework.data.rest.webmvc.json.JacksonMappingAwareSortTranslator; import org.springframework.data.rest.webmvc.json.JacksonSerializers; +import org.springframework.data.rest.webmvc.json.MappingAwareDefaultedPageableArgumentResolver; +import org.springframework.data.rest.webmvc.json.MappingAwarePageableArgumentResolver; +import org.springframework.data.rest.webmvc.json.MappingAwareSortArgumentResolver; import org.springframework.data.rest.webmvc.json.PersistentEntityJackson2Module; import org.springframework.data.rest.webmvc.json.PersistentEntityToJsonSchemaConverter; import org.springframework.data.rest.webmvc.spi.BackendIdConverter; @@ -94,6 +98,7 @@ import org.springframework.data.rest.webmvc.spi.BackendIdConverter.DefaultIdConv import org.springframework.data.rest.webmvc.support.BackendIdHandlerMethodArgumentResolver; import org.springframework.data.rest.webmvc.support.DefaultedPageableHandlerMethodArgumentResolver; import org.springframework.data.rest.webmvc.support.DelegatingHandlerMapping; +import org.springframework.data.rest.webmvc.support.DomainClassResolver; import org.springframework.data.rest.webmvc.support.ETagArgumentResolver; import org.springframework.data.rest.webmvc.support.HttpMethodHandlerMethodArgumentResolver; import org.springframework.data.rest.webmvc.support.JpaHelper; @@ -146,6 +151,7 @@ import com.fasterxml.jackson.databind.SerializationFeature; * @author Oliver Gierke * @author Jon Brisbin * @author Greg Turnquist + * @author Mark Paluch */ @Configuration @EnableHypermediaSupport(type = HypermediaType.HAL) @@ -687,6 +693,12 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon return resolver; } + @Bean + public JacksonMappingAwareSortTranslator sortMethodArgumentTranslator() { + return new JacksonMappingAwareSortTranslator(objectMapper(), repositories(), + DomainClassResolver.create(repositories(), resourceMappings(), baseUri())); + } + @Bean public PluginRegistry> backendIdConverterRegistry() { @@ -713,10 +725,16 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon resourceMappings()); HateoasPageableHandlerMethodArgumentResolver pageableResolver = pageableResolver(); - HandlerMethodArgumentResolver defaultedPageableResolver = new DefaultedPageableHandlerMethodArgumentResolver( - pageableResolver); - return Arrays.asList(defaultedPageableResolver, pageableResolver, sortResolver(), + + HandlerMethodArgumentResolver sortResolver = new MappingAwareSortArgumentResolver(sortMethodArgumentTranslator(), + sortResolver()); + HandlerMethodArgumentResolver jacksonPageableResolver = new MappingAwarePageableArgumentResolver( + sortMethodArgumentTranslator(), pageableResolver); + HandlerMethodArgumentResolver defaultedPageableResolver = new MappingAwareDefaultedPageableArgumentResolver( + sortMethodArgumentTranslator(), pageableResolver); + + return Arrays.asList(defaultedPageableResolver, jacksonPageableResolver, sortResolver, serverHttpRequestMethodArgumentResolver(), repoRequestArgumentResolver(), persistentEntityArgumentResolver(), resourceMetadataHandlerMethodArgumentResolver(), HttpMethodHandlerMethodArgumentResolver.INSTANCE, peraResolver, backendIdHandlerMethodArgumentResolver(), eTagArgumentResolver()); diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/DomainObjectReader.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/DomainObjectReader.java index 25b40d6d4..cdf3a1993 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/DomainObjectReader.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/DomainObjectReader.java @@ -16,7 +16,6 @@ package org.springframework.data.rest.webmvc.json; import java.io.InputStream; -import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Map.Entry; @@ -31,12 +30,8 @@ import org.springframework.data.rest.webmvc.mapping.AssociationLinks; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.util.Assert; -import com.fasterxml.jackson.databind.BeanDescription; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.introspect.BasicClassIntrospector; -import com.fasterxml.jackson.databind.introspect.BeanPropertyDefinition; -import com.fasterxml.jackson.databind.introspect.ClassIntrospector; import com.fasterxml.jackson.databind.node.ObjectNode; /** @@ -45,13 +40,13 @@ import com.fasterxml.jackson.databind.node.ObjectNode; * detect nested objects, lookup the original value and apply the merge recursively. * * @author Oliver Gierke + * @author Mark Paluch * @since 2.2 */ public class DomainObjectReader { private final PersistentEntities entities; private final AssociationLinks associationLinks; - private final ClassIntrospector introspector; /** * Creates a new {@link DomainObjectReader} using the given {@link PersistentEntities} and {@link ResourceMappings}. @@ -66,13 +61,12 @@ public class DomainObjectReader { this.entities = entities; this.associationLinks = new AssociationLinks(mappings); - this.introspector = new BasicClassIntrospector(); } /** * Reads the given input stream into an {@link ObjectNode} and applies that to the given existing instance. * - * @param request must not be {@literal null}. + * @param source must not be {@literal null}. * @param target must not be {@literal null}. * @param mapper must not be {@literal null}. * @return @@ -110,7 +104,7 @@ public class DomainObjectReader { Assert.notNull(entity, "No PersistentEntity found for ".concat(type.getName()).concat("!")); - final MappedProperties properties = getJacksonProperties(entity, mapper); + final MappedProperties properties = MappedProperties.fromJacksonProperties(entity, mapper); entity.doWithProperties(new SimplePropertyHandler() { @@ -157,6 +151,7 @@ public class DomainObjectReader { * @return * @throws Exception */ + @SuppressWarnings("unchecked") private T doMerge(ObjectNode root, T target, ObjectMapper mapper) throws Exception { Assert.notNull(root, "Root ObjectNode must not be null!"); @@ -169,7 +164,7 @@ public class DomainObjectReader { return mapper.readerForUpdating(target).readValue(root); } - MappedProperties mappedProperties = getJacksonProperties(entity, mapper); + MappedProperties mappedProperties = MappedProperties.fromJacksonProperties(entity, mapper); for (Iterator> i = root.fields(); i.hasNext();) { @@ -254,63 +249,4 @@ public class DomainObjectReader { } } } - - /** - * Returns the {@link MappedProperties} for the given {@link PersistentEntity}. - * - * @param entity must not be {@literal null}. - * @param mapper must not be {@literal null}. - * @return - */ - private MappedProperties getJacksonProperties(PersistentEntity entity, ObjectMapper mapper) { - - BeanDescription description = introspector.forDeserialization(mapper.getDeserializationConfig(), - mapper.constructType(entity.getType()), mapper.getDeserializationConfig()); - - return new MappedProperties(entity, description); - } - - /** - * Simple value object to capture a mapping of Jackson mapped field names and {@link PersistentProperty} instances. - * - * @author Oliver Gierke - */ - private static class MappedProperties { - - private final Map, String> propertyToFieldName; - private final Map> fieldNameToProperty; - - /** - * Creates a new {@link MappedProperties} instance for the given {@link PersistentEntity} and - * {@link BeanDescription}. - * - * @param entity must not be {@literal null}. - * @param description must not be {@literal null}. - */ - public MappedProperties(PersistentEntity entity, BeanDescription description) { - - this.propertyToFieldName = new HashMap, String>(); - this.fieldNameToProperty = new HashMap>(); - - for (BeanPropertyDefinition property : description.findProperties()) { - - PersistentProperty persistentProperty = entity.getPersistentProperty(property.getInternalName()); - - propertyToFieldName.put(persistentProperty, property.getName()); - fieldNameToProperty.put(property.getName(), persistentProperty); - } - } - - public String getMappedName(PersistentProperty property) { - return propertyToFieldName.get(property); - } - - public boolean hasPersistentPropertyForField(String fieldName) { - return fieldNameToProperty.containsKey(fieldName); - } - - public PersistentProperty getPersistentProperty(String fieldName) { - return fieldNameToProperty.get(fieldName); - } - } } diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/JacksonMappingAwareSortTranslator.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/JacksonMappingAwareSortTranslator.java new file mode 100644 index 000000000..8954cf5a9 --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/JacksonMappingAwareSortTranslator.java @@ -0,0 +1,136 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.rest.webmvc.json; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.core.MethodParameter; +import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Sort.Order; +import org.springframework.data.mapping.PersistentEntity; +import org.springframework.data.mapping.PersistentProperty; +import org.springframework.data.repository.support.Repositories; +import org.springframework.data.rest.webmvc.support.DomainClassResolver; +import org.springframework.util.Assert; +import org.springframework.web.context.request.NativeWebRequest; + +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * Translator for {@link Sort} arguments that is aware of Jackson-Mapping on domain classes. Jackson field names are + * translated to {@link PersistentProperty} names. Domain class are looked up by resolving request URLs to mapped + * repositories. + * + * @author Mark Paluch + */ +public class JacksonMappingAwareSortTranslator { + + private final ObjectMapper objectMapper; + private final Repositories repositories; + private final DomainClassResolver domainClassResolver; + + /** + * Creates a new {@link JacksonMappingAwareSortTranslator} for the given {@link ObjectMapper}, {@link Repositories} + * and {@link DomainClassResolver}. + * + * @param objectMapper must not be {@literal null}. + * @param repositories must not be {@literal null}. + * @param domainClassResolver must not be {@literal null}. + */ + public JacksonMappingAwareSortTranslator(ObjectMapper objectMapper, Repositories repositories, + DomainClassResolver domainClassResolver) { + + Assert.notNull(objectMapper, "ObjectMapper must not be null!"); + Assert.notNull(repositories, "Repositories must not be null!"); + Assert.notNull(domainClassResolver, "DomainClassResolver must not be null!"); + + this.objectMapper = objectMapper; + this.repositories = repositories; + this.domainClassResolver = domainClassResolver; + } + + /** + * Translates Jackson field names within a {@link Sort} to {@link PersistentProperty} property names. + * + * @param input must not be {@literal null}. + * @param parameter must not be {@literal null}. + * @param webRequest must not be {@literal null}. + * @return a {@link Sort} containing translated property names or {@literal null} the resulting {@link Sort} contains + * no properties. + */ + protected Sort translateMethodParameter(Sort input, MethodParameter parameter, NativeWebRequest webRequest) { + + Assert.notNull(input, "Sort must not be null!"); + Assert.notNull(parameter, "MethodParameter must not be null!"); + Assert.notNull(webRequest, "NativeWebRequest must not be null!"); + + Class domainClass = domainClassResolver.resolve(parameter.getMethod(), webRequest); + PersistentEntity persistentEntity = repositories.getPersistentEntity(domainClass); + MappedProperties mappedProperties = MappedProperties.fromJacksonProperties(persistentEntity, objectMapper); + + return new SortTranslator(mappedProperties).translateSort(input); + } + + /** + * Translates {@link Sort} orders from Jackson-mapped field names to {@link PersistentProperty} names. + * + * @author Mark Paluch + */ + static class SortTranslator { + + private final MappedProperties mappedProperties; + + /** + * Creates a new {@link SortTranslator}. + * + * @param mappedProperties must not be {@literal null}. + */ + public SortTranslator(MappedProperties mappedProperties) { + + Assert.notNull(mappedProperties, "MappedProperties must not be null!"); + + this.mappedProperties = mappedProperties; + } + + /** + * Translates {@link Sort} orders from Jackson-mapped field names to {@link PersistentProperty} names. Properties + * that cannot be resolved are dropped. + * + * @param input must not be {@literal null}. + * @return {@link Sort} with translated field names or {@literal null} if translation dropped all sort fields. + */ + Sort translateSort(Sort input) { + + List filteredOrders = new ArrayList(); + + for (Order order : input) { + + if (mappedProperties.hasPersistentPropertyForField(order.getProperty())) { + PersistentProperty persistentProperty = mappedProperties.getPersistentProperty(order.getProperty()); + Order mappedOrder = new Order(order.getDirection(), persistentProperty.getName(), order.getNullHandling()); + filteredOrders.add(order.isIgnoreCase() ? mappedOrder.ignoreCase() : mappedOrder); + } + } + + if (filteredOrders.isEmpty()) { + return null; + } + + return new Sort(filteredOrders); + } + } +} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/MappedProperties.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/MappedProperties.java new file mode 100644 index 000000000..19ca4ae2b --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/MappedProperties.java @@ -0,0 +1,111 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.rest.webmvc.json; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.data.mapping.PersistentEntity; +import org.springframework.data.mapping.PersistentProperty; +import org.springframework.util.Assert; + +import com.fasterxml.jackson.databind.BeanDescription; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.introspect.BasicClassIntrospector; +import com.fasterxml.jackson.databind.introspect.BeanPropertyDefinition; +import com.fasterxml.jackson.databind.introspect.ClassIntrospector; + +/** + * Simple value object to capture a mapping of Jackson mapped field names and {@link PersistentProperty} instances. + * + * @author Oliver Gierke + * @author Mark Paluch + */ +class MappedProperties { + + private final Map, String> propertyToFieldName; + private final Map> fieldNameToProperty; + + /** + * Creates a new {@link MappedProperties} instance for the given {@link PersistentEntity} and {@link BeanDescription}. + * + * @param entity must not be {@literal null}. + * @param description must not be {@literal null}. + */ + private MappedProperties(PersistentEntity entity, BeanDescription description) { + + this.propertyToFieldName = new HashMap, String>(); + this.fieldNameToProperty = new HashMap>(); + + for (BeanPropertyDefinition property : description.findProperties()) { + + PersistentProperty persistentProperty = entity.getPersistentProperty(property.getInternalName()); + + propertyToFieldName.put(persistentProperty, property.getName()); + fieldNameToProperty.put(property.getName(), persistentProperty); + } + } + + /** + * Creates {@link MappedProperties} for the given {@link PersistentEntity}. + * + * @param entity must not be {@literal null}. + * @param mapper must not be {@literal null}. + * @return + */ + public static MappedProperties fromJacksonProperties(PersistentEntity entity, ObjectMapper mapper) { + + ClassIntrospector introspector = new BasicClassIntrospector(); + + BeanDescription description = introspector.forDeserialization(mapper.getDeserializationConfig(), + mapper.constructType(entity.getType()), mapper.getDeserializationConfig()); + + return new MappedProperties(entity, description); + } + + /** + * @param property must not be {@literal null} + * @return the mapped name for the {@link PersistentProperty} + */ + public String getMappedName(PersistentProperty property) { + + Assert.notNull(property, "PersistentProperty must not be null!"); + + return propertyToFieldName.get(property); + } + + /** + * @param fieldName must not be empty or {@literal null}. + * @return {@literal true} if the field name resolves to a {@literal PersistentProperty}. + */ + public boolean hasPersistentPropertyForField(String fieldName) { + + Assert.hasText(fieldName, "Field name must not be empty or null!"); + + return fieldNameToProperty.containsKey(fieldName); + } + + /** + * @param fieldName must not be empty or {@literal null}. + * @return + */ + public PersistentProperty getPersistentProperty(String fieldName) { + + Assert.hasText(fieldName, "Field name must not be empty or null!"); + + return fieldNameToProperty.get(fieldName); + } +} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/MappingAwareDefaultedPageableArgumentResolver.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/MappingAwareDefaultedPageableArgumentResolver.java new file mode 100644 index 000000000..07ea1ee2f --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/MappingAwareDefaultedPageableArgumentResolver.java @@ -0,0 +1,92 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.rest.webmvc.json; + +import org.springframework.core.MethodParameter; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.rest.webmvc.support.DefaultedPageable; +import org.springframework.data.web.PageableHandlerMethodArgumentResolver; +import org.springframework.util.Assert; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +/** + * {@link HandlerMethodArgumentResolver} to resolve {@link DefaultedPageable} from a + * {@link PageableHandlerMethodArgumentResolver} applying field to property mapping. + *

+ * A resolved {@link DefaultedPageable} is post-processed by applying Jackson field-to-property mapping if it contains a + * {@link Sort} instance. Customized fields are resolved to their property names. Unknown properties are removed from + * {@link Sort}. + * + * @author Mark Paluch + * @since 2.6 + */ +public class MappingAwareDefaultedPageableArgumentResolver implements HandlerMethodArgumentResolver { + + private final PageableHandlerMethodArgumentResolver delegate; + private final JacksonMappingAwareSortTranslator translator; + + /** + * Creates a new {@link MappingAwareDefaultedPageableArgumentResolver} for the given + * {@link JacksonMappingAwareSortTranslator} and {@link PageableHandlerMethodArgumentResolver}. + * + * @param translator must not be {@literal null}. + * @param delegate must not be {@literal null}. + */ + public MappingAwareDefaultedPageableArgumentResolver(JacksonMappingAwareSortTranslator translator, + PageableHandlerMethodArgumentResolver delegate) { + + Assert.notNull(translator, "JacksonMappingSortTranslator must not be null!"); + Assert.notNull(delegate, "PageableHandlerMethodArgumentResolver must not be null!"); + + this.translator = translator; + this.delegate = delegate; + } + + /* + * (non-Javadoc) + * @see org.springframework.web.method.support.HandlerMethodArgumentResolver#supportsParameter(org.springframework.core.MethodParameter) + */ + @Override + public boolean supportsParameter(MethodParameter parameter) { + return DefaultedPageable.class.isAssignableFrom(parameter.getParameterType()); + } + + /* + * (non-Javadoc) + * @see org.springframework.web.method.support.HandlerMethodArgumentResolver#resolveArgument(org.springframework.core.MethodParameter, org.springframework.web.method.support.ModelAndViewContainer, org.springframework.web.context.request.NativeWebRequest, org.springframework.web.bind.support.WebDataBinderFactory) + */ + @Override + public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { + + Pageable pageable = delegate.resolveArgument(parameter, mavContainer, webRequest, binderFactory); + + if (pageable == null || pageable.getSort() == null) { + return new DefaultedPageable(pageable, delegate.isFallbackPageable(pageable)); + } + + Sort translated = translator.translateMethodParameter(pageable.getSort(), parameter, webRequest); + pageable = new PageRequest(pageable.getPageNumber(), pageable.getPageSize(), translated); + + return new DefaultedPageable(pageable, delegate.isFallbackPageable(pageable)); + } + +} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/MappingAwarePageableArgumentResolver.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/MappingAwarePageableArgumentResolver.java new file mode 100644 index 000000000..2669ab1e7 --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/MappingAwarePageableArgumentResolver.java @@ -0,0 +1,89 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.rest.webmvc.json; + +import org.springframework.core.MethodParameter; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.web.PageableHandlerMethodArgumentResolver; +import org.springframework.util.Assert; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +/** + * {@link HandlerMethodArgumentResolver} to resolve {@link Pageable} from a + * {@link PageableHandlerMethodArgumentResolver} applying field to property mapping. + *

+ * A resolved {@link Pageable} is post-processed by applying Jackson field-to-property mapping if it contains a + * {@link Sort} instance. Customized fields are resolved to their property names. Unknown properties are removed from + * {@link Sort}. + * + * @author Mark Paluch + * @since 2.6 + */ +public class MappingAwarePageableArgumentResolver implements HandlerMethodArgumentResolver { + + private final JacksonMappingAwareSortTranslator translator; + private final PageableHandlerMethodArgumentResolver delegate; + + /** + * Creates a new {@link MappingAwarePageableArgumentResolver} for the given {@link JacksonMappingAwareSortTranslator} + * and {@link PageableHandlerMethodArgumentResolver}. + * + * @param translator must not be {@literal null}. + * @param delegate must not be {@literal null}. + */ + public MappingAwarePageableArgumentResolver(JacksonMappingAwareSortTranslator translator, + PageableHandlerMethodArgumentResolver delegate) { + + Assert.notNull(translator, "JacksonMappingSortTranslator must not be null!"); + Assert.notNull(delegate, "PageableHandlerMethodArgumentResolver must not be null!"); + + this.translator = translator; + this.delegate = delegate; + } + + /* + * (non-Javadoc) + * @see org.springframework.web.method.support.HandlerMethodArgumentResolver#supportsParameter(org.springframework.core.MethodParameter) + */ + @Override + public boolean supportsParameter(MethodParameter parameter) { + return delegate.supportsParameter(parameter); + } + + /* + * (non-Javadoc) + * @see org.springframework.web.method.support.HandlerMethodArgumentResolver#resolveArgument(org.springframework.core.MethodParameter, org.springframework.web.method.support.ModelAndViewContainer, org.springframework.web.context.request.NativeWebRequest, org.springframework.web.bind.support.WebDataBinderFactory) + */ + @Override + public Pageable resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { + + Pageable pageable = delegate.resolveArgument(parameter, mavContainer, webRequest, binderFactory); + + if (pageable == null || pageable.getSort() == null) { + return null; + } + + Sort translated = translator.translateMethodParameter(pageable.getSort(), parameter, webRequest); + return new PageRequest(pageable.getPageNumber(), pageable.getPageSize(), translated); + } + +} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/MappingAwareSortArgumentResolver.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/MappingAwareSortArgumentResolver.java new file mode 100644 index 000000000..ef99f2ab9 --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/MappingAwareSortArgumentResolver.java @@ -0,0 +1,84 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.rest.webmvc.json; + +import org.springframework.core.MethodParameter; +import org.springframework.data.domain.Sort; +import org.springframework.data.web.SortHandlerMethodArgumentResolver; +import org.springframework.util.Assert; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +/** + * {@link HandlerMethodArgumentResolver} to resolve {@link Sort} from a {@link SortHandlerMethodArgumentResolver} + * applying field to property mapping. + *

+ * A resolved {@link Sort} is post-processed by applying Jackson field-to-property mapping. Customized fields are + * resolved to their property names. Unknown properties are removed from {@link Sort}. + * + * @author Mark Paluch + * @since 2.6 + */ +public class MappingAwareSortArgumentResolver implements HandlerMethodArgumentResolver { + + private final JacksonMappingAwareSortTranslator translator; + private final SortHandlerMethodArgumentResolver delegate; + + /** + * Creates a new {@link MappingAwareSortArgumentResolver} for the given {@link JacksonMappingAwareSortTranslator} and + * {@link SortHandlerMethodArgumentResolver}. + * + * @param translator must not be {@literal null}. + * @param delegate must not be {@literal null}. + */ + public MappingAwareSortArgumentResolver(JacksonMappingAwareSortTranslator translator, + SortHandlerMethodArgumentResolver delegate) { + + Assert.notNull(translator, "JacksonMappingSortTranslator must not be null!"); + Assert.notNull(delegate, "PageableHandlerMethodArgumentResolver must not be null!"); + + this.translator = translator; + this.delegate = delegate; + } + + /* + * (non-Javadoc) + * @see org.springframework.web.method.support.HandlerMethodArgumentResolver#supportsParameter(org.springframework.core.MethodParameter) + */ + @Override + public boolean supportsParameter(MethodParameter parameter) { + return delegate.supportsParameter(parameter); + } + + /* + * (non-Javadoc) + * @see org.springframework.web.method.support.HandlerMethodArgumentResolver#resolveArgument(org.springframework.core.MethodParameter, org.springframework.web.method.support.ModelAndViewContainer, org.springframework.web.context.request.NativeWebRequest, org.springframework.web.bind.support.WebDataBinderFactory) + */ + @Override + public Sort resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { + + Sort sort = delegate.resolveArgument(parameter, mavContainer, webRequest, binderFactory); + + if (sort == null) { + return null; + } + + return translator.translateMethodParameter(sort, parameter, webRequest); + } +} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/DomainClassResolver.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/DomainClassResolver.java new file mode 100644 index 000000000..227242476 --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/DomainClassResolver.java @@ -0,0 +1,101 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.rest.webmvc.support; + +import java.lang.reflect.Method; + +import org.springframework.data.repository.support.Repositories; +import org.springframework.data.rest.core.mapping.ResourceMappings; +import org.springframework.data.rest.core.mapping.ResourceMetadata; +import org.springframework.data.rest.webmvc.BaseUri; +import org.springframework.data.rest.webmvc.util.UriUtils; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; +import org.springframework.web.context.request.NativeWebRequest; + +/** + * Resolves a domain class from a web request. Domain class resolution is only available for {@link NativeWebRequest web + * requests} related to mapped and exported {@link Repositories}. + * + * @author Mark Paluch + * @since 2.6 + */ +public class DomainClassResolver { + + private final Repositories repositories; + private final ResourceMappings mappings; + private final BaseUri baseUri; + + /** + * Creates a new {@link DomainClassResolver} for the given {@link Repositories} and {@link ResourceMappings}. + * + * @param repositories must not be {@literal null}. + * @param mappings must not be {@literal null}. + * @param baseUri must not be {@literal null}. + */ + private DomainClassResolver(Repositories repositories, ResourceMappings mappings, BaseUri baseUri) { + + Assert.notNull(repositories, "Repositories must not be null!"); + Assert.notNull(mappings, "ResourceMappings must not be null!"); + Assert.notNull(baseUri, "BaseUri must not be null!"); + + this.repositories = repositories; + this.mappings = mappings; + this.baseUri = baseUri; + } + + /** + * Creates a new {@link DomainClassResolver} for the given {@link Repositories} and {@link ResourceMappings}. + * + * @param repositories must not be {@literal null}. + * @param mappings must not be {@literal null}. + * @param baseUri must not be {@literal null}. + */ + public static DomainClassResolver create(Repositories repositories, ResourceMappings mappings, BaseUri baseUri) { + return new DomainClassResolver(repositories, mappings, baseUri); + } + + /** + * Resolves a domain class that is associated with the {@link NativeWebRequest} + * + * @param method must not be {@literal null}. + * @param webRequest must not be {@literal null}. + * @return domain type that is associated with this request. + * @throws IllegalArgumentException if there's no repository key associated or no domain type can be resolved. + */ + public Class resolve(Method method, NativeWebRequest webRequest) { + + Assert.notNull(method, "Method must not be null!"); + Assert.notNull(webRequest, "NativeWebRequest must not be null!"); + + String lookupPath = baseUri.getRepositoryLookupPath(webRequest); + String repositoryKey = UriUtils.findMappingVariable("repository", method, lookupPath); + + if (!StringUtils.hasText(repositoryKey)) { + throw new IllegalArgumentException(String.format("Could not determine a repository key from %s.", lookupPath)); + } + + for (Class domainType : repositories) { + ResourceMetadata mapping = mappings.getMetadataFor(domainType); + if (mapping.getPath().matches(repositoryKey) && mapping.isExported()) { + return domainType; + } + } + + throw new IllegalArgumentException( + String.format("Could not resolve an exported domain type for %s.", repositoryKey)); + } +} diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/Book.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/Book.java index 86bf1680b..f136be7ba 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/Book.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/Book.java @@ -26,8 +26,11 @@ import javax.persistence.ManyToMany; import org.springframework.data.rest.core.annotation.RestResource; +import com.fasterxml.jackson.annotation.JsonProperty; + /** * @author Oliver Gierke + * @author Mark Paluch */ @Entity public class Book { @@ -38,14 +41,18 @@ public class Book { @ManyToMany(cascade = { CascadeType.MERGE })// @RestResource(path = "creators") public Set authors; + @JsonProperty("sales") + public long soldUnits; + public String title; protected Book() {} - public Book(String isbn, String title, Iterable authors) { + public Book(String isbn, String title, long soldUnits, Iterable authors) { this.isbn = isbn; this.title = title; + this.soldUnits = soldUnits; this.authors = new HashSet(); diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/BookRepository.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/BookRepository.java index 43d277a80..eb419f787 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/BookRepository.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/BookRepository.java @@ -17,6 +17,8 @@ package org.springframework.data.rest.webmvc.jpa; import java.util.List; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.CrudRepository; @@ -26,6 +28,7 @@ import org.springframework.data.rest.core.annotation.RestResource; /** * @author Oliver Gierke + * @author Mark Paluch */ @RepositoryRestResource(excerptProjection = BookExcerpt.class) public interface BookRepository extends CrudRepository { @@ -35,4 +38,8 @@ public interface BookRepository extends CrudRepository { @Query("select b from Book b where :author member of b.authors") List findByAuthorsContains(@Param("author") Author author); + + @RestResource(rel = "find-spring-books-sorted") + @Query("select b from Book b where b.title like 'Spring%'") + Page findByTitleIsLike(Pageable pageable); } diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/JpaWebTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/JpaWebTests.java index 35ca5ce37..b0f7400a1 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/JpaWebTests.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/JpaWebTests.java @@ -20,6 +20,7 @@ import static org.junit.Assert.*; import static org.springframework.data.rest.webmvc.util.TestUtils.*; import static org.springframework.http.HttpHeaders.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; import net.minidev.json.JSONArray; @@ -57,6 +58,7 @@ import com.jayway.jsonpath.JsonPath; * * @author Oliver Gierke * @author Greg Turnquist + * @author Mark Paluch */ @Transactional @ContextConfiguration(classes = JpaRepositoryConfig.class) @@ -546,7 +548,7 @@ public class JpaWebTests extends CommonWebTests { * @see DATAREST-384 */ @Test - public void execturesSearchThatTakesASort() throws Exception { + public void exectuesSearchThatTakesASort() throws Exception { Link booksLink = client.discoverUnique("books"); Link searchLink = client.discoverUnique(booksLink, "search"); @@ -568,6 +570,57 @@ public class JpaWebTests extends CommonWebTests { andExpect(client.hasLinkWithRel("self")); } + /** + * @see DATAREST-883 + */ + @Test + public void exectuesSearchThatTakesAMappedSortProperty() throws Exception { + + Link booksLink = client.discoverUnique("books"); + Link searchLink = client.discoverUnique(booksLink, "search"); + Link findBySortedLink = client.discoverUnique(searchLink, "find-by-sorted"); + + // Assert sort options advertised + assertThat(findBySortedLink.isTemplated(), is(true)); + assertThat(findBySortedLink.getVariableNames(), hasItems("sort", "projection")); + + // Assert results returned as specified + client.follow(findBySortedLink.expand("sales,desc")).// + andExpect(jsonPath("$._embedded.books[0].title").value("Spring Data (Second Edition)")).// + andExpect(jsonPath("$._embedded.books[1].title").value("Spring Data")).// + andExpect(client.hasLinkWithRel("self")); + + client.follow(findBySortedLink.expand("sales,asc")).// + andExpect(jsonPath("$._embedded.books[0].title").value("Spring Data")).// + andExpect(jsonPath("$._embedded.books[1].title").value("Spring Data (Second Edition)")).// + andExpect(client.hasLinkWithRel("self")); + } + + /** + * @see DATAREST-883 + */ + @Test + public void exectuesCustomQuerySearchThatTakesAMappedSortProperty() throws Exception { + + Link booksLink = client.discoverUnique("books"); + Link searchLink = client.discoverUnique(booksLink, "search"); + Link findByLink = client.discoverUnique(searchLink, "find-spring-books-sorted"); + + // Assert sort options advertised + assertThat(findByLink.isTemplated(), is(true)); + + // Assert results returned as specified + client.follow(findByLink.expand("0", "10", "sales,desc")).// + andExpect(jsonPath("$._embedded.books[0].title").value("Spring Data (Second Edition)")).// + andExpect(jsonPath("$._embedded.books[1].title").value("Spring Data")).// + andExpect(client.hasLinkWithRel("self")); + + client.follow(findByLink.expand("0", "10", "unknown,asc,sales,asc")).// + andExpect(jsonPath("$._embedded.books[0].title").value("Spring Data")).// + andExpect(jsonPath("$._embedded.books[1].title").value("Spring Data (Second Edition)")).// + andExpect(client.hasLinkWithRel("self")); + } + /** * @see DATAREST-160 */ diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/TestDataPopulator.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/TestDataPopulator.java index f617922d5..c7d0fe48f 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/TestDataPopulator.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/TestDataPopulator.java @@ -53,8 +53,8 @@ public class TestDataPopulator { Iterable authors = this.authors.save(Arrays.asList(ollie, mark, michael, david, john, thomas)); - books.save(new Book("1449323952", "Spring Data", authors)); - books.save(new Book("1449323953", "Spring Data (Second Edition)", authors)); + books.save(new Book("1449323952", "Spring Data", 1000, authors)); + books.save(new Book("1449323953", "Spring Data (Second Edition)", 2000, authors)); } private void populateOrders() { diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/SortTranslatorUnitTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/SortTranslatorUnitTests.java new file mode 100644 index 000000000..345043d4a --- /dev/null +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/SortTranslatorUnitTests.java @@ -0,0 +1,112 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.rest.webmvc.json; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.data.domain.Sort; +import org.springframework.data.mongodb.core.mapping.MongoMappingContext; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * Unit tests for {@link JacksonMappingAwareSortTranslator.SortTranslator}. + * + * @author Mark Paluch + * @soundtrack dkn - Out Of This World (original version) + */ +public class SortTranslatorUnitTests { + + private MongoMappingContext mappingContext; + + @Before + public void setUp() { + + mappingContext = new MongoMappingContext(); + mappingContext.getPersistentEntity(Plain.class); + mappingContext.getPersistentEntity(WithJsonProperty.class); + } + + /** + * @see DATAREST-883 + */ + @Test + public void shouldMapKnownProperties() { + + MappedProperties mappedProperties = MappedProperties + .fromJacksonProperties(mappingContext.getPersistentEntity(Plain.class), new ObjectMapper()); + Sort translatedSort = new JacksonMappingAwareSortTranslator.SortTranslator(mappedProperties) + .translateSort(new Sort("hello", "name")); + + assertThat(translatedSort.getOrderFor("hello"), is(nullValue())); + assertThat(translatedSort.getOrderFor("name"), is(notNullValue())); + } + + /** + * @see DATAREST-883 + */ + @Test + public void returnsNullSortIfNoPropertiesMatch() { + + MappedProperties mappedProperties = MappedProperties + .fromJacksonProperties(mappingContext.getPersistentEntity(Plain.class), new ObjectMapper()); + Sort translatedSort = new JacksonMappingAwareSortTranslator.SortTranslator(mappedProperties) + .translateSort(new Sort("hello", "world")); + + assertThat(translatedSort, is(nullValue())); + } + + /** + * @see DATAREST-883 + */ + @Test + public void shouldMapKnownPropertiesWithJsonProperty() { + + MappedProperties mappedProperties = MappedProperties + .fromJacksonProperties(mappingContext.getPersistentEntity(WithJsonProperty.class), new ObjectMapper()); + Sort translatedSort = new JacksonMappingAwareSortTranslator.SortTranslator(mappedProperties) + .translateSort(new Sort("hello", "foo")); + + assertThat(translatedSort.getOrderFor("hello"), is(nullValue())); + assertThat(translatedSort.getOrderFor("name"), is(notNullValue())); + } + + /** + * @see DATAREST-883 + */ + @Test + public void shouldJacksonFieldNameForMapping() { + + MappedProperties mappedProperties = MappedProperties + .fromJacksonProperties(mappingContext.getPersistentEntity(WithJsonProperty.class), new ObjectMapper()); + Sort translatedSort = new JacksonMappingAwareSortTranslator.SortTranslator(mappedProperties) + .translateSort(new Sort("name")); + + assertThat(translatedSort, is(nullValue())); + } + + static class Plain { + public String name; + } + + static class WithJsonProperty { + public @JsonProperty("foo") String name; + } +}