diff --git a/pom.xml b/pom.xml index 74b071dfb6..ff331b2cd8 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-commons - 1.12.0.BUILD-SNAPSHOT + 1.12.0.DATACMNS-89-SNAPSHOT Spring Data Core 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 0000000000..4cb6adb286 --- /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 8bc21702f3..a4cfbb3765 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 0000000000..a98d27f4b7 --- /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 d44945562e..c2a653c830 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.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 @@ * @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; - /* - * (non-Javadoc) + /** * @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.resourceLoader = resourceLoader; + this.classLoader = resourceLoader.getClassLoader(); + } + + /* + * (non-Javadoc) + * @see org.springframework.beans.factory.BeanClassLoaderAware#setBeanClassLoader(java.lang.ClassLoader) + */ + @Override + public void setBeanClassLoader(ClassLoader classLoader) { + this.classLoader = classLoader; } /* @@ -85,8 +95,7 @@ public T createProjection(Class projectionType, Object source) { 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 @@ 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); + 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 @@ public List getInputProperties(Class projectionType) { @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 @@ protected MethodInterceptor postProcessAccessorInterceptor(MethodInterceptor int 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 b3f698ff90..277179cfcd 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 @@ protected MethodInterceptor postProcessAccessorInterceptor(MethodInterceptor int 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) { + + Method readMethod = descriptor.getReadMethod(); + + if (readMethod == null) { + return false; + } - return AnnotationUtils.findAnnotation(readMethod, Value.class) == null; + 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 025429e696..12ef4656cd 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 @@ 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 @@ * @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 void setBeanClassLoader(ClassLoader classLoader) { 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 EntityInformation getEntityInformation() { */ 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 void afterPropertiesSet() { 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 56c5ae52b3..b45ec1757f 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.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 @@ * * @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 void setBeanClassLoader(ClassLoader classLoader) { 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 @@ private void validate(RepositoryInformation repositoryInformation, Object custom 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 QueryExecutorMethodInterceptor(RepositoryInformation repositoryInformatio 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 9c9f3df6ea..b90c84f053 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 @@ 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 @@ protected Parameter(MethodParameter parameter) { * @see #TYPES */ public boolean isSpecialParameter() { - return TYPES.contains(parameter.getParameterType()); + return isDynamicProjectionParameter || TYPES.contains(parameter.getParameterType()); } /** @@ -69,6 +75,15 @@ public boolean isBindable() { 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 @@ boolean isPageable() { 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 ecdfa33ab7..a100e3e285 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 4d08108077..d1af70c6e1 100644 --- a/src/main/java/org/springframework/data/repository/query/Parameters.java +++ b/src/main/java/org/springframework/data/repository/query/Parameters.java @@ -37,18 +37,19 @@ */ 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(); private final int pageableIndex; private final int sortIndex; + private int dynamicProjectionIndex; private final List parameters; /** @@ -61,6 +62,7 @@ public Parameters(Method method) { Assert.notNull(method); this.parameters = new ArrayList(); + this.dynamicProjectionIndex = -1; List> types = Arrays.asList(method.getParameterTypes()); @@ -75,6 +77,10 @@ public Parameters(Method method) { throw new IllegalArgumentException(PARAM_ON_SPECIAL); } + if (parameter.isDynamicProjectionParameter()) { + this.dynamicProjectionIndex = parameter.getIndex(); + } + parameters.add(parameter); } @@ -95,6 +101,7 @@ protected Parameters(List originals) { int pageableIndexTemp = -1; int sortIndexTemp = -1; + int dynamicProjectionTemp = -1; for (int i = 0; i < originals.size(); i++) { @@ -103,10 +110,12 @@ protected Parameters(List originals) { 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 +164,25 @@ public boolean hasSortParameter() { 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 493fbf7c1d..84ca492f4e 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 Sort getSort() { 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 b145ea027e..b786f0c169 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 @@ 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 static Key create(String xml) { /** * 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 b80b4f4ce9..8d979a0d6a 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.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; @@ -54,10 +56,11 @@ public class QueryMethod { * @param method must not be {@literal null} * @param metadata 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 +92,8 @@ public QueryMethod(Method method, RepositoryMetadata metadata) { 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 +233,15 @@ public boolean isStreamQuery() { 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 0000000000..2414a278bb --- /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 0000000000..f2632fcb17 --- /dev/null +++ b/src/main/java/org/springframework/data/repository/query/ReturnedType.java @@ -0,0 +1,307 @@ +/* + * 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 + * @param domainType + * @param factory + * @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(); + } + + 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 8d875abbf8..632b13b964 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 @@ /** * 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 32e46fe5f1..b918f22acb 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 @@ 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 @@ * @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 void setBeanFactory(BeanFactory beanFactory) throws BeansException { 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 0000000000..e12979e649 --- /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 91c7830ac1..5677fc189a 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 @@ 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 void createsEmptyMapBasedProxy() { @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 void convertsPrimitiveValues() { 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 8d5de435fb..6269561ebc 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 void excludesAtValueAnnotatedMethodsForInputProperties() { 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 8279df80e2..a05c1d2342 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.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 DummyRepositoryFactory(Object repository) { 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 2693f90b9a..77824eaab2 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 @@ -26,6 +26,7 @@ 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 @@ @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 void skipsQueryLookupsIfQueryLookupStrategyIsNull() { 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 7024ecde56..86c0eb3a41 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.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 void invokesCustomQueryCreationListenerForSpecialRepositoryQueryOnly() th 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 be7e21df4d..524f8d6358 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 void doesNotConsiderParameterExplicitlyNamedEvenIfNamePresent() throws Ex 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 @@ static interface SampleDao { 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 55c1dbf340..5a8a0f1fc8 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.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 void rejectsPagingMethodWithInvalidReturnType() throws Exception { @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 void rejectsPagingMethodWithoutPageable() throws Exception { @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 void setsUpSimpleQueryMethodCorrectly() throws Exception { @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 void considersIterableMethodForCollectionQuery() throws Exception { @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 void doesNotConsiderPageMethodCollectionQuery() throws Exception { 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 void detectsAnEntityBeingReturned() throws Exception { 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 void detectsSliceMethod() throws Exception { 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 void detectsCollectionMethodForArrayRetrunType() throws Exception { 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 void considersMethodReturningAStreamStreaming() throws Exception { 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 void doesNotRejectStreamingForPagination() throws Exception { 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 void doesNotRejectCompletableFutureQueryForSingleEntity() throws Exceptio 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 void doesNotRejectCompletableFutureQueryForEntityCollection() throws Exce 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 void doesNotRejectFutureQueryForSingleEntity() throws Exception { 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 void doesNotRejectFutureQueryForEntityCollection() throws Exception { 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 0000000000..8e046b38df --- /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 0000000000..80706141da --- /dev/null +++ b/src/test/java/org/springframework/data/repository/query/ReturnedTypeUnitTests.java @@ -0,0 +1,177 @@ +/* + * 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; + +/** + * @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 ae049ade50..808981669f 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: