DATACMNS-630 - Integrate projection infrastructure from Spring Data REST.
Ported the projection infrastructure previously residing in Spring Data REST and extended it by defaulting to a Map-backed source to store and retrieve data. Separated out the SpEL based functionality mostly for SOC-reasons and easier testability. Related tickets: DATAREST-437, DATACMNS-618, DATACMNS-89.
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
* Copyright 2015 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.beans.PropertyDescriptor;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Map;
|
||||
|
||||
import org.aopalliance.intercept.MethodInterceptor;
|
||||
import org.aopalliance.intercept.MethodInvocation;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
/**
|
||||
* {@link MethodInterceptor} to support accessor methods to store and retrieve values from a {@link Map}.
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
* @since 1.10
|
||||
*/
|
||||
class MapAccessingMethodInterceptor implements MethodInterceptor {
|
||||
|
||||
private final Map<String, Object> map;
|
||||
|
||||
/**
|
||||
* Creates a new {@link MapAccessingMethodInterceptor} for the given {@link Map}.
|
||||
*
|
||||
* @param map must not be {@literal null}.
|
||||
*/
|
||||
public MapAccessingMethodInterceptor(Map<String, Object> map) {
|
||||
|
||||
Assert.notNull(map, "Map must not be null!");
|
||||
this.map = map;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.aopalliance.intercept.MethodInterceptor#invoke(org.aopalliance.intercept.MethodInvocation)
|
||||
*/
|
||||
@Override
|
||||
public Object invoke(MethodInvocation invocation) throws Throwable {
|
||||
|
||||
Method method = invocation.getMethod();
|
||||
|
||||
if (ReflectionUtils.isObjectMethod(method)) {
|
||||
return invocation.proceed();
|
||||
}
|
||||
|
||||
Accessor accessor = new Accessor(method);
|
||||
|
||||
if (accessor.isGetter()) {
|
||||
return map.get(accessor.getPropertyName());
|
||||
} else if (accessor.isSetter()) {
|
||||
map.put(accessor.getPropertyName(), invocation.getArguments()[0]);
|
||||
return null;
|
||||
}
|
||||
|
||||
throw new IllegalStateException("Should never get here!");
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper value to abstract an accessor.
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
private static final class Accessor {
|
||||
|
||||
private final PropertyDescriptor descriptor;
|
||||
private final Method method;
|
||||
|
||||
/**
|
||||
* Creates an {@link Accessor} for the given {@link Method}.
|
||||
*
|
||||
* @param method must not be {@literal null}.
|
||||
* @throws IllegalArgumentException in case the given method is not an accessor method.
|
||||
*/
|
||||
public Accessor(Method method) {
|
||||
|
||||
Assert.notNull(method, "Method must not be null!");
|
||||
|
||||
this.descriptor = BeanUtils.findPropertyForMethod(method);
|
||||
this.method = method;
|
||||
|
||||
Assert.notNull(descriptor, String.format("Invoked method %s is no accessor method!", method));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the acessor is a getter.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public boolean isGetter() {
|
||||
return method.equals(descriptor.getReadMethod());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the accessor is a setter.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public boolean isSetter() {
|
||||
return method.equals(descriptor.getWriteMethod());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the property this accessor handles.
|
||||
*
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
public String getPropertyName() {
|
||||
return descriptor.getName();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
/*
|
||||
* Copyright 2014-2015 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.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
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.data.util.ClassTypeInformation;
|
||||
import org.springframework.data.util.TypeInformation;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
/**
|
||||
* {@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;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/*
|
||||
* (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());
|
||||
|
||||
if (type.isCollectionLike()) {
|
||||
return projectCollectionElements(asCollection(result), type);
|
||||
} else if (type.isMap()) {
|
||||
return projectMapValues((Map<?, ?>) result, type);
|
||||
} else {
|
||||
return getProjection(result, type.getType());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 Collection<Object> projectCollectionElements(Collection<?> sources, TypeInformation<?> type) {
|
||||
|
||||
Collection<Object> result = CollectionFactory.createCollection(type.getType(), sources.size());
|
||||
|
||||
for (Object source : sources) {
|
||||
result.add(getProjection(source, type.getComponentType().getType()));
|
||||
}
|
||||
|
||||
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.getMapValueType().getType()));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private Object getProjection(Object result, Class<?> returnType) {
|
||||
return ClassUtils.isAssignable(returnType, result.getClass()) ? result : factory.createProjection(returnType,
|
||||
result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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((Object[]) source);
|
||||
} else {
|
||||
return Collections.singleton(source);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright 2014-2015 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.util.List;
|
||||
|
||||
/**
|
||||
* A factory to create projecting instances for other objects usually used to allow easy creation of representation
|
||||
* projections to define which properties of a domain objects shall be exported in which way.
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
* @since 1.10
|
||||
*/
|
||||
public interface ProjectionFactory {
|
||||
|
||||
/**
|
||||
* Creates a projection of the given type for the given source object. The individual mapping strategy is defined by
|
||||
* the implementations.
|
||||
*
|
||||
* @param projectionType the type to create, must not be {@literal null}.
|
||||
* @param source the object to create a projection for, can be {@literal null}
|
||||
* @return
|
||||
*/
|
||||
<T> T createProjection(Class<T> projectionType, Object source);
|
||||
|
||||
/**
|
||||
* Creates a pojection instance for the given type.
|
||||
*
|
||||
* @param projectionType the type to create, must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
<T> T createProjection(Class<T> projectionType);
|
||||
|
||||
/**
|
||||
* Returns the properties that will be consumed by the given projection type.
|
||||
*
|
||||
* @param projectionType must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
List<String> getInputProperties(Class<?> projectionType);
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* Copyright 2014-2015 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.beans.PropertyDescriptor;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import org.aopalliance.intercept.MethodInterceptor;
|
||||
import org.aopalliance.intercept.MethodInvocation;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.BeanWrapper;
|
||||
import org.springframework.data.util.DirectFieldAccessFallbackBeanWrapper;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
/**
|
||||
* Method interceptor to forward a delegation to bean property accessor methods to the property of a given target.
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
* @since 1.10
|
||||
*/
|
||||
class PropertyAccessingMethodInterceptor implements MethodInterceptor {
|
||||
|
||||
private final BeanWrapper target;
|
||||
|
||||
/**
|
||||
* Creates a new {@link PropertyAccessingMethodInterceptor} for the given target object.
|
||||
*
|
||||
* @param target must not be {@literal null}.
|
||||
*/
|
||||
public PropertyAccessingMethodInterceptor(Object target) {
|
||||
|
||||
Assert.notNull(target, "Proxy target must not be null!");
|
||||
this.target = new DirectFieldAccessFallbackBeanWrapper(target);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.aopalliance.intercept.MethodInterceptor#invoke(org.aopalliance.intercept.MethodInvocation)
|
||||
*/
|
||||
@Override
|
||||
public Object invoke(MethodInvocation invocation) throws Throwable {
|
||||
|
||||
Method method = invocation.getMethod();
|
||||
|
||||
if (ReflectionUtils.isObjectMethod(method)) {
|
||||
return invocation.proceed();
|
||||
}
|
||||
|
||||
PropertyDescriptor descriptor = BeanUtils.findPropertyForMethod(method);
|
||||
|
||||
if (descriptor == null) {
|
||||
throw new IllegalStateException("Invoked method is not a property accessor!");
|
||||
}
|
||||
|
||||
return target.getPropertyValue(descriptor.getName());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
/*
|
||||
* Copyright 2014-2105 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.beans.PropertyDescriptor;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.aopalliance.intercept.MethodInterceptor;
|
||||
import org.aopalliance.intercept.MethodInvocation;
|
||||
import org.springframework.aop.framework.Advised;
|
||||
import org.springframework.aop.framework.ProxyFactory;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
|
||||
/**
|
||||
* A {@link ProjectionFactory} to create JDK proxies to back interfaces and handle method invocations on them. By
|
||||
* default accessor methods are supported. In case the delegating lookups result in an object of different type that the
|
||||
* projection interface method's return type, another projection will be created to transparently mitigate between the
|
||||
* types.
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
* @see SpelAwareProxyProjectionFactory
|
||||
* @since 1.10
|
||||
*/
|
||||
class ProxyProjectionFactory implements ProjectionFactory {
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.rest.core.projection.ProjectionFactory#createProjection(java.lang.Object, java.lang.Class)
|
||||
*/
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> T createProjection(Class<T> projectionType, Object source) {
|
||||
|
||||
Assert.notNull(projectionType, "Projection type must not be null!");
|
||||
Assert.isTrue(projectionType.isInterface(), "Projection type must be an interface!");
|
||||
|
||||
if (source == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
ProxyFactory factory = new ProxyFactory();
|
||||
factory.setTarget(source);
|
||||
factory.setOpaque(true);
|
||||
factory.setInterfaces(projectionType, TargetClassAware.class);
|
||||
|
||||
factory.addAdvice(new TargetClassAwareMethodInterceptor(source.getClass()));
|
||||
factory.addAdvice(getMethodInterceptor(source, projectionType));
|
||||
|
||||
return (T) factory.getProxy(getClass().getClassLoader());
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.projection.ProjectionFactory#createProjection(java.lang.Class)
|
||||
*/
|
||||
@Override
|
||||
public <T> T createProjection(Class<T> projectionType) {
|
||||
|
||||
Assert.notNull(projectionType, "Projection type must not be null!");
|
||||
|
||||
return createProjection(projectionType, new HashMap<String, Object>());
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.projection.ProjectionFactory#getProperties(java.lang.Class)
|
||||
*/
|
||||
@Override
|
||||
public List<String> getInputProperties(Class<?> projectionType) {
|
||||
|
||||
Assert.notNull(projectionType, "Projection type must not be null!");
|
||||
|
||||
PropertyDescriptor[] descriptors = BeanUtils.getPropertyDescriptors(projectionType);
|
||||
List<String> result = new ArrayList<String>(descriptors.length);
|
||||
|
||||
for (PropertyDescriptor descriptor : descriptors) {
|
||||
if (isInputProperty(descriptor)) {
|
||||
result.add(descriptor.getName());
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link MethodInterceptor} to add to the proxy.
|
||||
*
|
||||
* @param source must not be {@literal null}.
|
||||
* @param projectionType must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private MethodInterceptor getMethodInterceptor(Object source, Class<?> projectionType) {
|
||||
|
||||
MethodInterceptor propertyInvocationInterceptor = source instanceof Map ? new MapAccessingMethodInterceptor(
|
||||
(Map<String, Object>) source) : new PropertyAccessingMethodInterceptor(source);
|
||||
|
||||
return new ProjectingMethodInterceptor(this, postProcessAccessorInterceptor(propertyInvocationInterceptor, source,
|
||||
projectionType));
|
||||
}
|
||||
|
||||
/**
|
||||
* Post-process the given {@link MethodInterceptor} for the given source instance and projection type. Default
|
||||
* implementation will simply return the given interceptor.
|
||||
*
|
||||
* @param interceptor will never be {@literal null}.
|
||||
* @param source will never be {@literal null}.
|
||||
* @param projectionType will never be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
protected MethodInterceptor postProcessAccessorInterceptor(MethodInterceptor interceptor, Object source,
|
||||
Class<?> projectionType) {
|
||||
return interceptor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the given {@link PropertyDescriptor} describes an input property for the projection, i.e. a
|
||||
* property that needs to be present on the source to be able to create reasonable projections for the type the
|
||||
* descritor was looked up on.
|
||||
*
|
||||
* @param descriptor will never be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
protected boolean isInputProperty(PropertyDescriptor descriptor) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extension of {@link org.springframework.aop.TargetClassAware} to be able to ignore the getter on JSON rendering.
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
public static interface TargetClassAware extends org.springframework.aop.TargetClassAware {
|
||||
|
||||
@JsonIgnore
|
||||
Class<?> getTargetClass();
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom {@link MethodInterceptor} to expose the proxy target class even if we set
|
||||
* {@link ProxyFactory#setOpaque(boolean)} to true to prevent properties on {@link Advised} to be rendered.
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
private static class TargetClassAwareMethodInterceptor implements MethodInterceptor {
|
||||
|
||||
private static final Method GET_TARGET_CLASS_METHOD;
|
||||
private final Class<?> targetClass;
|
||||
|
||||
static {
|
||||
try {
|
||||
GET_TARGET_CLASS_METHOD = TargetClassAware.class.getMethod("getTargetClass");
|
||||
} catch (NoSuchMethodException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link TargetClassAwareMethodInterceptor} with the given target class.
|
||||
*
|
||||
* @param targetClass must not be {@literal null}.
|
||||
*/
|
||||
public TargetClassAwareMethodInterceptor(Class<?> targetClass) {
|
||||
|
||||
Assert.notNull(targetClass, "Target class must not be null!");
|
||||
this.targetClass = targetClass;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.aopalliance.intercept.MethodInterceptor#invoke(org.aopalliance.intercept.MethodInvocation)
|
||||
*/
|
||||
@Override
|
||||
public Object invoke(MethodInvocation invocation) throws Throwable {
|
||||
|
||||
if (invocation.getMethod().equals(GET_TARGET_CLASS_METHOD)) {
|
||||
return targetClass;
|
||||
}
|
||||
|
||||
return invocation.proceed();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* Copyright 2015 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.beans.PropertyDescriptor;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.aopalliance.intercept.MethodInterceptor;
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.beans.factory.BeanFactoryAware;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.core.annotation.AnnotationUtils;
|
||||
import org.springframework.data.util.AnnotationDetectionMethodCallback;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
/**
|
||||
* A {@link ProxyProjectionFactory} that adds support to use {@link Value}-annotated methods on a projection interface
|
||||
* to evaluate the contained SpEL expression to define the outcome of the method call.
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
* @since 1.10
|
||||
*/
|
||||
public class SpelAwareProxyProjectionFactory extends ProxyProjectionFactory implements BeanFactoryAware {
|
||||
|
||||
private final Map<Class<?>, Boolean> typeCache = new HashMap<Class<?>, Boolean>();
|
||||
|
||||
private BeanFactory beanFactory;
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.beans.factory.BeanFactoryAware#setBeanFactory(org.springframework.beans.factory.BeanFactory)
|
||||
*/
|
||||
@Override
|
||||
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
|
||||
this.beanFactory = beanFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inspects the given target type for methods with {@link Value} annotations and caches the result. Will create a
|
||||
* {@link SpelEvaluatingMethodInterceptor} if an annotation was found or return the delegate as is if not.
|
||||
*
|
||||
* @param source The backing source object.
|
||||
* @param projectionType the proxy target type.
|
||||
* @param delegate the root {@link MethodInterceptor}.
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
protected MethodInterceptor postProcessAccessorInterceptor(MethodInterceptor interceptor, Object source,
|
||||
Class<?> projectionType) {
|
||||
|
||||
if (!typeCache.containsKey(projectionType)) {
|
||||
|
||||
AnnotationDetectionMethodCallback<Value> callback = new AnnotationDetectionMethodCallback<Value>(Value.class);
|
||||
ReflectionUtils.doWithMethods(projectionType, callback);
|
||||
|
||||
typeCache.put(projectionType, callback.hasFoundAnnotation());
|
||||
}
|
||||
|
||||
return typeCache.get(projectionType) ? new SpelEvaluatingMethodInterceptor(interceptor, source, beanFactory)
|
||||
: interceptor;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.projection.ProxyProjectionFactory#isProperty(java.beans.PropertyDescriptor)
|
||||
*/
|
||||
@Override
|
||||
protected boolean isInputProperty(PropertyDescriptor descriptor) {
|
||||
|
||||
Method readMethod = descriptor.getReadMethod();
|
||||
|
||||
if (readMethod == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return AnnotationUtils.findAnnotation(readMethod, Value.class) == null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* Copyright 2014-2015 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.Method;
|
||||
import java.util.Map;
|
||||
|
||||
import org.aopalliance.intercept.MethodInterceptor;
|
||||
import org.aopalliance.intercept.MethodInvocation;
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.expression.BeanFactoryResolver;
|
||||
import org.springframework.context.expression.MapAccessor;
|
||||
import org.springframework.expression.EvaluationContext;
|
||||
import org.springframework.expression.Expression;
|
||||
import org.springframework.expression.ParserContext;
|
||||
import org.springframework.expression.common.TemplateParserContext;
|
||||
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
||||
import org.springframework.expression.spel.support.StandardEvaluationContext;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* {@link MethodInterceptor} to invoke a SpEL expression to compute the method result. Will forward the resolution to a
|
||||
* delegate {@link MethodInterceptor} if no {@link Value} annotation is found.
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
* @see 1.10
|
||||
*/
|
||||
class SpelEvaluatingMethodInterceptor implements MethodInterceptor {
|
||||
|
||||
private final SpelExpressionParser parser;
|
||||
private final ParserContext parserContext;
|
||||
private final EvaluationContext evaluationContext;
|
||||
private final MethodInterceptor delegate;
|
||||
|
||||
/**
|
||||
* Creates a new {@link SpelEvaluatingMethodInterceptor} delegating to the given {@link MethodInterceptor} as fallback
|
||||
* and exposing the given target object via {@code target} to the SpEl expressions. If a {@link BeanFactory} is given,
|
||||
* bean references in SpEl expressions can be resolved as well.
|
||||
*
|
||||
* @param delegate must not be {@literal null}.
|
||||
* @param target must not be {@literal null}.
|
||||
* @param beanFactory can be {@literal null}.
|
||||
*/
|
||||
public SpelEvaluatingMethodInterceptor(MethodInterceptor delegate, Object target, BeanFactory beanFactory) {
|
||||
|
||||
Assert.notNull(delegate, "Delegate MethodInterceptor must not be null!");
|
||||
Assert.notNull(target, "TargetObject must not be null!");
|
||||
|
||||
StandardEvaluationContext evaluationContext = new StandardEvaluationContext(new TargetWrapper(target));
|
||||
|
||||
if (target instanceof Map) {
|
||||
evaluationContext.addPropertyAccessor(new MapAccessor());
|
||||
}
|
||||
|
||||
if (beanFactory != null) {
|
||||
evaluationContext.setBeanResolver(new BeanFactoryResolver(beanFactory));
|
||||
}
|
||||
|
||||
this.evaluationContext = evaluationContext;
|
||||
this.parser = new SpelExpressionParser();
|
||||
this.parserContext = new TemplateParserContext();
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.aopalliance.intercept.MethodInterceptor#invoke(org.aopalliance.intercept.MethodInvocation)
|
||||
*/
|
||||
@Override
|
||||
public Object invoke(MethodInvocation invocation) throws Throwable {
|
||||
|
||||
Method method = invocation.getMethod();
|
||||
Value annotation = method.getAnnotation(Value.class);
|
||||
|
||||
if (annotation == null) {
|
||||
return delegate.invoke(invocation);
|
||||
}
|
||||
|
||||
if (!StringUtils.hasText(annotation.value())) {
|
||||
throw new IllegalStateException(String.format("@Value annotation on %s contains empty expression!", method));
|
||||
}
|
||||
|
||||
Expression expression = parser.parseExpression(annotation.value(), parserContext);
|
||||
return expression.getValue(evaluationContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper class to expose an object to the SpEL expression as {@code target}.
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
static class TargetWrapper {
|
||||
|
||||
private final Object target;
|
||||
|
||||
public TargetWrapper(Object target) {
|
||||
this.target = target;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the target
|
||||
*/
|
||||
public Object getTarget() {
|
||||
return target;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
/*
|
||||
* Copyright 2015 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 static org.hamcrest.CoreMatchers.*;
|
||||
import static org.junit.Assert.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.aopalliance.intercept.MethodInvocation;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.runners.MockitoJUnitRunner;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link MapAccessingMethodInterceptor}.
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class MapAccessingMethodInterceptorUnitTests {
|
||||
|
||||
@Mock MethodInvocation invocation;
|
||||
|
||||
/**
|
||||
* @see DATACMNS-630
|
||||
*/
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void rejectsNullMap() {
|
||||
new MapAccessingMethodInterceptor(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACMNS-630
|
||||
*/
|
||||
@Test
|
||||
public void forwardsObjectMethodsToBackingMap() throws Throwable {
|
||||
|
||||
Map<String, Object> map = Collections.emptyMap();
|
||||
|
||||
when(invocation.proceed()).thenReturn(map.toString());
|
||||
when(invocation.getMethod()).thenReturn(Object.class.getMethod("toString"));
|
||||
|
||||
MapAccessingMethodInterceptor interceptor = new MapAccessingMethodInterceptor(map);
|
||||
Object result = interceptor.invoke(invocation);
|
||||
|
||||
assertThat(result, is((Object) map.toString()));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACMNS-630
|
||||
*/
|
||||
@Test
|
||||
public void setterInvocationStoresValueInMap() throws Throwable {
|
||||
|
||||
Map<String, Object> map = new HashMap<String, Object>();
|
||||
|
||||
when(invocation.getMethod()).thenReturn(Sample.class.getMethod("setName", String.class));
|
||||
when(invocation.getArguments()).thenReturn(new Object[] { "Foo" });
|
||||
|
||||
Object result = new MapAccessingMethodInterceptor(map).invoke(invocation);
|
||||
|
||||
assertThat(result, is(nullValue()));
|
||||
assertThat(map.get("name"), is((Object) "Foo"));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACMNS-630
|
||||
*/
|
||||
@Test
|
||||
public void getterInvocationReturnsValueFromMap() throws Throwable {
|
||||
|
||||
Map<String, Object> map = new HashMap<String, Object>();
|
||||
map.put("name", "Foo");
|
||||
|
||||
when(invocation.getMethod()).thenReturn(Sample.class.getMethod("getName"));
|
||||
|
||||
Object result = new MapAccessingMethodInterceptor(map).invoke(invocation);
|
||||
|
||||
assertThat(result, is((Object) "Foo"));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACMNS-630
|
||||
*/
|
||||
@Test
|
||||
public void getterReturnsNullIfMapDoesNotContainValue() throws Throwable {
|
||||
|
||||
Map<String, Object> map = new HashMap<String, Object>();
|
||||
|
||||
when(invocation.getMethod()).thenReturn(Sample.class.getMethod("getName"));
|
||||
|
||||
assertThat(new MapAccessingMethodInterceptor(map).invoke(invocation), is(nullValue()));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACMNS-630
|
||||
*/
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void rejectsNonAccessorInvocation() throws Throwable {
|
||||
|
||||
when(invocation.getMethod()).thenReturn(Sample.class.getMethod("someMethod"));
|
||||
new MapAccessingMethodInterceptor(Collections.<String, Object> emptyMap()).invoke(invocation);
|
||||
}
|
||||
|
||||
public static interface Sample {
|
||||
|
||||
String getName();
|
||||
|
||||
void setName(String name);
|
||||
|
||||
void someMethod();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
/*
|
||||
* Copyright 2014-2015 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 static org.hamcrest.Matchers.*;
|
||||
import static org.junit.Assert.*;
|
||||
import static org.mockito.Matchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Set;
|
||||
|
||||
import org.aopalliance.intercept.MethodInterceptor;
|
||||
import org.aopalliance.intercept.MethodInvocation;
|
||||
import org.hamcrest.Matcher;
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.runners.MockitoJUnitRunner;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link ProjectingMethodInterceptor}.
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
* @author Saulo Medeiros de Araujo
|
||||
*/
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class ProjectingMethodInterceptorUnitTests {
|
||||
|
||||
@Mock MethodInterceptor interceptor;
|
||||
@Mock MethodInvocation invocation;
|
||||
@Mock ProjectionFactory factory;
|
||||
|
||||
/**
|
||||
* @see DATAREST-221
|
||||
*/
|
||||
@Test
|
||||
public void wrapsDelegateResultInProxyIfTypesDontMatch() throws Throwable {
|
||||
|
||||
MethodInterceptor methodInterceptor = new ProjectingMethodInterceptor(new ProxyProjectionFactory(), interceptor);
|
||||
|
||||
when(invocation.getMethod()).thenReturn(Helper.class.getMethod("getHelper"));
|
||||
when(interceptor.invoke(invocation)).thenReturn("Foo");
|
||||
|
||||
assertThat(methodInterceptor.invoke(invocation), is(instanceOf(Helper.class)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATAREST-221
|
||||
*/
|
||||
@Test
|
||||
public void retunsDelegateResultAsIsIfTypesMatch() throws Throwable {
|
||||
|
||||
MethodInterceptor methodInterceptor = new ProjectingMethodInterceptor(factory, interceptor);
|
||||
|
||||
when(invocation.getMethod()).thenReturn(Helper.class.getMethod("getString"));
|
||||
when(interceptor.invoke(invocation)).thenReturn("Foo");
|
||||
|
||||
assertThat(methodInterceptor.invoke(invocation), is((Object) "Foo"));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATAREST-221
|
||||
*/
|
||||
@Test
|
||||
public void returnsNullAsIs() throws Throwable {
|
||||
|
||||
MethodInterceptor methodInterceptor = new ProjectingMethodInterceptor(factory, interceptor);
|
||||
|
||||
when(interceptor.invoke(invocation)).thenReturn(null);
|
||||
|
||||
assertThat(methodInterceptor.invoke(invocation), is(nullValue()));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATAREST-221
|
||||
*/
|
||||
@Test
|
||||
public void considersPrimitivesAsWrappers() throws Throwable {
|
||||
|
||||
MethodInterceptor methodInterceptor = new ProjectingMethodInterceptor(factory, interceptor);
|
||||
|
||||
when(invocation.getMethod()).thenReturn(Helper.class.getMethod("getPrimitive"));
|
||||
when(interceptor.invoke(invocation)).thenReturn(1L);
|
||||
|
||||
assertThat(methodInterceptor.invoke(invocation), is((Object) 1L));
|
||||
verify(factory, times(0)).createProjection((Class<?>) anyObject(), anyObject());
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATAREST-394, DATAREST-408
|
||||
*/
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void appliesProjectionToNonEmptySets() throws Throwable {
|
||||
|
||||
MethodInterceptor methodInterceptor = new ProjectingMethodInterceptor(new ProxyProjectionFactory(), interceptor);
|
||||
Object result = methodInterceptor.invoke(mockInvocationOf("getHelperCollection",
|
||||
Collections.singleton(mock(Helper.class))));
|
||||
|
||||
assertThat(result, is(instanceOf(Set.class)));
|
||||
|
||||
Set<Object> projections = (Set<Object>) result;
|
||||
assertThat(projections, hasSize(1));
|
||||
assertThat(projections, hasItem(instanceOf(HelperProjection.class)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATAREST-394, DATAREST-408
|
||||
*/
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void appliesProjectionToNonEmptyLists() throws Throwable {
|
||||
|
||||
MethodInterceptor methodInterceptor = new ProjectingMethodInterceptor(new ProxyProjectionFactory(), interceptor);
|
||||
Object result = methodInterceptor.invoke(mockInvocationOf("getHelperList",
|
||||
Collections.singletonList(mock(Helper.class))));
|
||||
|
||||
assertThat(result, is(instanceOf(List.class)));
|
||||
|
||||
List<Object> projections = (List<Object>) result;
|
||||
|
||||
assertThat(projections, hasSize(1));
|
||||
assertThat(projections, hasItem(instanceOf(HelperProjection.class)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATAREST-394, DATAREST-408
|
||||
*/
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void allowsMaskingAnArrayIntoACollection() throws Throwable {
|
||||
|
||||
MethodInterceptor methodInterceptor = new ProjectingMethodInterceptor(new ProxyProjectionFactory(), interceptor);
|
||||
Object result = methodInterceptor.invoke(mockInvocationOf("getHelperArray", new Helper[] { mock(Helper.class) }));
|
||||
|
||||
assertThat(result, is(instanceOf(Collection.class)));
|
||||
|
||||
Collection<Object> projections = (Collection<Object>) result;
|
||||
|
||||
assertThat(projections, hasSize(1));
|
||||
assertThat(projections, hasItem(instanceOf(HelperProjection.class)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATAREST-394, DATAREST-408
|
||||
*/
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void appliesProjectionToNonEmptyMap() throws Throwable {
|
||||
|
||||
MethodInterceptor methodInterceptor = new ProjectingMethodInterceptor(new ProxyProjectionFactory(), interceptor);
|
||||
|
||||
Object result = methodInterceptor.invoke(mockInvocationOf("getHelperMap",
|
||||
Collections.singletonMap("foo", mock(Helper.class))));
|
||||
|
||||
assertThat(result, is(instanceOf(Map.class)));
|
||||
|
||||
Map<String, Object> projections = (Map<String, Object>) result;
|
||||
assertThat(projections.entrySet(), is(Matchers.<Entry<String, Object>> iterableWithSize(1)));
|
||||
assertThat(projections, hasEntry(is("foo"), instanceOf(HelperProjection.class)));
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void returnsSingleElementCollectionForTargetThatReturnsNonCollection() throws Throwable {
|
||||
|
||||
MethodInterceptor methodInterceptor = new ProjectingMethodInterceptor(new ProxyProjectionFactory(), interceptor);
|
||||
|
||||
Helper reference = mock(Helper.class);
|
||||
Object result = methodInterceptor.invoke(mockInvocationOf("getHelperCollection", reference));
|
||||
|
||||
assertThat(result, is((Matcher<Object>) instanceOf(Collection.class)));
|
||||
assertThat((Collection<?>) result, hasSize(1));
|
||||
assertThat((Collection<Object>) result, hasItem(instanceOf(HelperProjection.class)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Mocks the {@link Helper} method of the given name to return the given value.
|
||||
*
|
||||
* @param methodName
|
||||
* @param returnValue
|
||||
* @return
|
||||
* @throws Throwable
|
||||
*/
|
||||
private MethodInvocation mockInvocationOf(String methodName, Object returnValue) throws Throwable {
|
||||
|
||||
when(invocation.getMethod()).thenReturn(Helper.class.getMethod(methodName));
|
||||
when(interceptor.invoke(invocation)).thenReturn(returnValue);
|
||||
|
||||
return invocation;
|
||||
}
|
||||
|
||||
interface Helper {
|
||||
|
||||
Helper getHelper();
|
||||
|
||||
String getString();
|
||||
|
||||
long getPrimitive();
|
||||
|
||||
Collection<HelperProjection> getHelperCollection();
|
||||
|
||||
List<HelperProjection> getHelperList();
|
||||
|
||||
Set<HelperProjection> getHelperSet();
|
||||
|
||||
Map<String, HelperProjection> getHelperMap();
|
||||
|
||||
Collection<HelperProjection> getHelperArray();
|
||||
}
|
||||
|
||||
interface HelperProjection {
|
||||
Helper getHelper();
|
||||
|
||||
String getString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* Copyright 2014 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 static org.hamcrest.CoreMatchers.*;
|
||||
import static org.junit.Assert.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import org.aopalliance.intercept.MethodInterceptor;
|
||||
import org.aopalliance.intercept.MethodInvocation;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.runners.MockitoJUnitRunner;
|
||||
import org.springframework.beans.NotReadablePropertyException;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link PropertyAccessingMethodInterceptor}.
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class PropertyAccessingMethodInterceptorUnitTests {
|
||||
|
||||
@Mock MethodInvocation invocation;
|
||||
|
||||
/**
|
||||
* @see DATAREST-221
|
||||
*/
|
||||
@Test
|
||||
public void triggersPropertyAccessOnTarget() throws Throwable {
|
||||
|
||||
Source source = new Source();
|
||||
source.firstname = "Dave";
|
||||
|
||||
when(invocation.getMethod()).thenReturn(Projection.class.getMethod("getFirstname"));
|
||||
MethodInterceptor interceptor = new PropertyAccessingMethodInterceptor(source);
|
||||
|
||||
assertThat(interceptor.invoke(invocation), is((Object) "Dave"));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATAREST-221
|
||||
*/
|
||||
@Test(expected = NotReadablePropertyException.class)
|
||||
public void throwsAppropriateExceptionIfThePropertyCannotBeFound() throws Throwable {
|
||||
|
||||
when(invocation.getMethod()).thenReturn(Projection.class.getMethod("getLastname"));
|
||||
new PropertyAccessingMethodInterceptor(new Source()).invoke(invocation);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATAREST-221
|
||||
*/
|
||||
@Test
|
||||
public void forwardsObjectMethodInvocation() throws Throwable {
|
||||
|
||||
when(invocation.getMethod()).thenReturn(Object.class.getMethod("toString"));
|
||||
new PropertyAccessingMethodInterceptor(new Source()).invoke(invocation);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACMNS-630
|
||||
*/
|
||||
@Test(expected = IllegalStateException.class)
|
||||
public void rejectsNonAccessorMethod() throws Throwable {
|
||||
|
||||
when(invocation.getMethod()).thenReturn(Projection.class.getMethod("someGarbage"));
|
||||
new PropertyAccessingMethodInterceptor(new Source()).invoke(invocation);
|
||||
}
|
||||
|
||||
static class Source {
|
||||
|
||||
String firstname;
|
||||
}
|
||||
|
||||
interface Projection {
|
||||
|
||||
String getFirstname();
|
||||
|
||||
String getLastname();
|
||||
|
||||
String someGarbage();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
/*
|
||||
* Copyright 2014-2015 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 static org.hamcrest.Matchers.*;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.springframework.aop.TargetClassAware;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link ProxyProjectionFactory}.
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
public class ProxyProjectionFactoryUnitTests {
|
||||
|
||||
ProjectionFactory factory = new ProxyProjectionFactory();
|
||||
|
||||
/**
|
||||
* @see DATACMNS-630
|
||||
*/
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void rejectsNullProjectionType() {
|
||||
factory.createProjection(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACMNS-630
|
||||
*/
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void rejectsNullProjectionTypeWithSource() {
|
||||
factory.createProjection(null, new Object());
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACMNS-630
|
||||
*/
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void rejectsNullProjectionTypeForInputProperties() {
|
||||
factory.getInputProperties(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACMNS-630
|
||||
*/
|
||||
@Test
|
||||
public void returnsNullForNullSource() {
|
||||
assertThat(factory.createProjection(CustomerExcerpt.class, null), is(nullValue()));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATAREST-221, DATACMNS-630
|
||||
*/
|
||||
@Test
|
||||
public void createsProjectingProxy() {
|
||||
|
||||
Customer customer = new Customer();
|
||||
customer.firstname = "Dave";
|
||||
customer.lastname = "Matthews";
|
||||
|
||||
customer.address = new Address();
|
||||
customer.address.city = "New York";
|
||||
customer.address.zipCode = "ZIP";
|
||||
|
||||
CustomerExcerpt excerpt = factory.createProjection(CustomerExcerpt.class, customer);
|
||||
|
||||
assertThat(excerpt.getFirstname(), is("Dave"));
|
||||
assertThat(excerpt.getAddress().getZipCode(), is("ZIP"));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATAREST-221, DATACMNS-630
|
||||
*/
|
||||
@Test
|
||||
@SuppressWarnings("rawtypes")
|
||||
public void proxyExposesTargetClassAware() {
|
||||
|
||||
CustomerExcerpt proxy = factory.createProjection(CustomerExcerpt.class);
|
||||
|
||||
assertThat(proxy, is(instanceOf(TargetClassAware.class)));
|
||||
assertThat(((TargetClassAware) proxy).getTargetClass(), is(equalTo((Class) HashMap.class)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATAREST-221, DATACMNS-630
|
||||
*/
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void rejectsNonInterfacesAsProjectionTarget() {
|
||||
factory.createProjection(Object.class, new Object());
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACMNS-630
|
||||
*/
|
||||
@Test
|
||||
public void createsMapBasedProxyFromSource() {
|
||||
|
||||
HashMap<String, Object> addressSource = new HashMap<String, Object>();
|
||||
addressSource.put("zipCode", "ZIP");
|
||||
addressSource.put("city", "NewYork");
|
||||
|
||||
Map<String, Object> source = new HashMap<String, Object>();
|
||||
source.put("firstname", "Dave");
|
||||
source.put("lastname", "Matthews");
|
||||
source.put("address", addressSource);
|
||||
|
||||
CustomerExcerpt projection = factory.createProjection(CustomerExcerpt.class, source);
|
||||
|
||||
assertThat(projection.getFirstname(), is("Dave"));
|
||||
|
||||
AddressExcerpt address = projection.getAddress();
|
||||
assertThat(address, is(notNullValue()));
|
||||
assertThat(address.getZipCode(), is("ZIP"));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACMNS-630
|
||||
*/
|
||||
@Test
|
||||
public void createsEmptyMapBasedProxy() {
|
||||
|
||||
CustomerProxy proxy = factory.createProjection(CustomerProxy.class);
|
||||
|
||||
assertThat(proxy, is(notNullValue()));
|
||||
|
||||
proxy.setFirstname("Dave");
|
||||
assertThat(proxy.getFirstname(), is("Dave"));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACMNS-630
|
||||
*/
|
||||
@Test
|
||||
public void returnsAllPropertiesAsInputProperties() {
|
||||
|
||||
List<String> result = factory.getInputProperties(CustomerExcerpt.class);
|
||||
|
||||
assertThat(result, hasSize(2));
|
||||
assertThat(result, hasItems("firstname", "address"));
|
||||
}
|
||||
|
||||
static class Customer {
|
||||
|
||||
public String firstname, lastname;
|
||||
public Address address;
|
||||
}
|
||||
|
||||
static class Address {
|
||||
|
||||
public String zipCode, city;
|
||||
}
|
||||
|
||||
interface CustomerExcerpt {
|
||||
|
||||
String getFirstname();
|
||||
|
||||
AddressExcerpt getAddress();
|
||||
}
|
||||
|
||||
interface AddressExcerpt {
|
||||
|
||||
String getZipCode();
|
||||
}
|
||||
|
||||
interface CustomerProxy {
|
||||
|
||||
String getFirstname();
|
||||
|
||||
void setFirstname(String firstname);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright 2015 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 static org.hamcrest.Matchers.*;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link SpelAwareProxyProjectionFactory}.
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
public class SpelAwareProxyProjectionFactoryUnitTests {
|
||||
|
||||
private final SpelAwareProxyProjectionFactory factory = new SpelAwareProxyProjectionFactory();
|
||||
|
||||
/**
|
||||
* @see DATAREST-221, DATACMNS-630
|
||||
*/
|
||||
@Test
|
||||
public void exposesSpelInvokingMethod() {
|
||||
|
||||
Customer customer = new Customer();
|
||||
customer.firstname = "Dave";
|
||||
customer.lastname = "Matthews";
|
||||
|
||||
CustomerExcerpt excerpt = factory.createProjection(CustomerExcerpt.class, customer);
|
||||
assertThat(excerpt.getFullName(), is("Dave Matthews"));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACMNS-630
|
||||
*/
|
||||
@Test
|
||||
public void excludesAtValueAnnotatedMethodsForInputProperties() {
|
||||
|
||||
List<String> properties = factory.getInputProperties(CustomerExcerpt.class);
|
||||
|
||||
assertThat(properties, hasSize(1));
|
||||
assertThat(properties, hasItem("firstname"));
|
||||
}
|
||||
|
||||
static class Customer {
|
||||
|
||||
public String firstname, lastname;
|
||||
}
|
||||
|
||||
interface CustomerExcerpt {
|
||||
|
||||
@Value("#{target.firstname + ' ' + target.lastname}")
|
||||
String getFullName();
|
||||
|
||||
String getFirstname();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
* Copyright 2014-2105 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 static org.hamcrest.CoreMatchers.*;
|
||||
import static org.junit.Assert.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.aopalliance.intercept.MethodInterceptor;
|
||||
import org.aopalliance.intercept.MethodInvocation;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.runners.MockitoJUnitRunner;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link SpelEvaluatingMethodInterceptor}.
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class SpelEvaluatingMethodInterceptorUnitTests {
|
||||
|
||||
@Mock MethodInterceptor delegate;
|
||||
@Mock MethodInvocation invocation;
|
||||
|
||||
/**
|
||||
* @see DATAREST-221, DATACMNS-630
|
||||
*/
|
||||
@Test
|
||||
public void invokesMethodOnTarget() throws Throwable {
|
||||
|
||||
when(invocation.getMethod()).thenReturn(Projection.class.getMethod("propertyFromTarget"));
|
||||
|
||||
MethodInterceptor interceptor = new SpelEvaluatingMethodInterceptor(delegate, new Target(), null);
|
||||
|
||||
assertThat(interceptor.invoke(invocation), is((Object) "property"));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATAREST-221, DATACMNS-630
|
||||
*/
|
||||
@Test
|
||||
public void invokesMethodOnBean() throws Throwable {
|
||||
|
||||
when(invocation.getMethod()).thenReturn(Projection.class.getMethod("invokeBean"));
|
||||
|
||||
DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
|
||||
factory.registerSingleton("someBean", new SomeBean());
|
||||
|
||||
SpelEvaluatingMethodInterceptor interceptor = new SpelEvaluatingMethodInterceptor(delegate, new Target(), factory);
|
||||
|
||||
assertThat(interceptor.invoke(invocation), is((Object) "value"));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACMNS-630
|
||||
*/
|
||||
@Test
|
||||
public void forwardNonAtValueAnnotatedMethodToDelegate() throws Throwable {
|
||||
|
||||
when(invocation.getMethod()).thenReturn(Projection.class.getMethod("getName"));
|
||||
|
||||
SpelEvaluatingMethodInterceptor interceptor = new SpelEvaluatingMethodInterceptor(delegate, new Target(),
|
||||
new DefaultListableBeanFactory());
|
||||
|
||||
interceptor.invoke(invocation);
|
||||
|
||||
verify(delegate).invoke(invocation);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACMNS-630
|
||||
*/
|
||||
@Test(expected = IllegalStateException.class)
|
||||
public void rejectsEmptySpelExpression() throws Throwable {
|
||||
|
||||
when(invocation.getMethod()).thenReturn(Projection.class.getMethod("getAddress"));
|
||||
|
||||
SpelEvaluatingMethodInterceptor interceptor = new SpelEvaluatingMethodInterceptor(delegate, new Target(),
|
||||
new DefaultListableBeanFactory());
|
||||
|
||||
interceptor.invoke(invocation);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACMNS-630
|
||||
*/
|
||||
@Test
|
||||
public void allowsMapAccessViaPropertyExpression() throws Throwable {
|
||||
|
||||
Map<String, Object> map = new HashMap<String, Object>();
|
||||
map.put("name", "Dave");
|
||||
|
||||
when(invocation.getMethod()).thenReturn(Projection.class.getMethod("propertyFromTarget"));
|
||||
|
||||
SpelEvaluatingMethodInterceptor interceptor = new SpelEvaluatingMethodInterceptor(delegate, map,
|
||||
new DefaultListableBeanFactory());
|
||||
|
||||
assertThat(interceptor.invoke(invocation), is((Object) "Dave"));
|
||||
}
|
||||
|
||||
interface Projection {
|
||||
|
||||
@Value("#{target.name}")
|
||||
String propertyFromTarget();
|
||||
|
||||
@Value("#{@someBean.value}")
|
||||
String invokeBean();
|
||||
|
||||
String getName();
|
||||
|
||||
@Value("")
|
||||
String getAddress();
|
||||
}
|
||||
|
||||
static class Target {
|
||||
|
||||
public String getName() {
|
||||
return "property";
|
||||
}
|
||||
}
|
||||
|
||||
static class SomeBean {
|
||||
|
||||
public String getValue() {
|
||||
return "value";
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user