/*
* Copyright 2014-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.projection;
import java.lang.reflect.Array;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.core.CollectionFactory;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.data.util.ClassTypeInformation;
import org.springframework.data.util.TypeInformation;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
/**
* {@link MethodInterceptor} to delegate the invocation to a different {@link MethodInterceptor} but creating a
* projecting proxy in case the returned value is not of the return type of the invoked method.
*
* @author Oliver Gierke
* @since 1.10
*/
class ProjectingMethodInterceptor implements MethodInterceptor {
private final ProjectionFactory factory;
private final MethodInterceptor delegate;
private final ConversionService conversionService;
/**
* Creates a new {@link ProjectingMethodInterceptor} using the given {@link ProjectionFactory} and delegate
* {@link MethodInterceptor}.
*
* @param factory the {@link ProjectionFactory} to use to create projections if types do not match, must not be
* {@literal null}..
* @param delegate the {@link MethodInterceptor} to trigger to create the source value, must not be {@literal null}..
*/
public ProjectingMethodInterceptor(ProjectionFactory factory, MethodInterceptor delegate) {
Assert.notNull(factory, "ProjectionFactory must not be null!");
Assert.notNull(delegate, "Delegate MethodInterceptor must not be null!");
this.factory = factory;
this.delegate = delegate;
this.conversionService = new DefaultConversionService();
}
/*
* (non-Javadoc)
* @see org.aopalliance.intercept.MethodInterceptor#invoke(org.aopalliance.intercept.MethodInvocation)
*/
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
Object result = delegate.invoke(invocation);
if (result == null) {
return null;
}
TypeInformation<?> type = ClassTypeInformation.fromReturnTypeOf(invocation.getMethod());
Class<?> rawType = type.getType();
if (type.isCollectionLike() && !ClassUtils.isPrimitiveArray(rawType)) {
return projectCollectionElements(asCollection(result), type);
} else if (type.isMap()) {
return projectMapValues((Map<?, ?>) result, type);
} else if (conversionRequiredAndPossible(result, rawType)) {
return conversionService.convert(result, rawType);
} else {
return getProjection(result, rawType);
}
}
/**
* Creates projections of the given {@link Collection}'s elements if necessary and returns a new collection containing
* the projection results.
*
* @param sources must not be {@literal null}.
* @param type must not be {@literal null}.
* @return
*/
private Object projectCollectionElements(Collection<?> sources, TypeInformation<?> type) {
Class<?> rawType = type.getType();
Collection<Object> result = CollectionFactory.createCollection(rawType.isArray() ? List.class : rawType,
sources.size());
for (Object source : sources) {
result.add(getProjection(source, type.getRequiredComponentType().getType()));
}
if (rawType.isArray()) {
return result.toArray((Object[]) Array.newInstance(type.getRequiredComponentType().getType(), result.size()));
}
return result;
}
/**
* Creates projections of the given {@link Map}'s values if necessary and returns an new {@link Map} with the handled
* values.
*
* @param sources must not be {@literal null}.
* @param type must not be {@literal null}.
* @return
*/
private Map<Object, Object> projectMapValues(Map<?, ?> sources, TypeInformation<?> type) {
Map<Object, Object> result = CollectionFactory.createMap(type.getType(), sources.size());
for (Entry<?, ?> source : sources.entrySet()) {
result.put(source.getKey(), getProjection(source.getValue(), type.getRequiredMapValueType().getType()));
}
return result;
}
private Object getProjection(Object result, Class<?> returnType) {
return result == null || ClassUtils.isAssignable(returnType, result.getClass()) ? result
: factory.createProjection(returnType, result);
}
/**
* Returns whether the source object needs to be converted to the given target type and whether we can convert it at
* all.
*
* @param source can be {@literal null}.
* @param targetType must not be {@literal null}.
* @return
*/
private boolean conversionRequiredAndPossible(Object source, Class<?> targetType) {
if (source == null || targetType.isInstance(source)) {
return false;
}
return conversionService.canConvert(source.getClass(), targetType);
}
/**
* Turns the given value into a {@link Collection}. Will turn an array into a collection an wrap all other values into
* a single-element collection.
*
* @param source must not be {@literal null}.
* @return
*/
private static Collection<?> asCollection(Object source) {
Assert.notNull(source, "Source object must not be null!");
if (source instanceof Collection) {
return (Collection<?>) source;
} else if (source.getClass().isArray()) {
return Arrays.asList(ObjectUtils.toObjectArray(source));
} else {
return Collections.singleton(source);
}
}
}