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.
This commit is contained in:
Mark Paluch
2016-09-05 12:39:45 +02:00
committed by Oliver Gierke
parent 129ea2a5e3
commit c48050bfa1
13 changed files with 823 additions and 77 deletions

View File

@@ -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<BackendIdConverter, Class<?>> 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());

View File

@@ -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> 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<Entry<String, JsonNode>> 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<PersistentProperty<?>, String> propertyToFieldName;
private final Map<String, PersistentProperty<?>> 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<PersistentProperty<?>, String>();
this.fieldNameToProperty = new HashMap<String, PersistentProperty<?>>();
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);
}
}
}

View File

@@ -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<Order> filteredOrders = new ArrayList<Order>();
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);
}
}
}

View File

@@ -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<PersistentProperty<?>, String> propertyToFieldName;
private final Map<String, PersistentProperty<?>> 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<PersistentProperty<?>, String>();
this.fieldNameToProperty = new HashMap<String, PersistentProperty<?>>();
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);
}
}

View File

@@ -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.
* <p>
* 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));
}
}

View File

@@ -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.
* <p>
* 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);
}
}

View File

@@ -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.
* <p>
* 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);
}
}

View File

@@ -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));
}
}

View File

@@ -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<Author> authors;
@JsonProperty("sales")
public long soldUnits;
public String title;
protected Book() {}
public Book(String isbn, String title, Iterable<Author> authors) {
public Book(String isbn, String title, long soldUnits, Iterable<Author> authors) {
this.isbn = isbn;
this.title = title;
this.soldUnits = soldUnits;
this.authors = new HashSet<Author>();

View File

@@ -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<Book, Long> {
@@ -35,4 +38,8 @@ public interface BookRepository extends CrudRepository<Book, Long> {
@Query("select b from Book b where :author member of b.authors")
List<Book> findByAuthorsContains(@Param("author") Author author);
@RestResource(rel = "find-spring-books-sorted")
@Query("select b from Book b where b.title like 'Spring%'")
Page<Book> findByTitleIsLike(Pageable pageable);
}

View File

@@ -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
*/

View File

@@ -53,8 +53,8 @@ public class TestDataPopulator {
Iterable<Author> 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() {

View File

@@ -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;
}
}