From dbe79f89a98bfe385375b197538bf720e641f745 Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Fri, 27 Nov 2015 22:42:41 +0100 Subject: [PATCH] DATACMNS-89 - Infrastructure for projections on repository queries. QueryMethods now expose a ResourceProcessor which is exposes information about the final to be created object types which can either be DTOs containing a persistence constructor (see @PersistenceConstructor) or projection interfaces. The former are analyzed for constructor properties so that store implementations can use that information to create projected queries that return exactly the fields required for that DTO. Projection interfaces are inspected, their properties are considered input properties and the same projection queries can be issued against the data store. If a projection contains dynamically calculated properties (i.e. it uses SpEL expressions via @Value) the original entities have to be queried and can be projected during post processing. ProjectionFactory now exposes a more advanced ProjectionInformation that has additional meta information about the projection type. ProxyProjectionFactory now refers to the BeanClassLoader instead of the ResourceLoader. RepositoryFactory(Bean)Support now also implement BeanFactoryAware to forward the BeanFactory to the SpelAwareProxyProjectionFactory which in turn now gets handed into the QueryLookupStrategy as well as the QueryMethod. Parameter now knows about a dynamic projection type, which is a query method parameter of type Class bound to a generic method parameter and will be used to determine the projection to be used on a per call basis. Original pull request: #150. --- .../DefaultProjectionInformation.java | 115 +++++++ .../data/projection/ProjectionFactory.java | 11 + .../projection/ProjectionInformation.java | 51 +++ .../projection/ProxyProjectionFactory.java | 64 ++-- .../SpelAwareProxyProjectionFactory.java | 29 +- .../support/RepositoryFactoryBeanSupport.java | 23 +- .../support/RepositoryFactorySupport.java | 30 +- .../data/repository/query/Parameter.java | 46 ++- .../repository/query/ParameterAccessor.java | 10 +- .../data/repository/query/Parameters.java | 35 +- .../query/ParametersParameterAccessor.java | 9 + .../repository/query/QueryLookupStrategy.java | 11 +- .../data/repository/query/QueryMethod.java | 21 +- .../repository/query/ResultProcessor.java | 246 ++++++++++++++ .../data/repository/query/ReturnedType.java | 311 ++++++++++++++++++ .../data/util/TypeInformation.java | 4 +- ...ProxyingHandlerMethodArgumentResolver.java | 17 +- ...DefaultProjectionInformationUnitTests.java | 78 +++++ .../ProxyProjectionFactoryUnitTests.java | 17 +- ...lAwareProxyProjectionFactoryUnitTests.java | 11 + .../core/support/DummyRepositoryFactory.java | 6 +- ...eryExecutorMethodInterceptorUnitTests.java | 15 +- .../RepositoryFactorySupportUnitTests.java | 4 +- .../repository/query/ParametersUnitTests.java | 18 +- .../query/QueryMethodUnitTests.java | 33 +- .../query/ResultProcessorUnitTests.java | 224 +++++++++++++ .../query/ReturnedTypeUnitTests.java | 179 ++++++++++ template.mf | 2 + 28 files changed, 1523 insertions(+), 97 deletions(-) create mode 100644 src/main/java/org/springframework/data/projection/DefaultProjectionInformation.java create mode 100644 src/main/java/org/springframework/data/projection/ProjectionInformation.java create mode 100644 src/main/java/org/springframework/data/repository/query/ResultProcessor.java create mode 100644 src/main/java/org/springframework/data/repository/query/ReturnedType.java create mode 100644 src/test/java/org/springframework/data/projection/DefaultProjectionInformationUnitTests.java create mode 100644 src/test/java/org/springframework/data/repository/query/ResultProcessorUnitTests.java create mode 100644 src/test/java/org/springframework/data/repository/query/ReturnedTypeUnitTests.java diff --git a/src/main/java/org/springframework/data/projection/DefaultProjectionInformation.java b/src/main/java/org/springframework/data/projection/DefaultProjectionInformation.java new file mode 100644 index 000000000..4cb6adb28 --- /dev/null +++ b/src/main/java/org/springframework/data/projection/DefaultProjectionInformation.java @@ -0,0 +1,115 @@ +/* + * 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.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.springframework.beans.BeanUtils; +import org.springframework.util.Assert; + +/** + * Default implementation of {@link ProjectionInformation}. Exposes all properties of the type as required input + * properties. + * + * @author Oliver Gierke + * @since 1.12 + */ +class DefaultProjectionInformation implements ProjectionInformation { + + private final Class projectionType; + private final List properties; + + /** + * Creates a new {@link DefaultProjectionInformation} for the given type. + * + * @param type must not be {@literal null}. + */ + public DefaultProjectionInformation(Class type) { + + Assert.notNull(type, "Projection type must not be null!"); + + this.projectionType = type; + this.properties = collectDescriptors(type); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.projection.ProjectionInformation#getType() + */ + @Override + public Class getType() { + return projectionType; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.projection.ProjectionInformation#getInputProperties() + */ + public List getInputProperties() { + + List result = new ArrayList(); + + for (PropertyDescriptor descriptor : properties) { + if (isInputProperty(descriptor)) { + result.add(descriptor); + } + } + + return result; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.projection.ProjectionInformation#isDynamic() + */ + @Override + public boolean isClosed() { + return this.properties.equals(getInputProperties()); + } + + /** + * 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 + * descriptor was looked up on. + * + * @param descriptor will never be {@literal null}. + * @return + */ + protected boolean isInputProperty(PropertyDescriptor descriptor) { + return true; + } + + /** + * Collects {@link PropertyDescriptor}s for all properties exposed by the given type and all its super interfaces. + * + * @param type must not be {@literal null}. + * @return + */ + private static List collectDescriptors(Class type) { + + List result = new ArrayList(); + result.addAll(Arrays.asList(BeanUtils.getPropertyDescriptors(type))); + + for (Class interfaze : type.getInterfaces()) { + result.addAll(collectDescriptors(interfaze)); + } + + return result; + } +} diff --git a/src/main/java/org/springframework/data/projection/ProjectionFactory.java b/src/main/java/org/springframework/data/projection/ProjectionFactory.java index 8bc21702f..a4cfbb376 100644 --- a/src/main/java/org/springframework/data/projection/ProjectionFactory.java +++ b/src/main/java/org/springframework/data/projection/ProjectionFactory.java @@ -49,6 +49,17 @@ public interface ProjectionFactory { * * @param projectionType must not be {@literal null}. * @return + * @deprecated use {@link #getProjectionInformation(Class)} */ + @Deprecated List getInputProperties(Class projectionType); + + /** + * Returns the {@link ProjectionInformation} for the given projection type. + * + * @param projectionType must not be {@literal null}. + * @return + * @since 1.12 + */ + ProjectionInformation getProjectionInformation(Class projectionType); } diff --git a/src/main/java/org/springframework/data/projection/ProjectionInformation.java b/src/main/java/org/springframework/data/projection/ProjectionInformation.java new file mode 100644 index 000000000..a98d27f4b --- /dev/null +++ b/src/main/java/org/springframework/data/projection/ProjectionInformation.java @@ -0,0 +1,51 @@ +/* + * 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.util.List; + +/** + * Information about a projection type. + * + * @author Oliver Gierke + * @since 1.12 + */ +public interface ProjectionInformation { + + /** + * Returns the projection type. + * + * @return will never be {@literal null}. + */ + Class getType(); + + /** + * Returns the properties that will be consumed by the projection type. + * + * @return will never be {@literal null}. + */ + List getInputProperties(); + + /** + * Returns whether supplying values for the properties returned via {@link #getInputProperties()} is sufficient to + * create a working proxy instance. This will usually be used to determine whether the projection uses any dynamically + * resolved properties. + * + * @return + */ + boolean isClosed(); +} diff --git a/src/main/java/org/springframework/data/projection/ProxyProjectionFactory.java b/src/main/java/org/springframework/data/projection/ProxyProjectionFactory.java index d44945562..c2a653c83 100644 --- a/src/main/java/org/springframework/data/projection/ProxyProjectionFactory.java +++ b/src/main/java/org/springframework/data/projection/ProxyProjectionFactory.java @@ -26,7 +26,7 @@ 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.beans.factory.BeanClassLoaderAware; import org.springframework.context.ResourceLoaderAware; import org.springframework.core.io.ResourceLoader; import org.springframework.util.Assert; @@ -42,20 +42,30 @@ import org.springframework.util.ClassUtils; * @see SpelAwareProxyProjectionFactory * @since 1.10 */ -class ProxyProjectionFactory implements ProjectionFactory, ResourceLoaderAware { +class ProxyProjectionFactory implements ProjectionFactory, ResourceLoaderAware, BeanClassLoaderAware { private static final boolean IS_JAVA_8 = org.springframework.util.ClassUtils.isPresent("java.util.Optional", ProxyProjectionFactory.class.getClassLoader()); - private ResourceLoader resourceLoader; + private ClassLoader classLoader; + + /** + * @see org.springframework.context.ResourceLoaderAware#setResourceLoader(org.springframework.core.io.ResourceLoader) + * @deprecated rather set the {@link ClassLoader} directly via {@link #setBeanClassLoader(ClassLoader)}. + */ + @Override + @Deprecated + public void setResourceLoader(ResourceLoader resourceLoader) { + this.classLoader = resourceLoader.getClassLoader(); + } /* * (non-Javadoc) - * @see org.springframework.context.ResourceLoaderAware#setResourceLoader(org.springframework.core.io.ResourceLoader) + * @see org.springframework.beans.factory.BeanClassLoaderAware#setBeanClassLoader(java.lang.ClassLoader) */ @Override - public void setResourceLoader(ResourceLoader resourceLoader) { - this.resourceLoader = resourceLoader; + public void setBeanClassLoader(ClassLoader classLoader) { + this.classLoader = classLoader; } /* @@ -85,8 +95,7 @@ class ProxyProjectionFactory implements ProjectionFactory, ResourceLoaderAware { factory.addAdvice(new TargetAwareMethodInterceptor(source.getClass())); factory.addAdvice(getMethodInterceptor(source, projectionType)); - return (T) factory - .getProxy(resourceLoader == null ? ClassUtils.getDefaultClassLoader() : resourceLoader.getClassLoader()); + return (T) factory.getProxy(classLoader == null ? ClassUtils.getDefaultClassLoader() : classLoader); } /* @@ -110,18 +119,24 @@ class ProxyProjectionFactory implements ProjectionFactory, ResourceLoaderAware { Assert.notNull(projectionType, "Projection type must not be null!"); - PropertyDescriptor[] descriptors = BeanUtils.getPropertyDescriptors(projectionType); - List result = new ArrayList(descriptors.length); + List result = new ArrayList(); - for (PropertyDescriptor descriptor : descriptors) { - if (isInputProperty(descriptor)) { - result.add(descriptor.getName()); - } + for (PropertyDescriptor descriptor : getProjectionInformation(projectionType).getInputProperties()) { + result.add(descriptor.getName()); } return result; } + /* + * (non-Javadoc) + * @see org.springframework.data.projection.ProjectionFactory#getProjectionInformation(java.lang.Class) + */ + @Override + public ProjectionInformation getProjectionInformation(Class projectionType) { + return new DefaultProjectionInformation(projectionType); + } + /** * Returns the {@link MethodInterceptor} to add to the proxy. * @@ -132,11 +147,12 @@ class ProxyProjectionFactory implements ProjectionFactory, ResourceLoaderAware { @SuppressWarnings("unchecked") private MethodInterceptor getMethodInterceptor(Object source, Class projectionType) { - MethodInterceptor propertyInvocationInterceptor = source instanceof Map ? new MapAccessingMethodInterceptor( - (Map) source) : new PropertyAccessingMethodInterceptor(source); + MethodInterceptor propertyInvocationInterceptor = source instanceof Map + ? new MapAccessingMethodInterceptor((Map) source) + : new PropertyAccessingMethodInterceptor(source); - return new ProjectingMethodInterceptor(this, postProcessAccessorInterceptor(propertyInvocationInterceptor, source, - projectionType)); + return new ProjectingMethodInterceptor(this, + postProcessAccessorInterceptor(propertyInvocationInterceptor, source, projectionType)); } /** @@ -153,18 +169,6 @@ class ProxyProjectionFactory implements ProjectionFactory, ResourceLoaderAware { 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 - * descriptor was looked up on. - * - * @param descriptor will never be {@literal null}. - * @return - */ - protected boolean isInputProperty(PropertyDescriptor descriptor) { - return true; - } - /** * 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. diff --git a/src/main/java/org/springframework/data/projection/SpelAwareProxyProjectionFactory.java b/src/main/java/org/springframework/data/projection/SpelAwareProxyProjectionFactory.java index b3f698ff9..277179cfc 100644 --- a/src/main/java/org/springframework/data/projection/SpelAwareProxyProjectionFactory.java +++ b/src/main/java/org/springframework/data/projection/SpelAwareProxyProjectionFactory.java @@ -75,23 +75,34 @@ public class SpelAwareProxyProjectionFactory extends ProxyProjectionFactory impl typeCache.put(projectionType, callback.hasFoundAnnotation()); } - return typeCache.get(projectionType) ? new SpelEvaluatingMethodInterceptor(interceptor, source, beanFactory, - parser, projectionType) : interceptor; + return typeCache.get(projectionType) + ? new SpelEvaluatingMethodInterceptor(interceptor, source, beanFactory, parser, projectionType) : interceptor; } /* * (non-Javadoc) - * @see org.springframework.data.projection.ProxyProjectionFactory#isProperty(java.beans.PropertyDescriptor) + * @see org.springframework.data.projection.ProxyProjectionFactory#getProjectionInformation(java.lang.Class) */ @Override - protected boolean isInputProperty(PropertyDescriptor descriptor) { + public ProjectionInformation getProjectionInformation(Class projectionType) { - Method readMethod = descriptor.getReadMethod(); + return new DefaultProjectionInformation(projectionType) { - if (readMethod == null) { - return false; - } + /* + * (non-Javadoc) + * @see org.springframework.data.projection.DefaultProjectionInformation#isInputProperty(java.beans.PropertyDescriptor) + */ + @Override + protected boolean isInputProperty(PropertyDescriptor descriptor) { - return AnnotationUtils.findAnnotation(readMethod, Value.class) == null; + 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/repository/core/support/RepositoryFactoryBeanSupport.java b/src/main/java/org/springframework/data/repository/core/support/RepositoryFactoryBeanSupport.java index 025429e69..12ef4656c 100644 --- a/src/main/java/org/springframework/data/repository/core/support/RepositoryFactoryBeanSupport.java +++ b/src/main/java/org/springframework/data/repository/core/support/RepositoryFactoryBeanSupport.java @@ -18,7 +18,10 @@ package org.springframework.data.repository.core.support; import java.io.Serializable; import java.util.List; +import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Required; @@ -45,7 +48,7 @@ import org.springframework.util.Assert; * @author Thomas Darimont */ public abstract class RepositoryFactoryBeanSupport, S, ID extends Serializable> implements - InitializingBean, RepositoryFactoryInformation, FactoryBean, BeanClassLoaderAware { + InitializingBean, RepositoryFactoryInformation, FactoryBean, BeanClassLoaderAware, BeanFactoryAware { private RepositoryFactorySupport factory; @@ -56,6 +59,7 @@ public abstract class RepositoryFactoryBeanSupport, private NamedQueries namedQueries; private MappingContext mappingContext; private ClassLoader classLoader; + private BeanFactory beanFactory; private boolean lazyInit = false; private EvaluationContextProvider evaluationContextProvider = DefaultEvaluationContextProvider.INSTANCE; @@ -151,6 +155,16 @@ public abstract class RepositoryFactoryBeanSupport, this.classLoader = classLoader; } + /* + * (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; + + } + /* * (non-Javadoc) * @see org.springframework.data.repository.core.support.RepositoryFactoryInformation#getEntityInformation() @@ -167,8 +181,8 @@ public abstract class RepositoryFactoryBeanSupport, */ public RepositoryInformation getRepositoryInformation() { - return this.factory.getRepositoryInformation(repositoryMetadata, customImplementation == null ? null - : customImplementation.getClass()); + return this.factory.getRepositoryInformation(repositoryMetadata, + customImplementation == null ? null : customImplementation.getClass()); } /* @@ -227,9 +241,10 @@ public abstract class RepositoryFactoryBeanSupport, this.factory = createRepositoryFactory(); this.factory.setQueryLookupStrategyKey(queryLookupStrategyKey); this.factory.setNamedQueries(namedQueries); - this.factory.setBeanClassLoader(classLoader); this.factory.setEvaluationContextProvider(evaluationContextProvider); this.factory.setRepositoryBaseClass(repositoryBaseClass); + this.factory.setBeanClassLoader(classLoader); + this.factory.setBeanFactory(beanFactory); this.repositoryMetadata = this.factory.getRepositoryMetadata(repositoryInterface); diff --git a/src/main/java/org/springframework/data/repository/core/support/RepositoryFactorySupport.java b/src/main/java/org/springframework/data/repository/core/support/RepositoryFactorySupport.java index 56c5ae52b..b45ec1757 100644 --- a/src/main/java/org/springframework/data/repository/core/support/RepositoryFactorySupport.java +++ b/src/main/java/org/springframework/data/repository/core/support/RepositoryFactorySupport.java @@ -29,11 +29,15 @@ import org.aopalliance.intercept.MethodInvocation; import org.springframework.aop.framework.ProxyFactory; import org.springframework.aop.interceptor.ExposeInvocationInterceptor; import org.springframework.beans.BeanUtils; +import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.core.GenericTypeResolver; import org.springframework.core.MethodParameter; import org.springframework.core.convert.TypeDescriptor; import org.springframework.data.projection.DefaultMethodInvokingMethodInterceptor; +import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.EntityInformation; import org.springframework.data.repository.core.NamedQueries; @@ -57,7 +61,7 @@ import org.springframework.util.ObjectUtils; * * @author Oliver Gierke */ -public abstract class RepositoryFactorySupport implements BeanClassLoaderAware { +public abstract class RepositoryFactorySupport implements BeanClassLoaderAware, BeanFactoryAware { private static final boolean IS_JAVA_8 = org.springframework.util.ClassUtils.isPresent("java.util.Optional", RepositoryFactorySupport.class.getClassLoader()); @@ -72,6 +76,7 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware { private NamedQueries namedQueries = PropertiesBasedNamedQueries.EMPTY; private ClassLoader classLoader = org.springframework.util.ClassUtils.getDefaultClassLoader(); private EvaluationContextProvider evaluationContextProvider = DefaultEvaluationContextProvider.INSTANCE; + private BeanFactory beanFactory; private QueryCollectingQueryCreationListener collectingListener = new QueryCollectingQueryCreationListener(); @@ -106,6 +111,15 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware { this.classLoader = classLoader == null ? org.springframework.util.ClassUtils.getDefaultClassLoader() : classLoader; } + /* + * (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; + } + /** * Sets the {@link EvaluationContextProvider} to be used to evaluate SpEL expressions in manually defined queries. * @@ -307,9 +321,9 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware { if (null == customImplementation && repositoryInformation.hasCustomMethod()) { - throw new IllegalArgumentException(String.format( - "You have custom methods in %s but not provided a custom implementation!", - repositoryInformation.getRepositoryInterface())); + throw new IllegalArgumentException( + String.format("You have custom methods in %s but not provided a custom implementation!", + repositoryInformation.getRepositoryInterface())); } validate(repositoryInformation); @@ -412,8 +426,14 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware { return; } + SpelAwareProxyProjectionFactory factory = new SpelAwareProxyProjectionFactory(); + factory.setBeanClassLoader(classLoader); + factory.setBeanFactory(beanFactory); + for (Method method : queryMethods) { - RepositoryQuery query = lookupStrategy.resolveQuery(method, repositoryInformation, namedQueries); + + RepositoryQuery query = lookupStrategy.resolveQuery(method, repositoryInformation, factory, namedQueries); + invokeListeners(query); queries.put(method, query); } diff --git a/src/main/java/org/springframework/data/repository/query/Parameter.java b/src/main/java/org/springframework/data/repository/query/Parameter.java index 9c9f3df6e..b90c84f05 100644 --- a/src/main/java/org/springframework/data/repository/query/Parameter.java +++ b/src/main/java/org/springframework/data/repository/query/Parameter.java @@ -1,5 +1,5 @@ /* - * Copyright 2008-2013 the original author or authors. + * Copyright 2008-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. @@ -17,12 +17,15 @@ package org.springframework.data.repository.query; import static java.lang.String.*; +import java.lang.reflect.Method; import java.util.Arrays; import java.util.List; import org.springframework.core.MethodParameter; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; +import org.springframework.data.util.ClassTypeInformation; +import org.springframework.data.util.TypeInformation; import org.springframework.util.Assert; /** @@ -38,6 +41,7 @@ public class Parameter { private static final String POSITION_PARAMETER_TEMPLATE = "?%s"; private final MethodParameter parameter; + private final boolean isDynamicProjectionParameter; /** * Creates a new {@link Parameter} for the given {@link MethodParameter}. @@ -47,7 +51,9 @@ public class Parameter { protected Parameter(MethodParameter parameter) { Assert.notNull(parameter); + this.parameter = parameter; + this.isDynamicProjectionParameter = isDynamicProjectionParameter(parameter); } /** @@ -57,7 +63,7 @@ public class Parameter { * @see #TYPES */ public boolean isSpecialParameter() { - return TYPES.contains(parameter.getParameterType()); + return isDynamicProjectionParameter || TYPES.contains(parameter.getParameterType()); } /** @@ -69,6 +75,15 @@ public class Parameter { return !isSpecialParameter(); } + /** + * Returns whether the current {@link Parameter} is the one used for dynamic projections. + * + * @return + */ + public boolean isDynamicProjectionParameter() { + return isDynamicProjectionParameter; + } + /** * Returns the placeholder to be used for the parameter. Can either be a named one or positional. * @@ -157,4 +172,31 @@ public class Parameter { boolean isSort() { return Sort.class.isAssignableFrom(getType()); } + + /** + * Returns whether the given {@link MethodParameter} is a dynamic projection parameter, which means it carries a + * dynamic type parameter which is identical to the type parameter of the actually returned type. + *

