/*
* Copyright 2016-2017 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.List;
import java.util.Optional;
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.mapping.Associations;
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 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
* @author Oliver Gierke
* @since 2.6
*/
@RequiredArgsConstructor
public class JacksonMappingAwareSortTranslator {
private final Repositories repositories;
private final DomainClassResolver domainClassResolver;
private final SortTranslator sortTranslator;
/**
* Creates a new {@link JacksonMappingAwareSortTranslator} for the given {@link ObjectMapper}, {@link Repositories},
* {@link DomainClassResolver} and {@link PersistentEntities}.
*
* @param objectMapper must not be {@literal null}.
* @param repositories must not be {@literal null}.
* @param domainClassResolver must not be {@literal null}.
* @param persistentEntities must not be {@literal null}.
* @param associations must not be {@literal null}.
*/
public JacksonMappingAwareSortTranslator(ObjectMapper objectMapper, Repositories repositories,
DomainClassResolver domainClassResolver, PersistentEntities persistentEntities, Associations associations) {
Assert.notNull(repositories, "Repositories must not be null!");
Assert.notNull(domainClassResolver, "DomainClassResolver must not be null!");
Assert.notNull(associations, "Associations must not be null!");
this.repositories = repositories;
this.domainClassResolver = domainClassResolver;
this.sortTranslator = new SortTranslator(persistentEntities, objectMapper, associations);
}
/**
* 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 translateSort(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);
if (domainClass == null) {
return input;
}
PersistentEntity<?, ?> persistentEntity = repositories.getPersistentEntity(domainClass);
return sortTranslator.translateSort(input, persistentEntity);
}
/**
* Translates {@link Sort} orders from Jackson-mapped field names to {@link PersistentProperty} names.
*
* @author Mark Paluch
* @author Oliver Gierke
* @since 2.6
*/
@RequiredArgsConstructor
public static class SortTranslator {
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;
private final @NonNull Associations associations;
/**
* 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.
*/
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) {
List<String> iteratorSource = new ArrayList<String>();
Matcher matcher = SPLITTER.matcher("_" + order.getProperty());
while (matcher.find()) {
iteratorSource.add(matcher.group(1));
}
String mappedPropertyPath = getMappedPropertyPath(rootEntity, iteratorSource);
if (mappedPropertyPath != null) {
filteredOrders.add(order.withProperty(mappedPropertyPath));
}
}
return filteredOrders.isEmpty() ? Sort.unsorted() : Sort.by(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 (associations.isLinkableAssociation(persistentProperty)) {
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 Optional<PersistentEntity<?, ? extends PersistentProperty<?>>> currentType;
private final MappedProperties currentProperties;
private final WrappedProperties currentWrappedProperties;
private TypedSegment(TypedSegment previous,
Optional<PersistentEntity<?, ? extends PersistentProperty<?>>> persistentEntity) {
this(previous.persistentEntities, previous.objectMapper, persistentEntity);
}
private TypedSegment(PersistentEntities persistentEntities, ObjectMapper objectMapper,
Optional<PersistentEntity<?, ? extends PersistentProperty<?>>> persistentEntity) {
this.persistentEntities = persistentEntities;
this.objectMapper = objectMapper;
this.currentType = persistentEntity;
this.currentProperties = persistentEntity//
.map(it -> MappedProperties.fromJacksonProperties(it, objectMapper))//
.orElseGet(() -> MappedProperties.none());
this.currentWrappedProperties = persistentEntity//
.map(it -> WrappedProperties.fromJacksonProperties(persistentEntities, it, objectMapper))//
.orElseGet(() -> WrappedProperties.none());
}
/**
* 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, Optional.of(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!");
return new TypedSegment(this, persistentEntities.getPersistentEntity(persistentProperty.getType()));
}
private boolean hasPersistentPropertyForField(String fieldName) {
return currentType != null && (currentProperties.hasPersistentPropertyForField(fieldName)
|| currentWrappedProperties.hasPersistentPropertiesForField(fieldName));
}
private List<? extends PersistentProperty<?>> getPersistentProperties(String fieldName) {
if (currentWrappedProperties.hasPersistentPropertiesForField(fieldName)) {
return currentWrappedProperties.getPersistentProperties(fieldName);
}
return Collections.singletonList(currentProperties.getPersistentProperty(fieldName));
}
}
}