DATAREST-910 - Support nested Sort properties.

We now support nested Sort properties considering Jackson mapping. Sort translation is optional and skipped if the domain class is not resolvable. Translation in the scope of a domain class maps property paths to apply sorting using embedded properties.

A sort string `nested-name` resolves to a property path `anotherWrap.embedded.name`.

class Aggregate {

	@JsonUnwrapped
	public UnwrapEmbedded anotherWrap;
}

class UnwrapEmbedded {

	@JsonUnwrapped(prefix = "nested-")
	public Embedded embedded;
}

class Embedded {
	public String name;
}

Original pull request: #232.
This commit is contained in:
Mark Paluch
2016-09-28 11:06:04 +02:00
committed by Oliver Gierke
parent 7880bef68c
commit 357bb7a28b
10 changed files with 861 additions and 41 deletions

View File

@@ -17,8 +17,12 @@ package org.springframework.data.rest.webmvc.jpa;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.data.rest.webmvc.BasePathAwareController;
import org.springframework.data.rest.webmvc.RepositoryRestController;
import org.springframework.data.rest.webmvc.support.DefaultedPageable;
import org.springframework.http.MediaType;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.web.bind.annotation.PathVariable;
@@ -30,6 +34,7 @@ import org.springframework.web.bind.annotation.RequestMethod;
*
* @author Jon Brisbin
* @author Oliver Gierke
* @author Mark Paluch
*/
@Configuration
@EnableJpaRepositories
@@ -50,6 +55,14 @@ public class JpaRepositoryConfig extends JpaInfrastructureConfig {
static class BooksHtmlController {
@RequestMapping(value = "/books/{id}", method = RequestMethod.GET, produces = MediaType.TEXT_HTML_VALUE)
void person(@PathVariable String id) {}
void someMethod(@PathVariable String id) {}
}
@RepositoryRestController
@RequestMapping("orders")
static class OrdersJsonController {
@RequestMapping(value = "/search/sort", method = RequestMethod.POST, produces = "application/hal+json")
void someMethodWithArgs(Sort sort, Pageable pageable, DefaultedPageable defaultedPageable) {}
}
}

View File