+ * + * Collection findBy…(…, Class type); + * + * + * @param parameter must not be {@literal null}. + * @return + */ + private static boolean isDynamicProjectionParameter(MethodParameter parameter) { + + Method method = parameter.getMethod(); + + ClassTypeInformation ownerType = ClassTypeInformation.from(parameter.getDeclaringClass()); + TypeInformation parameterTypes = ownerType.getParameterTypes(method).get(parameter.getParameterIndex()); + TypeInformation returnType = ClassTypeInformation.fromReturnTypeOf(method); + + if (!parameterTypes.getType().equals(Class.class)) { + return false; + } + + TypeInformation bound = parameterTypes.getTypeArguments().get(0); + return bound.equals(returnType.getActualType()); + } } diff --git a/src/main/java/org/springframework/data/repository/query/ParameterAccessor.java b/src/main/java/org/springframework/data/repository/query/ParameterAccessor.java index ecdfa33ab..a100e3e28 100644 --- a/src/main/java/org/springframework/data/repository/query/ParameterAccessor.java +++ b/src/main/java/org/springframework/data/repository/query/ParameterAccessor.java @@ -42,6 +42,14 @@ public interface ParameterAccessor extends Iterable { */ Sort getSort(); + /** + * Returns the dynamic projection type to be used when executing the query or {@literal null} if none is defined. + * + * @return + * @since 1.12 + */ + Class getDynamicProjection(); + /** * Returns the bindable value with the given index. Bindable means, that {@link Pageable} and {@link Sort} values are * skipped without noticed in the index. For a method signature taking {@link String}, {@link Pageable} , @@ -66,4 +74,4 @@ public interface ParameterAccessor extends Iterable { * @return */ Iterator iterator(); -} \ No newline at end of file +} diff --git a/src/main/java/org/springframework/data/repository/query/Parameters.java b/src/main/java/org/springframework/data/repository/query/Parameters.java index 4d0810807..ba69d0637 100644 --- a/src/main/java/org/springframework/data/repository/query/Parameters.java +++ b/src/main/java/org/springframework/data/repository/query/Parameters.java @@ -37,13 +37,13 @@ import org.springframework.util.Assert; */ public abstract class Parameters, T extends Parameter> implements Iterable { - @SuppressWarnings("unchecked")// + @SuppressWarnings("unchecked") // public static final List> TYPES = Arrays.asList(Pageable.class, Sort.class); private static final String PARAM_ON_SPECIAL = format("You must not user @%s on a parameter typed %s or %s", Param.class.getSimpleName(), Pageable.class.getSimpleName(), Sort.class.getSimpleName()); - private static final String ALL_OR_NOTHING = String.format("Either use @%s " - + "on all parameters except %s and %s typed once, or none at all!", Param.class.getSimpleName(), + private static final String ALL_OR_NOTHING = String.format( + "Either use @%s on all parameters except %s and %s typed once, or none at all!", Param.class.getSimpleName(), Pageable.class.getSimpleName(), Sort.class.getSimpleName()); private final ParameterNameDiscoverer discoverer = new DefaultParameterNameDiscoverer(); @@ -51,6 +51,8 @@ public abstract class Parameters, T extends Parameter private final int sortIndex; private final List parameters; + private int dynamicProjectionIndex; + /** * Creates a new instance of {@link Parameters}. * @@ -61,6 +63,7 @@ public abstract class Parameters, T extends Parameter Assert.notNull(method); this.parameters = new ArrayList(); + this.dynamicProjectionIndex = -1; List> types = Arrays.asList(method.getParameterTypes()); @@ -75,6 +78,10 @@ public abstract class Parameters, T extends Parameter throw new IllegalArgumentException(PARAM_ON_SPECIAL); } + if (parameter.isDynamicProjectionParameter()) { + this.dynamicProjectionIndex = parameter.getIndex(); + } + parameters.add(parameter); } @@ -95,6 +102,7 @@ public abstract class Parameters, T extends Parameter int pageableIndexTemp = -1; int sortIndexTemp = -1; + int dynamicProjectionTemp = -1; for (int i = 0; i < originals.size(); i++) { @@ -103,10 +111,12 @@ public abstract class Parameters, T extends Parameter pageableIndexTemp = original.isPageable() ? i : -1; sortIndexTemp = original.isSort() ? i : -1; + dynamicProjectionTemp = original.isDynamicProjectionParameter() ? i : -1; } this.pageableIndex = pageableIndexTemp; this.sortIndex = sortIndexTemp; + this.dynamicProjectionIndex = dynamicProjectionTemp; } /** @@ -155,6 +165,25 @@ public abstract class Parameters, T extends Parameter return sortIndex != -1; } + /** + * Returns the index of the parameter that represents the dynamic projection type. Will return {@literal -1} if no + * such parameter exists. + * + * @return + */ + public int getDynamicProjectionIndex() { + return dynamicProjectionIndex; + } + + /** + * Returns whether a parameter expressing a dynamic projection exists. + * + * @return + */ + public boolean hasDynamicProjection() { + return dynamicProjectionIndex != -1; + } + /** * Returns whether we potentially find a {@link Sort} parameter in the parameters. * diff --git a/src/main/java/org/springframework/data/repository/query/ParametersParameterAccessor.java b/src/main/java/org/springframework/data/repository/query/ParametersParameterAccessor.java index 493fbf7c1..84ca492f4 100644 --- a/src/main/java/org/springframework/data/repository/query/ParametersParameterAccessor.java +++ b/src/main/java/org/springframework/data/repository/query/ParametersParameterAccessor.java @@ -97,6 +97,15 @@ public class ParametersParameterAccessor implements ParameterAccessor { return null; } + /** + * Returns the dynamic projection type if available, {@literal null} otherwise. + * + * @return + */ + public Class getDynamicProjection() { + return parameters.hasDynamicProjection() ? (Class) values.get(parameters.getDynamicProjectionIndex()) : null; + } + /** * Returns the value with the given index. * diff --git a/src/main/java/org/springframework/data/repository/query/QueryLookupStrategy.java b/src/main/java/org/springframework/data/repository/query/QueryLookupStrategy.java index b145ea027..b786f0c16 100644 --- a/src/main/java/org/springframework/data/repository/query/QueryLookupStrategy.java +++ b/src/main/java/org/springframework/data/repository/query/QueryLookupStrategy.java @@ -18,6 +18,7 @@ package org.springframework.data.repository.query; import java.lang.reflect.Method; import java.util.Locale; +import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.repository.core.NamedQueries; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.util.StringUtils; @@ -52,10 +53,12 @@ public interface QueryLookupStrategy { /** * Resolves a {@link RepositoryQuery} from the given {@link QueryMethod} that can be executed afterwards. * - * @param method - * @param metadata - * @param namedQueries + * @param method will never be {@literal null}. + * @param metadata will never be {@literal null}. + * @param factory will never be {@literal null}. + * @param namedQueries will never be {@literal null}. * @return */ - RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, NamedQueries namedQueries); + RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, ProjectionFactory factory, + NamedQueries namedQueries); } diff --git a/src/main/java/org/springframework/data/repository/query/QueryMethod.java b/src/main/java/org/springframework/data/repository/query/QueryMethod.java index b80b4f4ce..3e876764d 100644 --- a/src/main/java/org/springframework/data/repository/query/QueryMethod.java +++ b/src/main/java/org/springframework/data/repository/query/QueryMethod.java @@ -24,6 +24,7 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import org.springframework.data.domain.Sort; +import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.repository.core.EntityMetadata; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.util.QueryExecutionConverters; @@ -44,6 +45,7 @@ public class QueryMethod { private final Method method; private final Class unwrappedReturnType; private final Parameters parameters; + private final ResultProcessor resultProcessor; private Class domainClass; @@ -51,13 +53,15 @@ public class QueryMethod { * Creates a new {@link QueryMethod} from the given parameters. Looks up the correct query to use for following * invocations of the method given. * - * @param method must not be {@literal null} - * @param metadata must not be {@literal null} + * @param method must not be {@literal null}. + * @param metadata must not be {@literal null}. + * @param factory must not be {@literal null}. */ - public QueryMethod(Method method, RepositoryMetadata metadata) { + public QueryMethod(Method method, RepositoryMetadata metadata, ProjectionFactory factory) { Assert.notNull(method, "Method must not be null!"); Assert.notNull(metadata, "Repository metadata must not be null!"); + Assert.notNull(factory, "ProjectionFactory must not be null!"); for (Class type : Parameters.TYPES) { if (getNumberOfOccurences(method, type) > 1) { @@ -89,6 +93,8 @@ public class QueryMethod { Assert.isTrue(this.parameters.hasPageableParameter(), String.format("Paging query needs to have a Pageable parameter! Offending method %s", method.toString())); } + + this.resultProcessor = new ResultProcessor(this, factory); } /** @@ -228,6 +234,15 @@ public class QueryMethod { return parameters; } + /** + * Returns the {@link ResultProcessor} to be usedwith the query method. + * + * @return the resultFactory + */ + public ResultProcessor getResultProcessor() { + return resultProcessor; + } + /* * (non-Javadoc) * @see java.lang.Object#toString() diff --git a/src/main/java/org/springframework/data/repository/query/ResultProcessor.java b/src/main/java/org/springframework/data/repository/query/ResultProcessor.java new file mode 100644 index 000000000..2414a278b --- /dev/null +++ b/src/main/java/org/springframework/data/repository/query/ResultProcessor.java @@ -0,0 +1,246 @@ +/* + * 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.repository.query; + +import lombok.NonNull; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.core.CollectionFactory; +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.domain.Page; +import org.springframework.data.projection.ProjectionFactory; +import org.springframework.util.Assert; + +/** + * A {@link ResultProcessor} to expose metadata about query result element projection and eventually post prcessing raw + * query results into projections and data transfer objects. + * + * @author Oliver Gierke + * @since 1.12 + */ +public class ResultProcessor { + + private final QueryMethod method; + private final ProjectingConverter converter; + private final ProjectionFactory factory; + + private ReturnedType type; + + /** + * Creates a new {@link ResultProcessor} from the given {@link QueryMethod} and {@link ProjectionFactory}. + * + * @param method must not be {@literal null}. + * @param factory must not be {@literal null}. + */ + ResultProcessor(QueryMethod method, ProjectionFactory factory) { + this(method, factory, method.getReturnedObjectType()); + } + + /** + * Creates a new {@link ResultProcessor} for the given {@link QueryMethod}, {@link ProjectionFactory} and type. + * + * @param method must not be {@literal null}. + * @param factory must not be {@literal null}. + * @param type must not be {@literal null}. + */ + private ResultProcessor(QueryMethod method, ProjectionFactory factory, Class type) { + + Assert.notNull(method, "QueryMethod must not be null!"); + Assert.notNull(factory, "ProjectionFactory must not be null!"); + Assert.notNull(type, "Type must not be null!"); + + this.method = method; + this.type = ReturnedType.of(type, method.getDomainClass(), factory); + this.converter = new ProjectingConverter(this.type, factory); + this.factory = factory; + } + + /** + * Returns a new {@link ResultProcessor} with a new projection type obtained from the given {@link ParameterAccessor}. + * + * @param accessor can be {@literal null}. + * @return + */ + public ResultProcessor withDynamicProjection(ParameterAccessor accessor) { + + if (accessor == null) { + return this; + } + + Class projectionType = accessor.getDynamicProjection(); + + return projectionType == null ? this : new ResultProcessor(method, factory, projectionType); + } + + /** + * Returns the {@link ReturnedType}. + * + * @return + */ + public ReturnedType getReturnedType() { + return type; + } + + /** + * Post-processes the given query result. + * + * @param source can be {@literal null}. + * @return + */ + public T processResult(Object source) { + return processResult(source, NoOpConverter.INSTANCE); + } + + /** + * Post-processes the given query result using the given preparing {@link Converter} to potentially prepare collection + * elements. + * + * @param source can be {@literal null}. + * @param preparingConverter must not be {@literal null}. + * @return + */ + @SuppressWarnings("unchecked") + public T processResult(Object source, Converter preparingConverter) { + + if (type.isInstance(source) || !type.isProjecting()) { + return (T) source; + } + + Assert.notNull(preparingConverter, "Preparing converter must not be null!"); + + ChainingConverter converter = ChainingConverter.of(preparingConverter).and(this.converter); + + if (source instanceof Page && method.isPageQuery()) { + return (T) ((Page) source).map(converter); + } + + if (source instanceof Collection && method.isCollectionQuery()) { + + Collection collection = (Collection) source; + Collection target = CollectionFactory.createCollection(collection.getClass(), collection.size()); + + for (Object columns : collection) { + target.add(type.isInstance(columns) ? columns : converter.convert(columns)); + } + + return (T) target; + } + + return (T) converter.convert(source); + } + + @RequiredArgsConstructor(staticName = "of") + private static class ChainingConverter implements Converter { + + private final @NonNull Converter delegate; + + /** + * Returns a new {@link ChainingConverter} that hands the elements resulting from the current conversion to the + * given {@link Converter}. + * + * @param converter must not be {@literal null}. + * @return + */ + public ChainingConverter and(final Converter converter) { + + Assert.notNull(converter, "Converter must not be null!"); + + return new ChainingConverter(new Converter() { + + @Override + public Object convert(Object source) { + return converter.convert(ChainingConverter.this.convert(source)); + } + }); + } + + /* + * (non-Javadoc) + * @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object) + */ + @Override + public Object convert(Object source) { + return delegate.convert(source); + } + } + + /** + * A simple {@link Converter} that will return the source value as is. + * + * @author Oliver Gierke + * @since 1.12 + */ + private static enum NoOpConverter implements Converter { + + INSTANCE; + + /* + * (non-Javadoc) + * @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object) + */ + @Override + public Object convert(Object source) { + return source; + } + } + + @RequiredArgsConstructor + private static class ProjectingConverter implements Converter { + + private final @NonNull ReturnedType type; + private final @NonNull ProjectionFactory factory; + + /* + * (non-Javadoc) + * @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object) + */ + @Override + public Object convert(Object source) { + return factory.createProjection(type.getReturnedType(), getProjectionTarget(source)); + } + + private Object getProjectionTarget(Object source) { + + if (source != null && source.getClass().isArray()) { + source = Arrays.asList((Object[]) source); + } + + if (source instanceof Collection) { + return toMap((Collection) source, type.getInputProperties()); + } + + return source; + } + + private static Map toMap(Collection values, List names) { + + int i = 0; + Map result = new HashMap(values.size()); + + for (Object element : values) { + result.put(names.get(i++), element); + } + + return result; + } + } +} diff --git a/src/main/java/org/springframework/data/repository/query/ReturnedType.java b/src/main/java/org/springframework/data/repository/query/ReturnedType.java new file mode 100644 index 000000000..62d18c72a --- /dev/null +++ b/src/main/java/org/springframework/data/repository/query/ReturnedType.java @@ -0,0 +1,311 @@ +/* + * 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.repository.query; + +import lombok.AccessLevel; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; + +import java.beans.PropertyDescriptor; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.springframework.data.mapping.PreferredConstructor; +import org.springframework.data.mapping.model.PreferredConstructorDiscoverer; +import org.springframework.data.projection.ProjectionFactory; +import org.springframework.data.projection.ProjectionInformation; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * A representation of the type returned by a {@link QueryMethod}. + * + * @author Oliver Gierke + * @since 1.12 + */ +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +public abstract class ReturnedType { + + private final @NonNull Class domainType; + + /** + * Creates a new {@link ReturnedType} for the given returned type, domain type and {@link ProjectionFactory}. + * + * @param returnedType must not be {@literal null}. + * @param domainType must not be {@literal null}. + * @param factory must not be {@literal null}. + * @return + */ + static ReturnedType of(Class returnedType, Class domainType, ProjectionFactory factory) { + + Assert.notNull(returnedType, "Returned type must not be null!"); + Assert.notNull(domainType, "Domain type must not be null!"); + Assert.notNull(factory, "ProjectionFactory must not be null!"); + + return (ReturnedType) (returnedType.isInterface() + ? new ReturnedInterface(factory.getProjectionInformation(returnedType), domainType) + : new ReturnedClass(returnedType, domainType)); + } + + /** + * Returns the entity type. + * + * @return + */ + public final Class getDomainType() { + return domainType; + } + + /** + * Returns whether the given source object is an instance of the returned type. + * + * @param source can be {@literal null}. + * @return + */ + public final boolean isInstance(Object source) { + return getReturnedType().isInstance(source); + } + + /** + * Returns whether the type is projecting, i.e. not of the domain type. + * + * @return + */ + public abstract boolean isProjecting(); + + /** + * Returns the type of the individual objects to return. + * + * @return + */ + public abstract Class getReturnedType(); + + /** + * Returns whether the returned type will require custom construction. + * + * @return + */ + public abstract boolean needsCustomConstruction(); + + /** + * Returns the type that the query execution is supposed to pass to the underlying infrastructure. {@literal null} is + * returned to indicate a generic type (a map or tuple-like type) shall be used. + * + * @return + */ + public abstract Class getTypeToRead(); + + /** + * Returns the properties required to be used to populate the result. + * + * @return + */ + public abstract List getInputProperties(); + + /** + * A {@link ReturnedType} that's backed by an interface. + * + * @author Oliver Gierke + * @since 1.12 + */ + private static final class ReturnedInterface extends ReturnedType { + + private final ProjectionInformation information; + private final Class domainType; + + /** + * Creates a new {@link ReturnedInterface} from the given {@link ProjectionInformation} and domain type. + * + * @param information must not be {@literal null}. + * @param domainType must not be {@literal null}. + */ + public ReturnedInterface(ProjectionInformation information, Class domainType) { + + super(domainType); + + Assert.notNull(information, "Projection information must not be null!"); + + this.information = information; + this.domainType = domainType; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.ResultFactory.ReturnedTypeInformation#getReturnedType() + */ + @Override + public Class getReturnedType() { + return information.getType(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.ReturnedType#needsCustomConstruction() + */ + public boolean needsCustomConstruction() { + return information.isClosed(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.ResultFactory.ReturnedType#isProjecting() + */ + @Override + public boolean isProjecting() { + return true; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.ResultFactory.ReturnedTypeInformation#getTypeToRead() + */ + @Override + public Class getTypeToRead() { + return information.isClosed() ? null : domainType; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.ResultFactory.ReturnedTypeInformation#getInputProperties() + */ + @Override + public List getInputProperties() { + + List properties = new ArrayList(); + + for (PropertyDescriptor descriptor : information.getInputProperties()) { + properties.add(descriptor.getName()); + } + + return properties; + } + } + + /** + * A {@link ReturnedType} that's backed by an actual class. + * + * @author Oliver Gierke + * @since 1.12 + */ + private static final class ReturnedClass extends ReturnedType { + + @SuppressWarnings("unchecked") // + private static final Set> VOID_TYPES = new HashSet>(Arrays.asList(Void.class, void.class)); + + private final Class type; + private final List inputProperties; + + /** + * Creates a new {@link ReturnedClass} instance for the given returned type and domain type. + * + * @param returnedType must not be {@literal null}. + * @param domainType must not be {@literal null}. + * @param projectionInformation + */ + public ReturnedClass(Class returnedType, Class domainType) { + + super(domainType); + + Assert.notNull(returnedType, "Returned type must not be null!"); + Assert.notNull(domainType, "Domain type must not be null!"); + Assert.isTrue(!returnedType.isInterface(), "Returned type must not be an interface!"); + + this.type = returnedType; + this.inputProperties = detectConstructorParameterNames(returnedType); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.ResultFactory.ReturnedTypeInformation#getReturnedType() + */ + @Override + public Class getReturnedType() { + return type; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.ResultFactory.ReturnedType#getTypeToRead() + */ + public Class getTypeToRead() { + return type; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.ResultFactory.ReturnedType#isProjecting() + */ + @Override + public boolean isProjecting() { + return isDto(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.ResultFactory.ReturnedType#needsCustomConstruction() + */ + public boolean needsCustomConstruction() { + return isDto(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.ResultFactory.ReturnedTypeInformation#getInputProperties() + */ + @Override + public List getInputProperties() { + return inputProperties; + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + private List detectConstructorParameterNames(Class type) { + + if (!isDto()) { + return Collections.emptyList(); + } + + PreferredConstructorDiscoverer discoverer = new PreferredConstructorDiscoverer(type); + PreferredConstructor constructor = discoverer.getConstructor(); + List properties = new ArrayList(); + + for (PreferredConstructor.Parameter parameter : constructor.getParameters()) { + properties.add(parameter.getName()); + } + + return properties; + } + + private boolean isDto() { + return !Object.class.equals(type) && // + !isDomainSubtype() && // + !isPrimitiveOrWrapper() && // + !VOID_TYPES.contains(type) && // + !type.getPackage().getName().startsWith("java.lang"); + } + + private boolean isDomainSubtype() { + return getDomainType().equals(type) && getDomainType().isAssignableFrom(type); + } + + private boolean isPrimitiveOrWrapper() { + return ClassUtils.isPrimitiveOrWrapper(type); + } + } +} diff --git a/src/main/java/org/springframework/data/util/TypeInformation.java b/src/main/java/org/springframework/data/util/TypeInformation.java index 8d875abbf..632b13b96 100644 --- a/src/main/java/org/springframework/data/util/TypeInformation.java +++ b/src/main/java/org/springframework/data/util/TypeInformation.java @@ -1,5 +1,5 @@ /* - * Copyright 2008-2011 the original author or authors. + * Copyright 2008-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. @@ -21,7 +21,7 @@ import java.util.List; /** * Interface to access property types and resolving generics on the way. Starting with a {@link ClassTypeInformation} - * you can travers properties using {@link #getProperty(String)} to access type information. + * you can traverse properties using {@link #getProperty(String)} to access type information. * * @author Oliver Gierke */ diff --git a/src/main/java/org/springframework/data/web/ProxyingHandlerMethodArgumentResolver.java b/src/main/java/org/springframework/data/web/ProxyingHandlerMethodArgumentResolver.java index 32e46fe5f..b918f22ac 100644 --- a/src/main/java/org/springframework/data/web/ProxyingHandlerMethodArgumentResolver.java +++ b/src/main/java/org/springframework/data/web/ProxyingHandlerMethodArgumentResolver.java @@ -17,6 +17,7 @@ package org.springframework.data.web; import org.springframework.beans.BeansException; import org.springframework.beans.MutablePropertyValues; +import org.springframework.beans.factory.BeanClassLoaderAware; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.context.ResourceLoaderAware; @@ -37,7 +38,7 @@ import org.springframework.web.method.support.HandlerMethodArgumentResolver; * @since 1.10 */ public class ProxyingHandlerMethodArgumentResolver extends ModelAttributeMethodProcessor - implements BeanFactoryAware, ResourceLoaderAware { + implements BeanFactoryAware, ResourceLoaderAware, BeanClassLoaderAware { private final SpelAwareProxyProjectionFactory proxyFactory; private final ConversionService conversionService; @@ -64,15 +65,25 @@ public class ProxyingHandlerMethodArgumentResolver extends ModelAttributeMethodP this.proxyFactory.setBeanFactory(beanFactory); } - /* - * (non-Javadoc) + /** * @see org.springframework.context.ResourceLoaderAware#setResourceLoader(org.springframework.core.io.ResourceLoader) + * @deprecated rather set the {@link ClassLoader} via {@link #setBeanClassLoader(ClassLoader)}. */ @Override + @Deprecated public void setResourceLoader(ResourceLoader resourceLoader) { this.proxyFactory.setResourceLoader(resourceLoader); } + /* + * (non-Javadoc) + * @see org.springframework.beans.factory.BeanClassLoaderAware#setBeanClassLoader(java.lang.ClassLoader) + */ + @Override + public void setBeanClassLoader(ClassLoader classLoader) { + this.proxyFactory.setBeanClassLoader(classLoader); + } + /* * (non-Javadoc) * @see org.springframework.web.method.support.HandlerMethodArgumentResolver#supportsParameter(org.springframework.core.MethodParameter) diff --git a/src/test/java/org/springframework/data/projection/DefaultProjectionInformationUnitTests.java b/src/test/java/org/springframework/data/projection/DefaultProjectionInformationUnitTests.java new file mode 100644 index 000000000..e12979e64 --- /dev/null +++ b/src/test/java/org/springframework/data/projection/DefaultProjectionInformationUnitTests.java @@ -0,0 +1,78 @@ +/* + * 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.beans.PropertyDescriptor; +import java.util.ArrayList; +import java.util.List; + +import org.junit.Test; + +/** + * Unit tests for {@link DefaultProjectionInformation}. + * + * @author Oliver Gierke + */ +public class DefaultProjectionInformationUnitTests { + + /** + * @see DATACMNS-89 + */ + @Test + public void discoversInputProperties() { + + ProjectionInformation information = new DefaultProjectionInformation(CustomerProjection.class); + + assertThat(toNames(information.getInputProperties()), contains("firstname", "lastname")); + } + + /** + * @see DATACMNS-89 + */ + @Test + public void discoversAllInputProperties() { + + ProjectionInformation information = new DefaultProjectionInformation(ExtendedProjection.class); + + assertThat(toNames(information.getInputProperties()), hasItems("age", "firstname", "lastname")); + } + + private static List toNames(List descriptors) { + + List names = new ArrayList(descriptors.size()); + + for (PropertyDescriptor descriptor : descriptors) { + names.add(descriptor.getName()); + } + + return names; + } + + interface CustomerProjection { + + String getFirstname(); + + String getLastname(); + } + + interface ExtendedProjection extends CustomerProjection { + + int getAge(); + } +} diff --git a/src/test/java/org/springframework/data/projection/ProxyProjectionFactoryUnitTests.java b/src/test/java/org/springframework/data/projection/ProxyProjectionFactoryUnitTests.java index 91c7830ac..5677fc189 100644 --- a/src/test/java/org/springframework/data/projection/ProxyProjectionFactoryUnitTests.java +++ b/src/test/java/org/springframework/data/projection/ProxyProjectionFactoryUnitTests.java @@ -18,6 +18,7 @@ package org.springframework.data.projection; import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; +import java.beans.PropertyDescriptor; import java.lang.reflect.Proxy; import java.util.HashMap; import java.util.List; @@ -155,10 +156,10 @@ public class ProxyProjectionFactoryUnitTests { @Test public void returnsAllPropertiesAsInputProperties() { - List result = factory.getInputProperties(CustomerExcerpt.class); + ProjectionInformation projectionInformation = factory.getProjectionInformation(CustomerExcerpt.class); + List result = projectionInformation.getInputProperties(); assertThat(result, hasSize(5)); - assertThat(result, hasItems("firstname", "address", "shippingAddresses", "picture")); } /** @@ -234,6 +235,18 @@ public class ProxyProjectionFactoryUnitTests { assertThat(excerpt.getId(), is(customer.id.toString())); } + /** + * @see DATACMNS-89 + */ + @Test + public void exposesProjectionInformationCorrectly() { + + ProjectionInformation information = factory.getProjectionInformation(CustomerExcerpt.class); + + assertThat(information.getType(), is(typeCompatibleWith(CustomerExcerpt.class))); + assertThat(information.isClosed(), is(true)); + } + static class Customer { public Long id; diff --git a/src/test/java/org/springframework/data/projection/SpelAwareProxyProjectionFactoryUnitTests.java b/src/test/java/org/springframework/data/projection/SpelAwareProxyProjectionFactoryUnitTests.java index 8d5de435f..6269561eb 100644 --- a/src/test/java/org/springframework/data/projection/SpelAwareProxyProjectionFactoryUnitTests.java +++ b/src/test/java/org/springframework/data/projection/SpelAwareProxyProjectionFactoryUnitTests.java @@ -65,6 +65,17 @@ public class SpelAwareProxyProjectionFactoryUnitTests { assertThat(properties, hasItem("firstname")); } + /** + * @see DATACMNS-89 + */ + @Test + public void considersProjectionUsingAtValueNotClosed() { + + ProjectionInformation information = factory.getProjectionInformation(CustomerExcerpt.class); + + assertThat(information.isClosed(), is(false)); + } + static class Customer { public String firstname, lastname; diff --git a/src/test/java/org/springframework/data/repository/core/support/DummyRepositoryFactory.java b/src/test/java/org/springframework/data/repository/core/support/DummyRepositoryFactory.java index 8279df80e..a05c1d234 100644 --- a/src/test/java/org/springframework/data/repository/core/support/DummyRepositoryFactory.java +++ b/src/test/java/org/springframework/data/repository/core/support/DummyRepositoryFactory.java @@ -21,6 +21,7 @@ import java.io.Serializable; import java.lang.reflect.Method; import org.mockito.Mockito; +import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.repository.core.EntityInformation; import org.springframework.data.repository.core.NamedQueries; import org.springframework.data.repository.core.RepositoryInformation; @@ -48,9 +49,8 @@ public class DummyRepositoryFactory extends RepositoryFactorySupport { this.repository = repository; - when( - strategy.resolveQuery(Mockito.any(Method.class), Mockito.any(RepositoryMetadata.class), - Mockito.any(NamedQueries.class))).thenReturn(queryOne); + when(strategy.resolveQuery(Mockito.any(Method.class), Mockito.any(RepositoryMetadata.class), + Mockito.any(ProjectionFactory.class), Mockito.any(NamedQueries.class))).thenReturn(queryOne); } /* diff --git a/src/test/java/org/springframework/data/repository/core/support/QueryExecutorMethodInterceptorUnitTests.java b/src/test/java/org/springframework/data/repository/core/support/QueryExecutorMethodInterceptorUnitTests.java index 2693f90b9..8d70e3d6e 100644 --- a/src/test/java/org/springframework/data/repository/core/support/QueryExecutorMethodInterceptorUnitTests.java +++ b/src/test/java/org/springframework/data/repository/core/support/QueryExecutorMethodInterceptorUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2011 the original author or authors. + * Copyright 2011-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. @@ -26,6 +26,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.repository.core.NamedQueries; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.core.RepositoryMetadata; @@ -40,12 +41,9 @@ import org.springframework.data.repository.query.QueryLookupStrategy.Key; @RunWith(MockitoJUnitRunner.class) public class QueryExecutorMethodInterceptorUnitTests { - @Mock - RepositoryFactorySupport factory; - @Mock - RepositoryInformation information; - @Mock - QueryLookupStrategy strategy; + @Mock RepositoryFactorySupport factory; + @Mock RepositoryInformation information; + @Mock QueryLookupStrategy strategy; @Test(expected = IllegalStateException.class) public void rejectsRepositoryInterfaceWithQueryMethodsIfNoQueryLookupStrategyIsDefined() throws Exception { @@ -63,6 +61,7 @@ public class QueryExecutorMethodInterceptorUnitTests { when(factory.getQueryLookupStrategy(any(Key.class))).thenReturn(strategy); factory.new QueryExecutorMethodInterceptor(information, null, new Object()); - verify(strategy, times(0)).resolveQuery(any(Method.class), any(RepositoryMetadata.class), any(NamedQueries.class)); + verify(strategy, times(0)).resolveQuery(any(Method.class), any(RepositoryMetadata.class), + any(ProjectionFactory.class), any(NamedQueries.class)); } } diff --git a/src/test/java/org/springframework/data/repository/core/support/RepositoryFactorySupportUnitTests.java b/src/test/java/org/springframework/data/repository/core/support/RepositoryFactorySupportUnitTests.java index 7024ecde5..86c0eb3a4 100644 --- a/src/test/java/org/springframework/data/repository/core/support/RepositoryFactorySupportUnitTests.java +++ b/src/test/java/org/springframework/data/repository/core/support/RepositoryFactorySupportUnitTests.java @@ -46,6 +46,7 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; +import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.repository.PagingAndSortingRepository; import org.springframework.data.repository.Repository; import org.springframework.data.repository.RepositoryDefinition; @@ -94,7 +95,8 @@ public class RepositoryFactorySupportUnitTests { Mockito.reset(factory.strategy); when(factory.strategy.resolveQuery(Mockito.any(Method.class), Mockito.any(RepositoryMetadata.class), - Mockito.any(NamedQueries.class))).thenReturn(factory.queryOne, factory.queryTwo); + Mockito.any(ProjectionFactory.class), Mockito.any(NamedQueries.class))).thenReturn(factory.queryOne, + factory.queryTwo); factory.addQueryCreationListener(listener); factory.addQueryCreationListener(otherListener); diff --git a/src/test/java/org/springframework/data/repository/query/ParametersUnitTests.java b/src/test/java/org/springframework/data/repository/query/ParametersUnitTests.java index be7e21df4..524f8d635 100644 --- a/src/test/java/org/springframework/data/repository/query/ParametersUnitTests.java +++ b/src/test/java/org/springframework/data/repository/query/ParametersUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2008-2014 the original author or authors. + * Copyright 2008-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 @@ -151,7 +151,20 @@ public class ParametersUnitTests { assertThat(parameter.isExplicitlyNamed(), is(false)); } - private Parameters getParametersFor(String methodName, Class... parameterTypes) + /** + * @see DATACMNS-89 + */ + @Test + public void detectsDynamicProjectionParameter() throws Exception { + + Parameters parameters = getParametersFor("dynamicBind", Class.class, Class.class, Class.class); + + assertThat(parameters.getParameter(0).isDynamicProjectionParameter(), is(true)); + assertThat(parameters.getParameter(1).isDynamicProjectionParameter(), is(false)); + assertThat(parameters.getParameter(2).isDynamicProjectionParameter(), is(false)); + } + + private Parameters getParametersFor(String methodName, Class... parameterTypes) throws SecurityException, NoSuchMethodException { Method method = SampleDao.class.getMethod(methodName, parameterTypes); @@ -181,5 +194,6 @@ public class ParametersUnitTests { User emptyParameters(); + T dynamicBind(Class type, Class one, Class two); } } diff --git a/src/test/java/org/springframework/data/repository/query/QueryMethodUnitTests.java b/src/test/java/org/springframework/data/repository/query/QueryMethodUnitTests.java index 55c1dbf34..5a8a0f1fc 100644 --- a/src/test/java/org/springframework/data/repository/query/QueryMethodUnitTests.java +++ b/src/test/java/org/springframework/data/repository/query/QueryMethodUnitTests.java @@ -31,6 +31,8 @@ import org.springframework.core.SpringVersion; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; +import org.springframework.data.projection.ProjectionFactory; +import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; @@ -48,6 +50,7 @@ public class QueryMethodUnitTests { private static final Version FOUR_DOT_TWO = new Version(4, 2); RepositoryMetadata metadata = new DefaultRepositoryMetadata(SampleRepository.class); + ProjectionFactory factory = new SpelAwareProxyProjectionFactory(); /** * @see DATAJPA-59 @@ -56,7 +59,7 @@ public class QueryMethodUnitTests { public void rejectsPagingMethodWithInvalidReturnType() throws Exception { Method method = SampleRepository.class.getMethod("pagingMethodWithInvalidReturnType", Pageable.class); - new QueryMethod(method, metadata); + new QueryMethod(method, metadata, factory); } /** @@ -65,7 +68,7 @@ public class QueryMethodUnitTests { @Test(expected = IllegalArgumentException.class) public void rejectsPagingMethodWithoutPageable() throws Exception { Method method = SampleRepository.class.getMethod("pagingMethodWithoutPageable"); - new QueryMethod(method, metadata); + new QueryMethod(method, metadata, factory); } /** @@ -74,7 +77,7 @@ public class QueryMethodUnitTests { @Test public void setsUpSimpleQueryMethodCorrectly() throws Exception { Method method = SampleRepository.class.getMethod("findByUsername", String.class); - new QueryMethod(method, metadata); + new QueryMethod(method, metadata, factory); } /** @@ -83,7 +86,7 @@ public class QueryMethodUnitTests { @Test public void considersIterableMethodForCollectionQuery() throws Exception { Method method = SampleRepository.class.getMethod("sampleMethod"); - QueryMethod queryMethod = new QueryMethod(method, metadata); + QueryMethod queryMethod = new QueryMethod(method, metadata, factory); assertThat(queryMethod.isCollectionQuery(), is(true)); } @@ -93,7 +96,7 @@ public class QueryMethodUnitTests { @Test public void doesNotConsiderPageMethodCollectionQuery() throws Exception { Method method = SampleRepository.class.getMethod("anotherSampleMethod", Pageable.class); - QueryMethod queryMethod = new QueryMethod(method, metadata); + QueryMethod queryMethod = new QueryMethod(method, metadata, factory); assertThat(queryMethod.isPageQuery(), is(true)); assertThat(queryMethod.isCollectionQuery(), is(false)); } @@ -105,7 +108,7 @@ public class QueryMethodUnitTests { public void detectsAnEntityBeingReturned() throws Exception { Method method = SampleRepository.class.getMethod("returnsEntitySubclass"); - QueryMethod queryMethod = new QueryMethod(method, metadata); + QueryMethod queryMethod = new QueryMethod(method, metadata, factory); assertThat(queryMethod.isQueryForEntity(), is(true)); } @@ -117,7 +120,7 @@ public class QueryMethodUnitTests { public void detectsNonEntityBeingReturned() throws Exception { Method method = SampleRepository.class.getMethod("returnsProjection"); - QueryMethod queryMethod = new QueryMethod(method, metadata); + QueryMethod queryMethod = new QueryMethod(method, metadata, factory); assertThat(queryMethod.isQueryForEntity(), is(false)); } @@ -130,7 +133,7 @@ public class QueryMethodUnitTests { RepositoryMetadata repositoryMetadata = new DefaultRepositoryMetadata(SampleRepository.class); Method method = SampleRepository.class.getMethod("sliceOfUsers"); - QueryMethod queryMethod = new QueryMethod(method, repositoryMetadata); + QueryMethod queryMethod = new QueryMethod(method, repositoryMetadata, factory); assertThat(queryMethod.isSliceQuery(), is(true)); assertThat(queryMethod.isCollectionQuery(), is(false)); @@ -146,7 +149,7 @@ public class QueryMethodUnitTests { RepositoryMetadata repositoryMetadata = new DefaultRepositoryMetadata(SampleRepository.class); Method method = SampleRepository.class.getMethod("arrayOfUsers"); - assertThat(new QueryMethod(method, repositoryMetadata).isCollectionQuery(), is(true)); + assertThat(new QueryMethod(method, repositoryMetadata, factory).isCollectionQuery(), is(true)); } /** @@ -158,7 +161,7 @@ public class QueryMethodUnitTests { RepositoryMetadata repositoryMetadata = new DefaultRepositoryMetadata(SampleRepository.class); Method method = SampleRepository.class.getMethod("streaming"); - assertThat(new QueryMethod(method, repositoryMetadata).isStreamQuery(), is(true)); + assertThat(new QueryMethod(method, repositoryMetadata, factory).isStreamQuery(), is(true)); } /** @@ -170,7 +173,7 @@ public class QueryMethodUnitTests { RepositoryMetadata repositoryMetadata = new DefaultRepositoryMetadata(SampleRepository.class); Method method = SampleRepository.class.getMethod("streaming", Pageable.class); - assertThat(new QueryMethod(method, repositoryMetadata).isStreamQuery(), is(true)); + assertThat(new QueryMethod(method, repositoryMetadata, factory).isStreamQuery(), is(true)); } /** @@ -182,7 +185,7 @@ public class QueryMethodUnitTests { RepositoryMetadata repositoryMetadata = new DefaultRepositoryMetadata(SampleRepository.class); Method method = SampleRepository.class.getMethod("returnsCompletableFutureForSingleEntity"); - assertThat(new QueryMethod(method, repositoryMetadata).isCollectionQuery(), is(false)); + assertThat(new QueryMethod(method, repositoryMetadata, factory).isCollectionQuery(), is(false)); } /** @@ -196,7 +199,7 @@ public class QueryMethodUnitTests { RepositoryMetadata repositoryMetadata = new DefaultRepositoryMetadata(SampleRepository.class); Method method = SampleRepository.class.getMethod("returnsCompletableFutureForEntityCollection"); - assertThat(new QueryMethod(method, repositoryMetadata).isCollectionQuery(), is(true)); + assertThat(new QueryMethod(method, repositoryMetadata, factory).isCollectionQuery(), is(true)); } /** @@ -208,7 +211,7 @@ public class QueryMethodUnitTests { RepositoryMetadata repositoryMetadata = new DefaultRepositoryMetadata(SampleRepository.class); Method method = SampleRepository.class.getMethod("returnsFutureForSingleEntity"); - assertThat(new QueryMethod(method, repositoryMetadata).isCollectionQuery(), is(false)); + assertThat(new QueryMethod(method, repositoryMetadata, factory).isCollectionQuery(), is(false)); } /** @@ -220,7 +223,7 @@ public class QueryMethodUnitTests { RepositoryMetadata repositoryMetadata = new DefaultRepositoryMetadata(SampleRepository.class); Method method = SampleRepository.class.getMethod("returnsFutureForEntityCollection"); - assertThat(new QueryMethod(method, repositoryMetadata).isCollectionQuery(), is(true)); + assertThat(new QueryMethod(method, repositoryMetadata, factory).isCollectionQuery(), is(true)); } interface SampleRepository extends Repository { diff --git a/src/test/java/org/springframework/data/repository/query/ResultProcessorUnitTests.java b/src/test/java/org/springframework/data/repository/query/ResultProcessorUnitTests.java new file mode 100644 index 000000000..8e046b38d --- /dev/null +++ b/src/test/java/org/springframework/data/repository/query/ResultProcessorUnitTests.java @@ -0,0 +1,224 @@ +/* + * 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.repository.query; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.junit.Test; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.data.projection.SpelAwareProxyProjectionFactory; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; + +/** + * @author Oliver Gierke + */ +public class ResultProcessorUnitTests { + + /** + * @see DATACMNS-89 + */ + @Test + public void leavesNonProjectingResultUntouched() throws Exception { + + ResultProcessor information = new ResultProcessor(getQueryMethod("findAll"), new SpelAwareProxyProjectionFactory()); + + Sample sample = new Sample("Dave", "Matthews"); + List result = new ArrayList(Arrays.asList(sample)); + List converted = information.processResult(result); + + assertThat(converted, contains(sample)); + } + + /** + * @see DATACMNS-89 + */ + @Test + public void createsProjectionFromProperties() throws Exception { + + ResultProcessor information = getFactory("findOneProjection"); + + SampleProjection result = information.processResult(Arrays.asList("Matthews")); + + assertThat(result.getLastname(), is("Matthews")); + } + + /** + * @see DATACMNS-89 + */ + @Test + @SuppressWarnings("unchecked") + public void createsListOfProjectionsFormNestedLists() throws Exception { + + ResultProcessor information = getFactory("findAllProjection"); + + List columns = Arrays.asList("Matthews"); + List> source = new ArrayList>(Arrays.asList(columns)); + + List result = information.processResult(source); + + assertThat(result, hasSize(1)); + assertThat(result.get(0).getLastname(), is("Matthews")); + } + + /** + * @see DATACMNS-89 + */ + @Test + @SuppressWarnings("unchecked") + public void createsListOfProjectionsFromMaps() throws Exception { + + ResultProcessor information = getFactory("findAllProjection"); + + List> source = new ArrayList>( + Arrays.asList(Collections. singletonMap("lastname", "Matthews"))); + + List result = information.processResult(source); + + assertThat(result, hasSize(1)); + assertThat(result.get(0).getLastname(), is("Matthews")); + } + + /** + * @see DATACMNS-89 + */ + @Test + public void createsListOfProjectionsFromEntity() throws Exception { + + ResultProcessor information = getFactory("findAllProjection"); + + List source = new ArrayList(Arrays.asList(new Sample("Dave", "Matthews"))); + List result = information.processResult(source); + + assertThat(result, hasSize(1)); + assertThat(result.get(0).getLastname(), is("Matthews")); + } + + /** + * @see DATACMNS-89 + */ + @Test + public void createsPageOfProjectionsFromEntity() throws Exception { + + ResultProcessor information = getFactory("findPageProjection", Pageable.class); + + Page source = new PageImpl(Arrays.asList(new Sample("Dave", "Matthews"))); + Page result = information.processResult(source); + + assertThat(result.getContent(), hasSize(1)); + assertThat(result.getContent().get(0).getLastname(), is("Matthews")); + } + + /** + * @see DATACMNS-89 + */ + @Test + public void createsDynamicProjectionFromEntity() throws Exception { + + ResultProcessor information = getFactory("findOneOpenProjection"); + + OpenProjection result = information.processResult(new Sample("Dave", "Matthews")); + + assertThat(result.getLastname(), is("Matthews")); + assertThat(result.getFullName(), is("Dave Matthews")); + } + + /** + * @see DATACMNS-89 + */ + @Test + public void findsDynamicProjection() throws Exception { + + ParameterAccessor accessor = mock(ParameterAccessor.class); + + ResultProcessor factory = getFactory("findOneDynamic", Class.class); + assertThat(factory.withDynamicProjection(null), is(factory)); + assertThat(factory.withDynamicProjection(accessor), is(factory)); + + doReturn(SampleProjection.class).when(accessor).getDynamicProjection(); + + ResultProcessor processor = factory.withDynamicProjection(accessor); + assertThat(processor.getReturnedType().getReturnedType(), is(typeCompatibleWith(SampleProjection.class))); + } + + private static ResultProcessor getFactory(String methodName, Class... parameters) throws Exception { + return getQueryMethod(methodName, parameters).getResultProcessor(); + } + + private static QueryMethod getQueryMethod(String name, Class... parameters) throws Exception { + + Method method = SampleRepository.class.getMethod(name, parameters); + return new QueryMethod(method, new DefaultRepositoryMetadata(SampleRepository.class), + new SpelAwareProxyProjectionFactory()); + } + + interface SampleRepository extends Repository { + + List findAll(); + + List findAllDtos(); + + List findAllProjection(); + + Sample findOne(); + + SampleDTO findOneDto(); + + SampleProjection findOneProjection(); + + OpenProjection findOneOpenProjection(); + + Page findPageProjection(Pageable pageable); + + T findOneDynamic(Class type); + } + + static class Sample { + public String firstname, lastname; + + public Sample(String firstname, String lastname) { + this.firstname = firstname; + this.lastname = lastname; + } + } + + static class SampleDTO {} + + interface SampleProjection { + + String getLastname(); + } + + interface OpenProjection { + + String getLastname(); + + @Value("#{target.firstname + ' ' + target.lastname}") + String getFullName(); + } +} diff --git a/src/test/java/org/springframework/data/repository/query/ReturnedTypeUnitTests.java b/src/test/java/org/springframework/data/repository/query/ReturnedTypeUnitTests.java new file mode 100644 index 000000000..3f42c3632 --- /dev/null +++ b/src/test/java/org/springframework/data/repository/query/ReturnedTypeUnitTests.java @@ -0,0 +1,179 @@ +/* + * 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.repository.query; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +import java.lang.reflect.Method; +import java.util.List; + +import org.junit.Test; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.projection.SpelAwareProxyProjectionFactory; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; + +/** + * Unit tests for {@link ReturnedType}. + * + * @author Oliver Gierke + */ +public class ReturnedTypeUnitTests { + + /** + * @see DATACMNS-89 + */ + @Test + public void treatsSimpleDomainTypeAsIs() throws Exception { + + ReturnedType type = getReturnedType("findAll"); + + assertThat(type.getTypeToRead(), is(typeCompatibleWith(Sample.class))); + assertThat(type.getInputProperties(), is(empty())); + assertThat(type.isProjecting(), is(false)); + assertThat(type.needsCustomConstruction(), is(false)); + } + + /** + * @see DATACMNS-89 + */ + @Test + public void detectsDto() throws Exception { + + ReturnedType type = getReturnedType("findAllDtos"); + + assertThat(type.getTypeToRead(), is(typeCompatibleWith(SampleDto.class))); + assertThat(type.getInputProperties(), contains("firstname")); + assertThat(type.isInstance(new SampleDto("firstname")), is(true)); + assertThat(type.isProjecting(), is(true)); + assertThat(type.needsCustomConstruction(), is(true)); + } + + /** + * @see DATACMNS-89 + */ + @Test + public void detectsProjection() throws Exception { + + ReturnedType type = getReturnedType("findAllProjection"); + + assertThat(type.getTypeToRead(), is(nullValue())); + assertThat(type.getInputProperties(), contains("lastname")); + } + + /** + * @see DATACMNS-89 + */ + @Test + public void detectsVoidMethod() throws Exception { + + ReturnedType type = getReturnedType("voidMethod"); + + assertThat(type.getDomainType(), is(typeCompatibleWith(Sample.class))); + assertThat(type.getReturnedType(), is(typeCompatibleWith(void.class))); + } + + /** + * @see DATACMNS-89 + */ + @Test + public void detectsClosedProjection() throws Exception { + + ReturnedType type = getReturnedType("findOneProjection"); + + assertThat(type.getReturnedType(), is(typeCompatibleWith(SampleProjection.class))); + assertThat(type.isProjecting(), is(true)); + assertThat(type.needsCustomConstruction(), is(true)); + } + + /** + * @see DATACMNS-89 + */ + @Test + public void detectsOpenProjection() throws Exception { + + ReturnedType type = getReturnedType("findOneOpenProjection"); + + assertThat(type.getReturnedType(), is(typeCompatibleWith(OpenProjection.class))); + assertThat(type.isProjecting(), is(true)); + assertThat(type.needsCustomConstruction(), is(false)); + assertThat(type.getTypeToRead(), is(typeCompatibleWith(Sample.class))); + } + + private static ReturnedType getReturnedType(String methodName, Class... parameters) throws Exception { + return getQueryMethod(methodName, parameters).getResultProcessor().getReturnedType(); + } + + private static QueryMethod getQueryMethod(String name, Class... parameters) throws Exception { + + Method method = SampleRepository.class.getMethod(name, parameters); + return new QueryMethod(method, new DefaultRepositoryMetadata(SampleRepository.class), + new SpelAwareProxyProjectionFactory()); + } + + interface SampleRepository extends Repository { + + void voidMethod(); + + List findAll(); + + List findAllDtos(); + + List findAllProjection(); + + Sample findOne(); + + SampleDto findOneDto(); + + SampleProjection findOneProjection(); + + OpenProjection findOneOpenProjection(); + + Page findPageProjection(Pageable pageable); + } + + static class Sample { + public String firstname, lastname; + + public Sample(String firstname, String lastname) { + this.firstname = firstname; + this.lastname = lastname; + } + } + + static class SampleDto { + + public SampleDto(String firstname) { + + } + } + + interface SampleProjection { + + String getLastname(); + } + + interface OpenProjection { + + String getLastname(); + + @Value("#{target.firstname + ' ' + target.lastname}") + String getFullName(); + } +} diff --git a/template.mf b/template.mf index ae049ade5..808981669 100644 --- a/template.mf +++ b/template.mf @@ -4,6 +4,8 @@ Bundle-Vendor: Pivotal Software, Inc. Bundle-Version: ${project.version} Bundle-ManifestVersion: 2 Bundle-RequiredExecutionEnvironment: JavaSE-1.6 +Excluded-Imports: + lombok.* Import-Package: sun.reflect;version="0";resolution:=optional Import-Template: