package org.springframework.data.repository.query;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;
import javax.annotation.Nonnull;
import org.springframework.core.CollectionFactory;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.data.domain.Slice;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.repository.util.ReactiveWrapperConverters;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class ResultProcessor {
private final QueryMethod method;
private final ProjectingConverter converter;
private final ProjectionFactory factory;
private final ReturnedType type;
ResultProcessor(QueryMethod method, ProjectionFactory factory) {
this(method, factory, method.getReturnedObjectType());
}
private ResultProcessor(QueryMethod method, ProjectionFactory factory, Class<?> type) {
Assert.notNull(method, "QueryMethod must not be null!");
Assert.notNull(factory, "ProjectionFactory must not be null!");
Assert.notNull(type, "Type must not be null!");
this.method = method;
this.type = ReturnedType.of(type, method.getDomainClass(), factory);
this.converter = new ProjectingConverter(this.type, factory);
this.factory = factory;
}
public ResultProcessor withDynamicProjection(ParameterAccessor accessor) {
Assert.notNull(accessor, "Parameter accessor must not be null!");
Class<?> projection = accessor.findDynamicProjection();
return projection == null
? this
: withType(projection);
}
public ReturnedType getReturnedType() {
return type;
}
@Nullable
public <T> T processResult(@Nullable Object source) {
return processResult(source, NoOpConverter.INSTANCE);
}
@Nullable
@SuppressWarnings("unchecked")
public <T> T processResult(@Nullable Object source, Converter<Object, Object> preparingConverter) {
if (source == null || type.isInstance(source) || !type.isProjecting()) {
return (T) source;
}
Assert.notNull(preparingConverter, "Preparing converter must not be null!");
ChainingConverter converter = ChainingConverter.of(type.getReturnedType(), preparingConverter).and(this.converter);
if (source instanceof Slice && method.isPageQuery() || method.isSliceQuery()) {
return (T) ((Slice<?>) source).map(converter::convert);
}
if (source instanceof Collection && method.isCollectionQuery()) {
Collection<?> collection = (Collection<?>) source;
Collection<Object> target = createCollectionFor(collection);
for (Object columns : collection) {
target.add(type.isInstance(columns) ? columns : converter.convert(columns));
}
return (T) target;
}
if (source instanceof Stream && method.isStreamQuery()) {
return (T) ((Stream<Object>) source).map(t -> type.isInstance(t) ? t : converter.convert(t));
}
if (ReactiveWrapperConverters.supports(source.getClass())) {
return (T) ReactiveWrapperConverters.map(source, converter::convert);
}
return (T) converter.convert(source);
}
private ResultProcessor withType(Class<?> type) {
ReturnedType returnedType = ReturnedType.of(type, method.getDomainClass(), factory);
return new ResultProcessor(method, converter.withType(returnedType), factory, returnedType);
}
private static Collection<Object> createCollectionFor(Collection<?> source) {
try {
return CollectionFactory.createCollection(source.getClass(), source.size());
} catch (RuntimeException o_O) {
return CollectionFactory.createApproximateCollection(source, source.size());
}
}
@RequiredArgsConstructor(staticName = "of")
private static class ChainingConverter implements Converter<Object, Object> {
private final @NonNull Class<?> targetType;
private final @NonNull Converter<Object, Object> delegate;
public ChainingConverter and(final Converter<Object, Object> converter) {
Assert.notNull(converter, "Converter must not be null!");
return new ChainingConverter(targetType, source -> {
Object intermediate = ChainingConverter.this.convert(source);
return intermediate == null || targetType.isInstance(intermediate) ? intermediate
: converter.convert(intermediate);
});
}
@Nullable
@Override
public Object convert(Object source) {
return delegate.convert(source);
}
}
private static enum NoOpConverter implements Converter<Object, Object> {
INSTANCE;
@Nonnull
@Override
public Object convert(Object source) {
return source;
}
}
@RequiredArgsConstructor
private static class ProjectingConverter implements Converter<Object, Object> {
private final @NonNull ReturnedType type;
private final @NonNull ProjectionFactory factory;
private final @NonNull ConversionService conversionService;
ProjectingConverter(ReturnedType type, ProjectionFactory factory) {
this(type, factory, DefaultConversionService.getSharedInstance());
}
ProjectingConverter withType(ReturnedType type) {
Assert.notNull(type, "ReturnedType must not be null!");
return new ProjectingConverter(type, factory, conversionService);
}
@Nullable
@Override
public Object convert(Object source) {
Class<?> targetType = type.getReturnedType();
if (targetType.isInterface()) {
return factory.createProjection(targetType, getProjectionTarget(source));
}
return conversionService.convert(source, targetType);
}
private Object getProjectionTarget(Object source) {
if (source != null && source.getClass().isArray()) {
source = Arrays.asList((Object[]) source);
}
if (source instanceof Collection) {
return toMap((Collection<?>) source, type.getInputProperties());
}
return source;
}
private static Map<String, Object> toMap(Collection<?> values, List<String> names) {
int i = 0;
Map<String, Object> result = new HashMap<>(values.size());
for (Object element : values) {
result.put(names.get(i++), element);
}
return result;
}
}
}