@@ -702,6 +702,16 @@ public class JpaWebTests extends CommonWebTests {
andExpect(client.hasLinkWithRel("self"));
}
/**
* @see DATAREST-910
*/
@Test
public void callUnmappedCustomRepositoryController() throws Exception {
mvc.perform(post("/orders/search/sort")).andExpect(status().isOk());
mvc.perform(post("/orders/search/sort?sort=type&page=1&size=10")).andExpect(status().isOk());
}
private List<Link> preparePersonResources(Person primary, Person... persons) throws Exception {
Link peopleLink = client.discoverUnique("people");

View File

@@ -805,7 +805,7 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon
PageableHandlerMethodArgumentResolver pageableResolver = pageableResolver();
JacksonMappingAwareSortTranslator sortTranslator = new JacksonMappingAwareSortTranslator(objectMapper(),
repositories(), DomainClassResolver.of(repositories(), resourceMappings(), baseUri()));
repositories(), DomainClassResolver.of(repositories(), resourceMappings(), baseUri()), persistentEntities());
HandlerMethodArgumentResolver sortResolver = new MappingAwareSortArgumentResolver(sortTranslator, sortResolver());
HandlerMethodArgumentResolver jacksonPageableResolver = new MappingAwarePageableArgumentResolver(sortTranslator,

View File

@@ -19,27 +19,32 @@ import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
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.mapping.context.PersistentEntities;
import org.springframework.data.repository.support.Repositories;
import org.springframework.data.rest.webmvc.support.DomainClassResolver;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
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.
* translated to {@link PersistentProperty} names. Domain class is looked up by resolving request URLs to mapped
* repositories. {@link Sort} translation is skipped if a domain class cannot be resolved.
*
* @author Mark Paluch
* @since 2.6, 2.5.3
* @since 2.6
*/
@RequiredArgsConstructor
public class JacksonMappingAwareSortTranslator {
@@ -47,10 +52,11 @@ public class JacksonMappingAwareSortTranslator {
private final @NonNull ObjectMapper objectMapper;
private final @NonNull Repositories repositories;
private final @NonNull DomainClassResolver domainClassResolver;
private final @NonNull PersistentEntities persistentEntities;
/**
* 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}.
@@ -64,10 +70,14 @@ public class JacksonMappingAwareSortTranslator {
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);
if (domainClass != null) {
PersistentEntity<?, ?> persistentEntity = repositories.getPersistentEntity(domainClass);
return new SortTranslator(persistentEntities, objectMapper).translateSort(input, persistentEntity);
}
return input;
}
/**
@@ -75,35 +85,178 @@ public class JacksonMappingAwareSortTranslator {
*
* @author Mark Paluch
* @author Oliver Gierke
* @since 2.6, 2.5.3
* @since 2.6
*/
@RequiredArgsConstructor
static class SortTranslator {
public static class SortTranslator {
private final @NonNull MappedProperties mappedProperties;
private static final String DELIMITERS = "_\\.";
private static final String ALL_UPPERCASE = "[A-Z0-9._$]+";
private static final Pattern SPLITTER = Pattern.compile("(?:[%s]?([%s]*?[^%s]+))".replaceAll("%s", DELIMITERS));
private final @NonNull PersistentEntities persistentEntities;
private final @NonNull ObjectMapper objectMapper;
/**
* 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}.
* @param rootEntity 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) {
public Sort translateSort(Sort input, PersistentEntity<?, ?> rootEntity) {
Assert.notNull(input, "Sort must not be null!");
Assert.notNull(rootEntity, "PersistentEntity must not be null!");
List<Order> filteredOrders = new ArrayList<Order>();
for (Order order : input) {
if (mappedProperties.hasPersistentPropertyForField(order.getProperty())) {
List<String> iteratorSource = new ArrayList<String>();
Matcher matcher = SPLITTER.matcher("_" + order.getProperty());
PersistentProperty<?> persistentProperty = mappedProperties.getPersistentProperty(order.getProperty());
Order mappedOrder = new Order(order.getDirection(), persistentProperty.getName(), order.getNullHandling());
while (matcher.find()) {
iteratorSource.add(matcher.group(1));
}
String mappedPropertyPath = getMappedPropertyPath(rootEntity, iteratorSource);
if (mappedPropertyPath != null) {
Order mappedOrder = new Order(order.getDirection(), mappedPropertyPath, order.getNullHandling());
filteredOrders.add(order.isIgnoreCase() ? mappedOrder.ignoreCase() : mappedOrder);
}
}
return filteredOrders.isEmpty() ? null : new Sort(filteredOrders);
}
private String getMappedPropertyPath(PersistentEntity<?, ?> rootEntity, List<String> iteratorSource) {
List<String> persistentPropertyPath = mapPropertyPath(rootEntity, iteratorSource);
if (persistentPropertyPath.isEmpty()) {
return null;
}
return StringUtils.collectionToDelimitedString(persistentPropertyPath, ".");
}
private List<String> mapPropertyPath(PersistentEntity<?, ?> rootEntity, List<String> iteratorSource) {
List<String> persistentPropertyPath = new ArrayList<String>(iteratorSource.size());
TypedSegment typedSegment = TypedSegment.create(persistentEntities, objectMapper, rootEntity);
for (String field : iteratorSource) {
String fieldName = field.matches(ALL_UPPERCASE) ? field : StringUtils.uncapitalize(field);
if (!typedSegment.hasPersistentPropertyForField(fieldName)) {
return Collections.emptyList();
}
List<? extends PersistentProperty<?>> persistentProperties = typedSegment.getPersistentProperties(fieldName);
for (PersistentProperty<?> persistentProperty : persistentProperties) {
if (persistentProperty.isAssociation()) {
return Collections.emptyList();
}
persistentPropertyPath.add(persistentProperty.getName());
}
typedSegment = typedSegment.next(persistentProperties.get(persistentProperties.size() - 1));
}
return persistentPropertyPath;
}
}
/**
* A typed segment inside a Jackson property path. {@link TypedSegment} represents a segment in JSON field path to
* {@link PersistentProperty} mapping.
*
* @author Mark Paluch
*/
static class TypedSegment {
private final PersistentEntities persistentEntities;
private final ObjectMapper objectMapper;
private final PersistentEntity<?, ?> currentType;
private final MappedProperties currentProperties;
private final WrappedProperties currentWrappedProperties;
private TypedSegment(TypedSegment previous, PersistentEntity<?, ?> persistentEntity) {
this(previous.persistentEntities, previous.objectMapper, persistentEntity);
}
private TypedSegment(PersistentEntities persistentEntities, ObjectMapper objectMapper,
PersistentEntity<?, ?> persistentEntity) {
this.persistentEntities = persistentEntities;
this.objectMapper = objectMapper;
this.currentType = persistentEntity;
if (persistentEntity != null) {
this.currentProperties = MappedProperties.fromJacksonProperties(currentType, objectMapper);
this.currentWrappedProperties = WrappedProperties.fromJacksonProperties(persistentEntities, currentType,
objectMapper);
} else {
this.currentProperties = null;
this.currentWrappedProperties = null;
}
}
/**
* Creates the initial {@link TypedSegment} given {@link PersistentEntities}, {@link ObjectMapper} and
* {@link PersistentEntity}.
*
* @param persistentEntities must not be {@literal null}.
* @param objectMapper must not be {@literal null}.
* @param rootEntity the initial entity to start mapping from, must not be {@literal null}.
* @return
*/
public static TypedSegment create(PersistentEntities persistentEntities, ObjectMapper objectMapper,
PersistentEntity rootEntity) {
Assert.notNull(persistentEntities, "PersistentEntities must not be null!");
Assert.notNull(objectMapper, "ObjectMapper must not be null!");
Assert.notNull(rootEntity, "PersistentEntity must not be null!");
return new TypedSegment(persistentEntities, objectMapper, rootEntity);
}
/**
* Continue mapping by providing the next {@link PersistentProperty}.
*
* @param persistentProperty must not be {@literal null}.
* @return
*/
public TypedSegment next(PersistentProperty persistentProperty) {
Assert.notNull(persistentProperty, "PersistentProperty must not be null!");
PersistentEntity<?, ?> persistentEntity = persistentEntities.getPersistentEntity(persistentProperty.getType());
return new TypedSegment(this, persistentEntity);
}
protected boolean hasPersistentPropertyForField(String fieldName) {
return currentType != null && (currentProperties.hasPersistentPropertyForField(fieldName)
|| currentWrappedProperties.hasPersistentPropertiesForField(fieldName));
}
protected List<? extends PersistentProperty<?>> getPersistentProperties(String fieldName) {
if (currentWrappedProperties.hasPersistentPropertiesForField(fieldName)) {
return currentWrappedProperties.getPersistentProperties(fieldName);
}
return Collections.singletonList(currentProperties.getPersistentProperty(fieldName));
}
}
}

View File

@@ -0,0 +1,229 @@
/*
* 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 lombok.NonNull;
import lombok.RequiredArgsConstructor;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.context.PersistentEntities;
import org.springframework.util.Assert;
import com.fasterxml.jackson.annotation.JsonUnwrapped;
import com.fasterxml.jackson.databind.AnnotationIntrospector;
import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.introspect.AnnotatedMember;
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.introspect.JacksonAnnotationIntrospector;
import com.fasterxml.jackson.databind.util.NameTransformer;
/**
* Simple value object to capture a mapping of Jackson mapped field names using
* {@link com.fasterxml.jackson.annotation.JsonUnwrapped} and {@link PersistentProperty} instances.
*
* @author Mark Paluch
* @since 2.6
*/
class WrappedProperties {
private static final ClassIntrospector INTROSPECTOR = new BasicClassIntrospector();
private static final AnnotationIntrospector ANNOTATION_INTROSPECTOR = new JacksonAnnotationIntrospector();
private final Map<String, List<PersistentProperty<?>>> fieldNameToProperties;
/**
* Creates a new {@link WrappedProperties} instance for the given {@code fieldNameToProperties}.
*
* @param fieldNameToProperties must not be {@literal null}.
*/
private WrappedProperties(Map<String, List<PersistentProperty<?>>> fieldNameToProperties) {
this.fieldNameToProperties = new HashMap<String, List<PersistentProperty<?>>>(fieldNameToProperties);
}
/**
* Creates {@link WrappedProperties} for the given {@link PersistentEntities} and {@link PersistentEntity}.
*
* @param persistentEntities must not be {@literal null}.
* @param entity must not be {@literal null}.
* @param mapper must not be {@literal null}.
* @return
*/
public static WrappedProperties fromJacksonProperties(PersistentEntities persistentEntities,
PersistentEntity<?, ?> entity, ObjectMapper mapper) {
Assert.notNull(entity, "PersistentEntity must not be null!");
JacksonUnwrappedPropertiesResolver resolver = new JacksonUnwrappedPropertiesResolver(persistentEntities, mapper);
return new WrappedProperties(resolver.findUnwrappedPropertyPaths(entity.getType()));
}
/**
* @param fieldName must not be empty or {@literal null}.
* @return {@literal true} if the field name resolves to a {@literal PersistentProperty}.
*/
public boolean hasPersistentPropertiesForField(String fieldName) {
Assert.hasText(fieldName, "Field name must not be null or empty!");
return fieldNameToProperties.containsKey(fieldName);
}
/**
* @param fieldName must not be empty or {@literal null}.
* @return
*/
public List<PersistentProperty<?>> getPersistentProperties(String fieldName) {
Assert.hasText(fieldName, "Field name must not be null or empty!");
return hasPersistentPropertiesForField(fieldName)
? Collections.unmodifiableList(fieldNameToProperties.get(fieldName))
: Collections.<PersistentProperty<?>> emptyList();
}
/**
* This class resolves {@code @JsonUnwrapped} field names to a list of involved {@link PersistentProperty properties}.
*
* @author Mark Paluch
*/
@RequiredArgsConstructor
static class JacksonUnwrappedPropertiesResolver {
final @NonNull PersistentEntities persistentEntities;
final @NonNull ObjectMapper mapper;
/**
* Resolve {@code @JsonUnwrapped} field names to a list of involved {@link PersistentProperty properties}.
*
* @param type must not be {@literal null}.
* @return
*/
public Map<String, List<PersistentProperty<?>>> findUnwrappedPropertyPaths(Class<?> type) {
Assert.notNull(type, "Type must not be null!");
return findUnwrappedPropertyPaths(type, NameTransformer.NOP, false);
}
private Map<String, List<PersistentProperty<?>>> findUnwrappedPropertyPaths(Class<?> type,
NameTransformer nameTransformer, boolean considerRegularProperties) {
PersistentEntity<?, ?> entity = persistentEntities.getPersistentEntity(type);
if (entity == null) {
return Collections.emptyMap();
}
Map<String, List<PersistentProperty<?>>> mapping = new HashMap<String, List<PersistentProperty<?>>>();
for (BeanPropertyDefinition property : getMappedProperties(entity)) {
AnnotatedMember annotatedMember = findAnnotatedMember(property);
PersistentProperty<?> persistentProperty = entity.getPersistentProperty(property.getInternalName());
if (isJsonUnwrapped(annotatedMember)) {
mapping.putAll(findUnwrappedPropertyPaths(nameTransformer, annotatedMember, persistentProperty));
} else if (considerRegularProperties) {
mapping.put(nameTransformer.transform(property.getName()),
Collections.<PersistentProperty<?>> singletonList(persistentProperty));
}
}
return mapping;
}
private Map<String, List<PersistentProperty<?>>> findUnwrappedPropertyPaths(NameTransformer nameTransformer,
AnnotatedMember annotatedMember, PersistentProperty<?> persistentProperty) {
Map<String, List<PersistentProperty<?>>> mapping = new HashMap<String, List<PersistentProperty<?>>>();
NameTransformer propertyNameTransformer = NameTransformer.chainedTransformer(nameTransformer,
ANNOTATION_INTROSPECTOR.findUnwrappingNameTransformer(annotatedMember));
Map<String, List<PersistentProperty<?>>> nestedProperties = findUnwrappedPropertyPaths(
annotatedMember.getRawType(), propertyNameTransformer, true);
for (String key : nestedProperties.keySet()) {
List<PersistentProperty<?>> persistentProperties = new ArrayList<PersistentProperty<?>>();
persistentProperties.add(persistentProperty);
persistentProperties.addAll(nestedProperties.get(key));
mapping.put(key, persistentProperties);
}
return mapping;
}
private List<BeanPropertyDefinition> getMappedProperties(PersistentEntity<?, ?> entity) {
List<BeanPropertyDefinition> properties = getBeanDescription(entity.getType()).findProperties();
List<BeanPropertyDefinition> withInternalName = new ArrayList<BeanPropertyDefinition>(properties.size());
for (BeanPropertyDefinition property : properties) {
AnnotatedMember annotatedMember = findAnnotatedMember(property);
if (annotatedMember == null || entity.getPersistentProperty(property.getInternalName()) == null) {
continue;
}
withInternalName.add(property);
}
return withInternalName;
}
private AnnotatedMember findAnnotatedMember(BeanPropertyDefinition property) {
if (property.getPrimaryMember() != null) {
return property.getPrimaryMember();
}
if (property.getGetter() != null) {
return property.getGetter();
}
if (property.getSetter() != null) {
return property.getSetter();
}
return null;
}
private static boolean isJsonUnwrapped(AnnotatedMember primaryMember) {
return primaryMember.hasAnnotation(JsonUnwrapped.class)
&& primaryMember.getAnnotation(JsonUnwrapped.class).enabled();
}
private BeanDescription getBeanDescription(Class<?> type) {
return INTROSPECTOR.forDeserialization(mapper.getDeserializationConfig(), mapper.constructType(type),
mapper.getDeserializationConfig());
}
}
}

View File

@@ -19,6 +19,7 @@ import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import java.lang.reflect.Method;
import java.util.List;
import org.springframework.data.repository.support.Repositories;
import org.springframework.data.rest.core.mapping.ResourceMappings;
@@ -35,7 +36,7 @@ import org.springframework.web.context.request.NativeWebRequest;
*
* @author Mark Paluch
* @author Oliver Gierke
* @since 2.6, 2.5.3
* @since 2.6
*/
@RequiredArgsConstructor(staticName = "of")
public class DomainClassResolver {
@@ -49,8 +50,7 @@ public class DomainClassResolver {
*
* @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.
* @return domain type that is associated with this request or {@literal null} if no domain class can be resolved.
*/
public Class<?> resolve(Method method, NativeWebRequest webRequest) {
@@ -61,7 +61,14 @@ public class DomainClassResolver {
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));
List<String> pathSegments = UriUtils.getPathSegments(method);
if (!pathSegments.isEmpty()) {
repositoryKey = pathSegments.get(0);
}
}
if (!StringUtils.hasText(repositoryKey)) {
return null;
}
for (Class<?> domainType : repositories) {
@@ -73,7 +80,6 @@ public class DomainClassResolver {
}
}
throw new IllegalArgumentException(
String.format("Could not resolve an exported domain type for %s.", repositoryKey));
return null;
}
}

View File

@@ -16,16 +16,19 @@
package org.springframework.data.rest.webmvc.util;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Map;
import org.springframework.hateoas.core.AnnotationMappingDiscoverer;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.util.UriComponentsBuilder;
/**
* Utility methods to work with requests and URIs.
*
* @author Oliver Gierke
* @author Mark Paluch
*/
public abstract class UriUtils {
@@ -37,8 +40,8 @@ public abstract class UriUtils {
* Returns the value for the mapping variable with the given name.
*
* @param variable must not be {@literal null} or empty.
* @param parameter
* @param request
* @param method must not be {@literal null}.
* @param lookupPath
* @return
*/
public static String findMappingVariable(String variable, Method method, String lookupPath) {
@@ -57,4 +60,19 @@ public abstract class UriUtils {
return null;
}
/**
* Returns the mapping path segments request mapping.
*
* @param method must not be {@literal null}.
* @return
*/
public static List<String> getPathSegments(Method method) {
Assert.notNull(method, "Method must not be null!");
String mapping = DISCOVERER.getMapping(method.getDeclaringClass(), method);
return UriComponentsBuilder.fromPath(mapping).build().getPathSegments();
}
}

View File

@@ -18,12 +18,19 @@ package org.springframework.data.rest.webmvc.json;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import java.util.Collections;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import org.springframework.data.annotation.Reference;
import org.springframework.data.domain.Sort;
import org.springframework.data.keyvalue.core.mapping.context.KeyValueMappingContext;
import org.springframework.data.mapping.context.PersistentEntities;
import org.springframework.data.rest.webmvc.json.JacksonMappingAwareSortTranslator.SortTranslator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonUnwrapped;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
@@ -35,7 +42,10 @@ import com.fasterxml.jackson.databind.ObjectMapper;
*/
public class SortTranslatorUnitTests {
private ObjectMapper objectMapper = new ObjectMapper();
private KeyValueMappingContext mappingContext;
private PersistentEntities persistentEntities;
private SortTranslator sortTranslator;
@Before
public void setUp() {
@@ -43,6 +53,11 @@ public class SortTranslatorUnitTests {
mappingContext = new KeyValueMappingContext();
mappingContext.getPersistentEntity(Plain.class);
mappingContext.getPersistentEntity(WithJsonProperty.class);
mappingContext.getPersistentEntity(UnwrapEmbedded.class);
mappingContext.getPersistentEntity(MultiUnwrapped.class);
persistentEntities = new PersistentEntities(Collections.singleton(mappingContext));
sortTranslator = new SortTranslator(persistentEntities, objectMapper);
}
/**
@@ -51,10 +66,8 @@ public class SortTranslatorUnitTests {
@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"));
Sort translatedSort = sortTranslator.translateSort(new Sort("hello", "name"),
mappingContext.getPersistentEntity(Plain.class));
assertThat(translatedSort.getOrderFor("hello"), is(nullValue()));
assertThat(translatedSort.getOrderFor("name"), is(notNullValue()));
@@ -66,10 +79,8 @@ public class SortTranslatorUnitTests {
@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"));
Sort translatedSort = sortTranslator.translateSort(new Sort("hello", "world"),
mappingContext.getPersistentEntity(Plain.class));
assertThat(translatedSort, is(nullValue()));
}
@@ -80,10 +91,8 @@ public class SortTranslatorUnitTests {
@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"));
Sort translatedSort = sortTranslator.translateSort(new Sort("hello", "foo"),
mappingContext.getPersistentEntity(WithJsonProperty.class));
assertThat(translatedSort.getOrderFor("hello"), is(nullValue()));
assertThat(translatedSort.getOrderFor("name"), is(notNullValue()));
@@ -95,19 +104,122 @@ public class SortTranslatorUnitTests {
@Test
public void shouldJacksonFieldNameForMapping() {
MappedProperties mappedProperties = MappedProperties
.fromJacksonProperties(mappingContext.getPersistentEntity(WithJsonProperty.class), new ObjectMapper());
Sort translatedSort = new JacksonMappingAwareSortTranslator.SortTranslator(mappedProperties)
.translateSort(new Sort("name"));
Sort translatedSort = sortTranslator.translateSort(new Sort("name"),
mappingContext.getPersistentEntity(WithJsonProperty.class));
assertThat(translatedSort, is(nullValue()));
}
/**
* @see DATAREST-910
*/
@Test
public void shouldMapKnownNestedProperties() {
Sort translatedSort = sortTranslator.translateSort(
new Sort("embedded.name", "embedded.collection", "embedded.someInterface"),
mappingContext.getPersistentEntity(Plain.class));
assertThat(translatedSort.getOrderFor("embedded.name"), is(notNullValue()));
assertThat(translatedSort.getOrderFor("embedded.collection"), is(notNullValue()));
assertThat(translatedSort.getOrderFor("embedded.someInterface"), is(notNullValue()));
}
/**
* @see DATAREST-910
*/
@Test
public void shouldSkipWrongNestedProperties() {
Sort translatedSort = sortTranslator.translateSort(new Sort("embedded.unknown"),
mappingContext.getPersistentEntity(Plain.class));
assertThat(translatedSort, is(nullValue()));
}
/**
* @see DATAREST-910
*/
@Test
public void shouldSkipKnownAssociationProperties() {
Sort translatedSort = sortTranslator.translateSort(new Sort("refEmbedded.name"),
mappingContext.getPersistentEntity(Plain.class));
assertThat(translatedSort, is(nullValue()));
}
/**
* @see DATAREST-910
*/
@Test
public void shouldJacksonFieldNameForNestedFieldMapping() {
Sort translatedSort = sortTranslator.translateSort(new Sort("em.foo"),
mappingContext.getPersistentEntity(WithJsonProperty.class));
assertThat(translatedSort.getOrderFor("embeddedWithJsonProperty.bar"), is(notNullValue()));
}
/**
* @see DATAREST-910
*/
@Test
public void shouldTranslatePathForSingleLevelJsonUnwrappedObject() {
Sort translatedSort = sortTranslator.translateSort(new Sort("un-name"),
mappingContext.getPersistentEntity(UnwrapEmbedded.class));
assertThat(translatedSort.getOrderFor("embedded.name"), is(notNullValue()));
}
/**
* @see DATAREST-910
*/
@Test
public void shouldTranslatePathForMultiLevelLevelJsonUnwrappedObject() {
Sort translatedSort = sortTranslator.translateSort(new Sort("un-name", "burrito.un-name"),
mappingContext.getPersistentEntity(MultiUnwrapped.class));
assertThat(translatedSort.getOrderFor("anotherWrap.embedded.name"), is(notNullValue()));
assertThat(translatedSort.getOrderFor("burrito.embedded.name"), is(notNullValue()));
}
static class Plain {
public String name;
public Embedded embedded;
@Reference public Embedded refEmbedded;
}
static class UnwrapEmbedded {
@JsonUnwrapped(prefix = "un-") public Embedded embedded;
}
static class MultiUnwrapped {
public String name;
@JsonUnwrapped public UnwrapEmbedded anotherWrap;
public UnwrapEmbedded burrito;
}
static class Embedded {
public String name;
public List<String> collection;
public SomeInterface someInterface;
}
static class WithJsonProperty {
public @JsonProperty("foo") String name;
@JsonProperty("foo") public String name;
@JsonProperty("em") public EmbeddedWithJsonProperty embeddedWithJsonProperty;
}
static class EmbeddedWithJsonProperty {
@JsonProperty("foo") public String bar;
}
static interface SomeInterface {}
}

View File

@@ -0,0 +1,207 @@
/*
* 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.MatcherAssert.*;
import static org.hamcrest.Matchers.*;
import java.util.Collections;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import org.springframework.data.keyvalue.core.mapping.context.KeyValueMappingContext;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.context.PersistentEntities;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonUnwrapped;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* Unit tests for {@link WrappedProperties}.
*
* @author Mark Paluch
*/
public class WrappedPropertiesUnitTests {
private static final ObjectMapper MAPPER = new ObjectMapper();
private KeyValueMappingContext mappingContext;
private PersistentEntities persistentEntities;
@Before
public void setUp() {
mappingContext = new KeyValueMappingContext();
mappingContext.getPersistentEntity(MultiLevelNesting.class);
mappingContext.getPersistentEntity(SyntheticProperties.class);
persistentEntities = new PersistentEntities(Collections.singleton(mappingContext));
}
/**
* @see DATAREST-910
*/
@Test
public void wrappedPropertiesShouldConsiderSingleLevelUnwrapping() {
PersistentEntity<?, ?> persistentEntity = persistentEntities.getPersistentEntity(OneLevelNesting.class);
WrappedProperties wrappedProperties = WrappedProperties.fromJacksonProperties(persistentEntities, persistentEntity,
MAPPER);
assertThat(wrappedProperties.hasPersistentPropertiesForField("street"), is(true));
assertThat(wrappedProperties.hasPersistentPropertiesForField("one"), is(false));
List<PersistentProperty<?>> street = wrappedProperties.getPersistentProperties("street");
PersistentProperty<?> addressProperty = persistentEntity.getPersistentProperty("address");
PersistentProperty<?> streetProperty = persistentEntities.getPersistentEntity(Address.class)
.getPersistentProperty("street");
assertThat(street, contains(addressProperty, streetProperty));
}
/**
* @see DATAREST-910
*/
@Test
public void wrappedPropertiesShouldConsiderMultiLevelUnwrapping() {
PersistentEntity<?, ?> persistentEntity = persistentEntities.getPersistentEntity(MultiLevelNesting.class);
WrappedProperties wrappedProperties = WrappedProperties.fromJacksonProperties(persistentEntities, persistentEntity,
new ObjectMapper());
assertThat(wrappedProperties.hasPersistentPropertiesForField("pre-one-post"), is(true));
assertThat(wrappedProperties.hasPersistentPropertiesForField("pre-street-post"), is(true));
assertThat(wrappedProperties.hasPersistentPropertiesForField("nested"), is(false));
List<PersistentProperty<?>> street = wrappedProperties.getPersistentProperties("pre-street-post");
PersistentProperty<?> oneLevelNestingProperty = persistentEntity.getPersistentProperty("unwrapped");
PersistentProperty<?> addressProperty = persistentEntities.getPersistentEntity(OneLevelNesting.class)
.getPersistentProperty("address");
PersistentProperty<?> streetProperty = persistentEntities.getPersistentEntity(Address.class)
.getPersistentProperty("street");
assertThat(street, contains(oneLevelNestingProperty, addressProperty, streetProperty));
}
/**
* @see DATAREST-910
*/
@Test
public void wrappedPropertiesShouldConsiderJacksonFieldNames() {
PersistentEntity<?, ?> persistentEntity = persistentEntities.getPersistentEntity(MultiLevelNesting.class);
WrappedProperties wrappedProperties = WrappedProperties.fromJacksonProperties(persistentEntities, persistentEntity,
new ObjectMapper());
assertThat(wrappedProperties.hasPersistentPropertiesForField("pre-zip-post"), is(true));
}
/**
* @see DATAREST-910
*/
@Test
public void wrappedPropertiesShouldIgnoreIgnoredJacksonFields() {
PersistentEntity<?, ?> persistentEntity = persistentEntities.getPersistentEntity(MultiLevelNesting.class);
WrappedProperties wrappedProperties = WrappedProperties.fromJacksonProperties(persistentEntities, persistentEntity,
new ObjectMapper());
assertThat(wrappedProperties.hasPersistentPropertiesForField("pre-street-ignored"), is(false));
}
/**
* @see DATAREST-910
*/
@Test
public void wrappedPropertiesShouldIgnoreSyntheticProperties() {
PersistentEntity<?, ?> persistentEntity = persistentEntities.getPersistentEntity(SyntheticProperties.class);
WrappedProperties wrappedProperties = WrappedProperties.fromJacksonProperties(persistentEntities, persistentEntity,
new ObjectMapper());
assertThat(wrappedProperties.hasPersistentPropertiesForField("street"), is(false));
}
@Data
@AllArgsConstructor
@NoArgsConstructor
static class OneLevelNesting {
String one;
@JsonUnwrapped Address address;
}
static class SyntheticProperties {
@JsonUnwrapped
Address getUnwrapped() {
return null;
}
MultiLevelNesting getSynthetic() {
return null;
}
@JsonUnwrapped
void setWrapped(OneLevelNesting address) {}
}
/**
* <pre>
* <code>
{
"multi": "multi",
"pre-one-post": "one",
"pre-street-post": "street",
"pre-zip-post": "zip",
"nested": {
"one": "one",
"street": "street",
"zip": "zip"
}
}
</code>
* </pre>
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
static class MultiLevelNesting {
String multi;
@JsonUnwrapped(prefix = "pre-", suffix = "-post") OneLevelNesting unwrapped;
@JsonIgnore @JsonUnwrapped(prefix = "pre-", suffix = "-ignored") OneLevelNesting ignored;
@JsonUnwrapped(enabled = false) OneLevelNesting nested;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
static class Address {
String street;
@JsonProperty("zip") String zipCode;
}
}

View File

@@ -0,0 +1,72 @@
/*
* 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.util;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import java.lang.reflect.Method;
import java.util.List;
import org.junit.Test;
import org.springframework.util.ClassUtils;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* Unit tests for {@link UriUtils}.
*
* @author Mark Paluch
*/
public class UriUtilsUnitTests {
/**
* @see DATAREST-910
*/
@Test
public void pathSegmentsShouldDiscoverPathUsingMethodMapping() throws Exception {
Method method = ClassUtils.getMethod(MappedMethod.class, "method");
List<String> pathSegments = UriUtils.getPathSegments(method);
assertThat(pathSegments, hasItems("hello", "world"));
}
/**
* @see DATAREST-910
*/
@Test
public void pathSegmentsShouldDiscoverPathUsingTypeAndMethodMapping() throws Exception {
Method method = ClassUtils.getMethod(MappedClassAndMethod.class, "method");
List<String> pathSegments = UriUtils.getPathSegments(method);
assertThat(pathSegments, hasItems("hello", "world"));
}
static class MappedMethod {
@RequestMapping("hello/world")
public void method() {}
}
@RequestMapping("hello")
static class MappedClassAndMethod {
@RequestMapping("world")
public void method() {}
}
}