From a98d5c7ec6f148a09f686d011508cf6ef61242fd Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Sat, 10 Jan 2015 17:14:57 +0100 Subject: [PATCH] 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. --- .../MapAccessingMethodInterceptor.java | 127 ++++++++++ .../ProjectingMethodInterceptor.java | 147 +++++++++++ .../data/projection/ProjectionFactory.java | 54 ++++ .../PropertyAccessingMethodInterceptor.java | 71 ++++++ .../projection/ProxyProjectionFactory.java | 203 +++++++++++++++ .../SpelAwareProxyProjectionFactory.java | 94 +++++++ .../SpelEvaluatingMethodInterceptor.java | 122 +++++++++ ...apAccessingMethodInterceptorUnitTests.java | 131 ++++++++++ .../ProjectingMethodInterceptorUnitTests.java | 236 ++++++++++++++++++ ...tyAccessingMethodInterceptorUnitTests.java | 98 ++++++++ .../ProxyProjectionFactoryUnitTests.java | 189 ++++++++++++++ ...lAwareProxyProjectionFactoryUnitTests.java | 73 ++++++ ...lEvaluatingMethodInterceptorUnitTests.java | 148 +++++++++++ 13 files changed, 1693 insertions(+) create mode 100644 src/main/java/org/springframework/data/projection/MapAccessingMethodInterceptor.java create mode 100644 src/main/java/org/springframework/data/projection/ProjectingMethodInterceptor.java create mode 100644 src/main/java/org/springframework/data/projection/ProjectionFactory.java create mode 100644 src/main/java/org/springframework/data/projection/PropertyAccessingMethodInterceptor.java create mode 100644 src/main/java/org/springframework/data/projection/ProxyProjectionFactory.java create mode 100644 src/main/java/org/springframework/data/projection/SpelAwareProxyProjectionFactory.java create mode 100644 src/main/java/org/springframework/data/projection/SpelEvaluatingMethodInterceptor.java create mode 100644 src/test/java/org/springframework/data/projection/MapAccessingMethodInterceptorUnitTests.java create mode 100644 src/test/java/org/springframework/data/projection/ProjectingMethodInterceptorUnitTests.java create mode 100644 src/test/java/org/springframework/data/projection/PropertyAccessingMethodInterceptorUnitTests.java create mode 100644 src/test/java/org/springframework/data/projection/ProxyProjectionFactoryUnitTests.java create mode 100644 src/test/java/org/springframework/data/projection/SpelAwareProxyProjectionFactoryUnitTests.java create mode 100644 src/test/java/org/springframework/data/projection/SpelEvaluatingMethodInterceptorUnitTests.java diff --git a/src/main/java/org/springframework/data/projection/MapAccessingMethodInterceptor.java b/src/main/java/org/springframework/data/projection/MapAccessingMethodInterceptor.java new file mode 100644 index 000000000..c02c22980 --- /dev/null +++ b/src/main/java/org/springframework/data/projection/MapAccessingMethodInterceptor.java @@ -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 map; + + /** + * Creates a new {@link MapAccessingMethodInterceptor} for the given {@link Map}. + * + * @param map must not be {@literal null}. + */ + public MapAccessingMethodInterceptor(Map 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(); + } + } +} diff --git a/src/main/java/org/springframework/data/projection/ProjectingMethodInterceptor.java b/src/main/java/org/springframework/data/projection/ProjectingMethodInterceptor.java new file mode 100644 index 000000000..bd3b649e0 --- /dev/null +++ b/src/main/java/org/springframework/data/projection/ProjectingMethodInterceptor.java @@ -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 projectCollectionElements(Collection sources, TypeInformation type) { + + Collection 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 projectMapValues(Map sources, TypeInformation type) { + + Map 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); + } + } +} diff --git a/src/main/java/org/springframework/data/projection/ProjectionFactory.java b/src/main/java/org/springframework/data/projection/ProjectionFactory.java new file mode 100644 index 000000000..8bc21702f --- /dev/null +++ b/src/main/java/org/springframework/data/projection/ProjectionFactory.java @@ -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 createProjection(Class projectionType, Object source); + + /** + * Creates a pojection instance for the given type. + * + * @param projectionType the type to create, must not be {@literal null}. + * @return + */ + T createProjection(Class projectionType); + + /** + * Returns the properties that will be consumed by the given projection type. + * + * @param projectionType must not be {@literal null}. + * @return + */ + List getInputProperties(Class projectionType); +} diff --git a/src/main/java/org/springframework/data/projection/PropertyAccessingMethodInterceptor.java b/src/main/java/org/springframework/data/projection/PropertyAccessingMethodInterceptor.java new file mode 100644 index 000000000..2d7f7b15e --- /dev/null +++ b/src/main/java/org/springframework/data/projection/PropertyAccessingMethodInterceptor.java @@ -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()); + } +} diff --git a/src/main/java/org/springframework/data/projection/ProxyProjectionFactory.java b/src/main/java/org/springframework/data/projection/ProxyProjectionFactory.java new file mode 100644 index 000000000..658a0d6e3 --- /dev/null +++ b/src/main/java/org/springframework/data/projection/ProxyProjectionFactory.java @@ -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 createProjection(Class 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 createProjection(Class projectionType) { + + Assert.notNull(projectionType, "Projection type must not be null!"); + + return createProjection(projectionType, new HashMap()); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.projection.ProjectionFactory#getProperties(java.lang.Class) + */ + @Override + public List getInputProperties(Class projectionType) { + + Assert.notNull(projectionType, "Projection type must not be null!"); + + PropertyDescriptor[] descriptors = BeanUtils.getPropertyDescriptors(projectionType); + List result = new ArrayList(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) 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(); + } + } +} diff --git a/src/main/java/org/springframework/data/projection/SpelAwareProxyProjectionFactory.java b/src/main/java/org/springframework/data/projection/SpelAwareProxyProjectionFactory.java new file mode 100644 index 000000000..55c0a9c0c --- /dev/null +++ b/src/main/java/org/springframework/data/projection/SpelAwareProxyProjectionFactory.java @@ -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, Boolean> typeCache = new HashMap, 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 callback = new AnnotationDetectionMethodCallback(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; + } +} diff --git a/src/main/java/org/springframework/data/projection/SpelEvaluatingMethodInterceptor.java b/src/main/java/org/springframework/data/projection/SpelEvaluatingMethodInterceptor.java new file mode 100644 index 000000000..34faac185 --- /dev/null +++ b/src/main/java/org/springframework/data/projection/SpelEvaluatingMethodInterceptor.java @@ -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; + } + } +} diff --git a/src/test/java/org/springframework/data/projection/MapAccessingMethodInterceptorUnitTests.java b/src/test/java/org/springframework/data/projection/MapAccessingMethodInterceptorUnitTests.java new file mode 100644 index 000000000..4752160c8 --- /dev/null +++ b/src/test/java/org/springframework/data/projection/MapAccessingMethodInterceptorUnitTests.java @@ -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 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 map = new HashMap(); + + 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 map = new HashMap(); + 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 map = new HashMap(); + + 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. emptyMap()).invoke(invocation); + } + + public static interface Sample { + + String getName(); + + void setName(String name); + + void someMethod(); + } +} diff --git a/src/test/java/org/springframework/data/projection/ProjectingMethodInterceptorUnitTests.java b/src/test/java/org/springframework/data/projection/ProjectingMethodInterceptorUnitTests.java new file mode 100644 index 000000000..3f34aa9cf --- /dev/null +++ b/src/test/java/org/springframework/data/projection/ProjectingMethodInterceptorUnitTests.java @@ -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 projections = (Set) 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 projections = (List) 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 projections = (Collection) 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 projections = (Map) result; + assertThat(projections.entrySet(), is(Matchers.> 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) instanceOf(Collection.class))); + assertThat((Collection) result, hasSize(1)); + assertThat((Collection) 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 getHelperCollection(); + + List getHelperList(); + + Set getHelperSet(); + + Map getHelperMap(); + + Collection getHelperArray(); + } + + interface HelperProjection { + Helper getHelper(); + + String getString(); + } +} diff --git a/src/test/java/org/springframework/data/projection/PropertyAccessingMethodInterceptorUnitTests.java b/src/test/java/org/springframework/data/projection/PropertyAccessingMethodInterceptorUnitTests.java new file mode 100644 index 000000000..efcc1c5b8 --- /dev/null +++ b/src/test/java/org/springframework/data/projection/PropertyAccessingMethodInterceptorUnitTests.java @@ -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(); + } +} diff --git a/src/test/java/org/springframework/data/projection/ProxyProjectionFactoryUnitTests.java b/src/test/java/org/springframework/data/projection/ProxyProjectionFactoryUnitTests.java new file mode 100644 index 000000000..95b749803 --- /dev/null +++ b/src/test/java/org/springframework/data/projection/ProxyProjectionFactoryUnitTests.java @@ -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 addressSource = new HashMap(); + addressSource.put("zipCode", "ZIP"); + addressSource.put("city", "NewYork"); + + Map source = new HashMap(); + 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 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); + } +} diff --git a/src/test/java/org/springframework/data/projection/SpelAwareProxyProjectionFactoryUnitTests.java b/src/test/java/org/springframework/data/projection/SpelAwareProxyProjectionFactoryUnitTests.java new file mode 100644 index 000000000..bcc033e33 --- /dev/null +++ b/src/test/java/org/springframework/data/projection/SpelAwareProxyProjectionFactoryUnitTests.java @@ -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 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(); + } +} diff --git a/src/test/java/org/springframework/data/projection/SpelEvaluatingMethodInterceptorUnitTests.java b/src/test/java/org/springframework/data/projection/SpelEvaluatingMethodInterceptorUnitTests.java new file mode 100644 index 000000000..d71540df8 --- /dev/null +++ b/src/test/java/org/springframework/data/projection/SpelEvaluatingMethodInterceptorUnitTests.java @@ -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 map = new HashMap(); + 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"; + } + } +}