diff --git a/src/main/java/org/springframework/data/mapping/context/AbstractMappingContext.java b/src/main/java/org/springframework/data/mapping/context/AbstractMappingContext.java index fc39cca9e..6f303b41c 100644 --- a/src/main/java/org/springframework/data/mapping/context/AbstractMappingContext.java +++ b/src/main/java/org/springframework/data/mapping/context/AbstractMappingContext.java @@ -37,6 +37,8 @@ import java.util.stream.Collectors; import org.springframework.beans.BeanUtils; import org.springframework.beans.BeansException; import org.springframework.beans.factory.InitializingBean; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisherAware; import org.springframework.data.mapping.MappingException; @@ -50,6 +52,8 @@ import org.springframework.data.mapping.model.MutablePersistentEntity; import org.springframework.data.mapping.model.PersistentPropertyAccessorFactory; import org.springframework.data.mapping.model.Property; import org.springframework.data.mapping.model.SimpleTypeHolder; +import org.springframework.data.spel.EvaluationContextProvider; +import org.springframework.data.spel.ExtensionAwareEvaluationContextProvider; import org.springframework.data.util.ClassTypeInformation; import org.springframework.data.util.Optionals; import org.springframework.data.util.Streamable; @@ -79,7 +83,7 @@ import org.springframework.util.ReflectionUtils.FieldFilter; * @author Christoph Strobl */ public abstract class AbstractMappingContext, P extends PersistentProperty

> - implements MappingContext, ApplicationEventPublisherAware, InitializingBean { + implements MappingContext, ApplicationEventPublisherAware, ApplicationContextAware, InitializingBean { private final Optional NONE = Optional.empty(); private final Map, Optional> persistentEntities = new HashMap<>(); @@ -87,6 +91,7 @@ public abstract class AbstractMappingContext persistentPropertyPathFactory; private @Nullable ApplicationEventPublisher applicationEventPublisher; + private EvaluationContextProvider evaluationContextProvider = EvaluationContextProvider.DEFAULT; private Set> initialEntitySet = new HashSet<>(); private boolean strict = false; @@ -109,6 +114,20 @@ public abstract class AbstractMappingContext> implement private @Nullable P idProperty; private @Nullable P versionProperty; private PersistentPropertyAccessorFactory propertyAccessorFactory; + private EvaluationContextProvider evaluationContextProvider = EvaluationContextProvider.DEFAULT; private final Lazy typeAlias; @@ -240,6 +243,15 @@ public class BasicPersistentEntity> implement } } + /* + * (non-Javadoc) + * @see org.springframework.data.mapping.model.MutablePersistentEntity#setEvaluationContextProvider(org.springframework.data.spel.EvaluationContextProvider) + */ + @Override + public void setEvaluationContextProvider(EvaluationContextProvider provider) { + this.evaluationContextProvider = provider; + } + /** * Returns the given property if it is a better candidate for the id property than the current id property. * @@ -464,11 +476,19 @@ public class BasicPersistentEntity> implement return hasIdProperty() ? new IdPropertyIdentifierAccessor(this, bean) : new AbsentIdentifierAccessor(bean); } + /* + * (non-Javadoc) + * @see java.lang.Iterable#iterator() + */ @Override public Iterator

iterator() { return Collections.unmodifiableList(properties).iterator(); } + protected EvaluationContext getEvaluationContext(Object rootObject) { + return evaluationContextProvider.getEvaluationContext(rootObject); + } + /** * Calculates the {@link Alias} to be used for the given type. * diff --git a/src/main/java/org/springframework/data/mapping/model/MutablePersistentEntity.java b/src/main/java/org/springframework/data/mapping/model/MutablePersistentEntity.java index 9fbb5e9e8..408ceb63c 100644 --- a/src/main/java/org/springframework/data/mapping/model/MutablePersistentEntity.java +++ b/src/main/java/org/springframework/data/mapping/model/MutablePersistentEntity.java @@ -20,6 +20,7 @@ import org.springframework.data.mapping.MappingException; import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.mapping.PersistentPropertyAccessor; +import org.springframework.data.spel.EvaluationContextProvider; /** * Interface capturing mutator methods for {@link PersistentEntity}s. @@ -58,4 +59,11 @@ public interface MutablePersistentEntity> ext * @param factory must not be {@literal null}. */ void setPersistentPropertyAccessorFactory(PersistentPropertyAccessorFactory factory); + + /** + * Sets the {@link EvaluationContextProvider} to be used by the entity. + * + * @param provider must not be {@literal null}. + */ + void setEvaluationContextProvider(EvaluationContextProvider provider); } diff --git a/src/main/java/org/springframework/data/repository/cdi/CdiRepositoryBean.java b/src/main/java/org/springframework/data/repository/cdi/CdiRepositoryBean.java index aa0e3adfd..55eefff6a 100644 --- a/src/main/java/org/springframework/data/repository/cdi/CdiRepositoryBean.java +++ b/src/main/java/org/springframework/data/repository/cdi/CdiRepositoryBean.java @@ -44,6 +44,7 @@ import org.springframework.data.repository.config.RepositoryFragmentConfiguratio import org.springframework.data.repository.core.support.RepositoryComposition.RepositoryFragments; import org.springframework.data.repository.core.support.RepositoryFactorySupport; import org.springframework.data.repository.core.support.RepositoryFragment; +import org.springframework.data.util.Optionals; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -53,7 +54,7 @@ import org.springframework.util.StringUtils; * * @author Dirk Mahler * @author Oliver Gierke - * @author Mark Paluch + * @author Mark Paluchs * @author Peter Rietzler * @author Jens Schauder * @author Christoph Strobl @@ -392,14 +393,13 @@ public abstract class CdiRepositoryBean implements Bean, PassivationCapabl return fragmentConfigurations.flatMap(it -> { - Class interfaceClass = (Class) lookupFragmentInterface(repositoryType, it.getInterfaceName()); + Class interfaceClass = (Class) lookupFragmentInterface(repositoryType, it.getInterfaceName()); Class implementationClass = context.loadClass(it.getClassName()); Optional> bean = getBean(implementationClass, beanManager, qualifiers); - return bean.map(this::getDependencyInstance) // - .map(implementation -> RepositoryFragment.implemented(interfaceClass, implementation)) // - .map(Stream::of) // - .orElse(Stream.empty()); + return Optionals.toStream(bean.map(this::getDependencyInstance) // + .map(implementation -> RepositoryFragment.implemented(interfaceClass, implementation))); // + }).collect(Collectors.toList()); } diff --git a/src/main/java/org/springframework/data/repository/cdi/CdiRepositoryConfiguration.java b/src/main/java/org/springframework/data/repository/cdi/CdiRepositoryConfiguration.java index 62360ddfc..3e486eec2 100644 --- a/src/main/java/org/springframework/data/repository/cdi/CdiRepositoryConfiguration.java +++ b/src/main/java/org/springframework/data/repository/cdi/CdiRepositoryConfiguration.java @@ -19,8 +19,8 @@ package org.springframework.data.repository.cdi; import java.util.Optional; import org.springframework.data.repository.core.NamedQueries; -import org.springframework.data.repository.query.EvaluationContextProvider; import org.springframework.data.repository.query.QueryLookupStrategy; +import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; /** * Interface containing the configurable options for the Spring Data repository subsystem using CDI. @@ -31,13 +31,13 @@ import org.springframework.data.repository.query.QueryLookupStrategy; public interface CdiRepositoryConfiguration { /** - * Return the {@link EvaluationContextProvider} to use. Can be {@link Optional#empty()} . + * Return the {@link QueryMethodEvaluationContextProvider} to use. Can be {@link Optional#empty()} . * - * @return the optional {@link EvaluationContextProvider} base to use, can be {@link Optional#empty()}, must not be - * {@literal null}. + * @return the optional {@link QueryMethodEvaluationContextProvider} base to use, can be {@link Optional#empty()}, + * must not be {@literal null}. * @since 2.1 */ - default Optional getEvaluationContextProvider() { + default Optional getEvaluationContextProvider() { return Optional.empty(); } diff --git a/src/main/java/org/springframework/data/repository/cdi/CdiRepositoryContext.java b/src/main/java/org/springframework/data/repository/cdi/CdiRepositoryContext.java index 1c8b95d8e..2969cbc12 100644 --- a/src/main/java/org/springframework/data/repository/cdi/CdiRepositoryContext.java +++ b/src/main/java/org/springframework/data/repository/cdi/CdiRepositoryContext.java @@ -44,6 +44,7 @@ import org.springframework.data.repository.config.RepositoryFragmentConfiguratio import org.springframework.data.repository.config.RepositoryFragmentDiscovery; import org.springframework.data.util.Optionals; import org.springframework.data.util.Streamable; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -159,7 +160,7 @@ public class CdiRepositoryContext { Collections.emptySet(), // BeanDefinition::getBeanClassName); - return beanDefinition.map(it -> loadClass(it.getBeanClassName())); + return beanDefinition.map(this::loadBeanClass); } private Optional detectRepositoryFragmentConfiguration( @@ -173,6 +174,14 @@ public class CdiRepositoryContext { return beanDefinition.map(bd -> new RepositoryFragmentConfiguration(configuration.getFragmentInterfaceName(), bd)); } + @Nullable + private Class loadBeanClass(AbstractBeanDefinition definition) { + + String beanClassName = definition.getBeanClassName(); + + return beanClassName == null ? null : loadClass(beanClassName); + } + private static ClassMetadata getClassMetadata(MetadataReaderFactory metadataReaderFactory, String className) { try { diff --git a/src/main/java/org/springframework/data/repository/config/RepositoryBeanDefinitionBuilder.java b/src/main/java/org/springframework/data/repository/config/RepositoryBeanDefinitionBuilder.java index dad778e53..b52b67137 100644 --- a/src/main/java/org/springframework/data/repository/config/RepositoryBeanDefinitionBuilder.java +++ b/src/main/java/org/springframework/data/repository/config/RepositoryBeanDefinitionBuilder.java @@ -30,7 +30,6 @@ import org.springframework.beans.factory.BeanDefinitionStoreException; import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.BeanDefinitionRegistry; -import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.core.env.Environment; import org.springframework.core.io.ResourceLoader; import org.springframework.core.type.ClassMetadata; @@ -40,7 +39,6 @@ import org.springframework.core.type.filter.TypeFilter; import org.springframework.data.config.ParsingUtils; import org.springframework.data.repository.core.support.RepositoryFragment; import org.springframework.data.repository.core.support.RepositoryFragmentsFactoryBean; -import org.springframework.data.repository.query.ExtensionAwareEvaluationContextProvider; import org.springframework.data.util.Optionals; import org.springframework.data.util.Streamable; import org.springframework.util.Assert; @@ -134,12 +132,6 @@ class RepositoryBeanDefinitionBuilder { builder.addPropertyValue("repositoryFragments", ParsingUtils.getSourceBeanDefinition(fragmentsBuilder, configuration.getSource())); - RootBeanDefinition evaluationContextProviderDefinition = new RootBeanDefinition( - ExtensionAwareEvaluationContextProvider.class); - evaluationContextProviderDefinition.setSource(configuration.getSource()); - - builder.addPropertyValue("evaluationContextProvider", evaluationContextProviderDefinition); - return builder; } 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 358a3cd9b..edcd767c2 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 @@ -26,6 +26,7 @@ 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.ListableBeanFactory; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisherAware; import org.springframework.data.mapping.PersistentEntity; @@ -36,11 +37,11 @@ import org.springframework.data.repository.core.NamedQueries; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.core.support.RepositoryComposition.RepositoryFragments; -import org.springframework.data.repository.query.DefaultEvaluationContextProvider; -import org.springframework.data.repository.query.EvaluationContextProvider; +import org.springframework.data.repository.query.ExtensionAwareQueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.QueryLookupStrategy; import org.springframework.data.repository.query.QueryLookupStrategy.Key; import org.springframework.data.repository.query.QueryMethod; +import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; import org.springframework.data.util.Lazy; import org.springframework.util.Assert; @@ -69,7 +70,7 @@ public abstract class RepositoryFactoryBeanSupport, private ClassLoader classLoader; private BeanFactory beanFactory; private boolean lazyInit = false; - private EvaluationContextProvider evaluationContextProvider = DefaultEvaluationContextProvider.INSTANCE; + private Optional evaluationContextProvider = Optional.empty(); private ApplicationEventPublisher publisher; private Lazy repository; @@ -145,14 +146,13 @@ public abstract class RepositoryFactoryBeanSupport, } /** - * Sets the {@link EvaluationContextProvider} to be used to evaluate SpEL expressions in manually defined queries. + * Sets the {@link QueryMethodEvaluationContextProvider} to be used to evaluate SpEL expressions in manually defined + * queries. * - * @param evaluationContextProvider can be {@literal null}, defaults to - * {@link DefaultEvaluationContextProvider#INSTANCE}. + * @param evaluationContextProvider must not be {@literal null}. */ - public void setEvaluationContextProvider(EvaluationContextProvider evaluationContextProvider) { - this.evaluationContextProvider = evaluationContextProvider == null ? DefaultEvaluationContextProvider.INSTANCE - : evaluationContextProvider; + public void setEvaluationContextProvider(QueryMethodEvaluationContextProvider evaluationContextProvider) { + this.evaluationContextProvider = Optional.of(evaluationContextProvider); } /** @@ -179,7 +179,13 @@ public abstract class RepositoryFactoryBeanSupport, */ @Override public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + this.beanFactory = beanFactory; + + if (!this.evaluationContextProvider.isPresent() && ListableBeanFactory.class.isInstance(beanFactory)) { + this.evaluationContextProvider = Optional + .of(new ExtensionAwareQueryMethodEvaluationContextProvider((ListableBeanFactory) beanFactory)); + } } /* @@ -265,7 +271,8 @@ public abstract class RepositoryFactoryBeanSupport, this.factory = createRepositoryFactory(); this.factory.setQueryLookupStrategyKey(queryLookupStrategyKey); this.factory.setNamedQueries(namedQueries); - this.factory.setEvaluationContextProvider(evaluationContextProvider); + this.factory.setEvaluationContextProvider( + evaluationContextProvider.orElseGet(() -> QueryMethodEvaluationContextProvider.DEFAULT)); this.factory.setBeanClassLoader(classLoader); this.factory.setBeanFactory(beanFactory); 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 22ff9a0ee..297239c8d 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 @@ -51,11 +51,10 @@ import org.springframework.data.repository.core.NamedQueries; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.core.support.RepositoryComposition.RepositoryFragments; -import org.springframework.data.repository.query.DefaultEvaluationContextProvider; -import org.springframework.data.repository.query.EvaluationContextProvider; import org.springframework.data.repository.query.QueryLookupStrategy; import org.springframework.data.repository.query.QueryLookupStrategy.Key; import org.springframework.data.repository.query.QueryMethod; +import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.util.ClassUtils; import org.springframework.data.repository.util.ReactiveWrapperConverters; @@ -119,7 +118,7 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware, private List> queryPostProcessors; private NamedQueries namedQueries; private ClassLoader classLoader; - private EvaluationContextProvider evaluationContextProvider; + private QueryMethodEvaluationContextProvider evaluationContextProvider; private BeanFactory beanFactory; private final QueryCollectingQueryCreationListener collectingListener = new QueryCollectingQueryCreationListener(); @@ -133,7 +132,7 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware, this.repositoryBaseClass = Optional.empty(); this.namedQueries = PropertiesBasedNamedQueries.EMPTY; this.classLoader = org.springframework.util.ClassUtils.getDefaultClassLoader(); - this.evaluationContextProvider = DefaultEvaluationContextProvider.INSTANCE; + this.evaluationContextProvider = QueryMethodEvaluationContextProvider.DEFAULT; this.queryPostProcessors = new ArrayList<>(); this.queryPostProcessors.add(collectingListener); } @@ -175,13 +174,14 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware, } /** - * Sets the {@link EvaluationContextProvider} to be used to evaluate SpEL expressions in manually defined queries. + * Sets the {@link QueryMethodEvaluationContextProvider} to be used to evaluate SpEL expressions in manually defined + * queries. * * @param evaluationContextProvider can be {@literal null}, defaults to - * {@link DefaultEvaluationContextProvider#INSTANCE}. + * {@link DefaultQueryMethodEvaluationContextProvider#INSTANCE}. */ - public void setEvaluationContextProvider(EvaluationContextProvider evaluationContextProvider) { - this.evaluationContextProvider = evaluationContextProvider == null ? DefaultEvaluationContextProvider.INSTANCE + public void setEvaluationContextProvider(QueryMethodEvaluationContextProvider evaluationContextProvider) { + this.evaluationContextProvider = evaluationContextProvider == null ? QueryMethodEvaluationContextProvider.DEFAULT : evaluationContextProvider; } @@ -431,7 +431,7 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware, protected abstract Class getRepositoryBaseClass(RepositoryMetadata metadata); /** - * Returns the {@link QueryLookupStrategy} for the given {@link Key} and {@link EvaluationContextProvider}. + * Returns the {@link QueryLookupStrategy} for the given {@link Key} and {@link QueryMethodEvaluationContextProvider}. * * @param key can be {@literal null}. * @param evaluationContextProvider will never be {@literal null}. @@ -439,7 +439,7 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware, * @since 1.9 */ protected Optional getQueryLookupStrategy(@Nullable Key key, - EvaluationContextProvider evaluationContextProvider) { + QueryMethodEvaluationContextProvider evaluationContextProvider) { return Optional.empty(); } diff --git a/src/main/java/org/springframework/data/repository/query/ExtensionAwareQueryMethodEvaluationContextProvider.java b/src/main/java/org/springframework/data/repository/query/ExtensionAwareQueryMethodEvaluationContextProvider.java new file mode 100644 index 000000000..3de7cfb94 --- /dev/null +++ b/src/main/java/org/springframework/data/repository/query/ExtensionAwareQueryMethodEvaluationContextProvider.java @@ -0,0 +1,236 @@ +/* + * Copyright 2014-2018 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.RequiredArgsConstructor; + +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.beans.factory.ListableBeanFactory; +import org.springframework.data.spel.spi.EvaluationContextExtension; +import org.springframework.data.util.Lazy; +import org.springframework.data.util.Streamable; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.spel.support.StandardEvaluationContext; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ConcurrentReferenceHashMap; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.StringUtils; + +/** + * An {@link QueryMethodEvaluationContextProvider} that assembles an {@link EvaluationContext} from a list of + * {@link EvaluationContextExtension} instances. + * + * @author Thomas Darimont + * @author Oliver Gierke + * @author Christoph Strobl + * @author Jens Schauder + * @since 1.9 + */ +public class ExtensionAwareQueryMethodEvaluationContextProvider implements QueryMethodEvaluationContextProvider { + + private final Lazy delegate; + + /** + * Creates a new {@link ExtensionAwareQueryMethodEvaluationContextProvider}. + * + * @param beanFactory the {@link ListableBeanFactory} to lookup the {@link EvaluationContextExtension}s from, must not + * be {@literal null}. + */ + @SuppressWarnings("deprecation") + public ExtensionAwareQueryMethodEvaluationContextProvider(ListableBeanFactory beanFactory) { + + Assert.notNull(beanFactory, "ListableBeanFactory must not be null!"); + + this.delegate = Lazy.of(() -> { + + org.springframework.data.spel.ExtensionAwareEvaluationContextProvider delegate = new org.springframework.data.spel.ExtensionAwareEvaluationContextProvider( + () -> getExtensionsFrom(beanFactory)); + delegate.setBeanFactory(beanFactory); + + return delegate; + }); + } + + /** + * Creates a new {@link ExtensionAwareQueryMethodEvaluationContextProvider} using the given + * {@link EvaluationContextExtension}s. + * + * @param extensions must not be {@literal null}. + */ + public ExtensionAwareQueryMethodEvaluationContextProvider(List extensions) { + + Assert.notNull(extensions, "EvaluationContextExtensions must not be null!"); + + this.delegate = Lazy.of(new org.springframework.data.spel.ExtensionAwareEvaluationContextProvider(extensions)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.QueryMethodEvaluationContextProvider#getEvaluationContext(org.springframework.data.repository.query.Parameters, java.lang.Object[]) + */ + @Override + public > EvaluationContext getEvaluationContext(T parameters, Object[] parameterValues) { + + StandardEvaluationContext evaluationContext = delegate.get().getEvaluationContext(parameterValues); + + evaluationContext.setVariables(collectVariables(parameters, parameterValues)); + + return evaluationContext; + } + + /** + * Exposes variables for all named parameters for the given arguments. Also exposes non-bindable parameters under the + * names of their types. + * + * @param parameters must not be {@literal null}. + * @param arguments must not be {@literal null}. + * @return + */ + private static Map collectVariables(Streamable parameters, Object[] arguments) { + + Map variables = new HashMap<>(); + + parameters.stream()// + .filter(Parameter::isSpecialParameter)// + .forEach(it -> variables.put(// + StringUtils.uncapitalize(it.getType().getSimpleName()), // + arguments[it.getIndex()])); + + parameters.stream()// + .filter(Parameter::isNamedParameter)// + .forEach(it -> variables.put(// + it.getName().orElseThrow(() -> new IllegalStateException("Should never occur!")), // + arguments[it.getIndex()])); + + return variables; + } + + /** + * Looks up all {@link EvaluationContextExtension} and + * {@link org.springframework.data.repository.query.spi.EvaluationContextExtension} instances from the given + * {@link ListableBeanFactory} and wraps the latter into proxies so that they implement the former. + * + * @param beanFactory must not be {@literal null}. + * @return + */ + @SuppressWarnings("deprecation") + private static List getExtensionsFrom(ListableBeanFactory beanFactory) { + + Stream legacyExtensions = beanFactory // + .getBeansOfType(org.springframework.data.repository.query.spi.EvaluationContextExtension.class, true, false) + .values().stream() // + .map(ExtensionAwareQueryMethodEvaluationContextProvider::adaptFromLegacyApi); + + Stream extensions = beanFactory + .getBeansOfType(EvaluationContextExtension.class, true, false).values().stream(); + + return Stream.concat(extensions, legacyExtensions).collect(Collectors.toList()); + } + + @SuppressWarnings({ "deprecation", "unchecked" }) + private static final EvaluationContextExtension adaptFromLegacyApi( + org.springframework.data.repository.query.spi.EvaluationContextExtension extension) { + + DelegatingMethodInterceptor advice = new DelegatingMethodInterceptor(extension); + advice.registerResultMapping("getFunctions", + result -> ((Map) result).entrySet().stream() + .collect(Collectors.toMap(e -> e.getKey(), e -> e.getValue().toNewFunction()))); + + ProxyFactory factory = new ProxyFactory(); + factory.setInterfaces(EvaluationContextExtension.class); + factory.setTargetClass(extension.getClass()); + factory.setProxyTargetClass(true); + factory.setTarget(extension); + factory.addAdvice(advice); + + return (EvaluationContextExtension) factory.getProxy(); + } + + /** + * A {@link MethodInterceptor} that forwards all invocations of methods (by name and parameter types) that are + * available on a given target object + * + * @author Oliver Gierke + */ + @RequiredArgsConstructor + static class DelegatingMethodInterceptor implements MethodInterceptor { + + private static final Map METHOD_CACHE = new ConcurrentReferenceHashMap(); + + private final Object target; + private final Map> directMappings = new HashMap<>(); + + /** + * Registers a result mapping for the method with the given name. Invocation results for matching methods will be + * piped through the mapping. + * + * @param methodName + * @param mapping + */ + public void registerResultMapping(String methodName, java.util.function.Function mapping) { + this.directMappings.put(methodName, mapping); + } + + /* + * (non-Javadoc) + * @see org.aopalliance.intercept.MethodInterceptor#invoke(org.aopalliance.intercept.MethodInvocation) + */ + @Nullable + @Override + public Object invoke(@Nullable MethodInvocation invocation) throws Throwable { + + if (invocation == null) { + throw new IllegalArgumentException("Invocation must not be null!"); + } + + Method method = invocation.getMethod(); + Method targetMethod = METHOD_CACHE.computeIfAbsent(method, + it -> Optional.ofNullable(findTargetMethod(it)).orElse(it)); + + Object result = method.equals(targetMethod) ? invocation.proceed() + : ReflectionUtils.invokeMethod(targetMethod, target, invocation.getArguments()); + + if (result == null) { + return result; + } + + java.util.function.Function mapper = directMappings.get(targetMethod.getName()); + + return mapper != null ? mapper.apply(result) : result; + } + + @Nullable + private Method findTargetMethod(Method method) { + + try { + return target.getClass().getMethod(method.getName(), method.getParameterTypes()); + } catch (Exception e) { + return null; + } + } + } +} diff --git a/src/main/java/org/springframework/data/repository/query/EvaluationContextProvider.java b/src/main/java/org/springframework/data/repository/query/QueryMethodEvaluationContextProvider.java similarity index 86% rename from src/main/java/org/springframework/data/repository/query/EvaluationContextProvider.java rename to src/main/java/org/springframework/data/repository/query/QueryMethodEvaluationContextProvider.java index a30b38b6c..b283124d6 100644 --- a/src/main/java/org/springframework/data/repository/query/EvaluationContextProvider.java +++ b/src/main/java/org/springframework/data/repository/query/QueryMethodEvaluationContextProvider.java @@ -15,6 +15,8 @@ */ package org.springframework.data.repository.query; +import java.util.Collections; + import org.springframework.expression.EvaluationContext; import org.springframework.expression.spel.support.StandardEvaluationContext; @@ -26,7 +28,10 @@ import org.springframework.expression.spel.support.StandardEvaluationContext; * @author Christoph Strobl * @since 1.9 */ -public interface EvaluationContextProvider { +public interface QueryMethodEvaluationContextProvider { + + static QueryMethodEvaluationContextProvider DEFAULT = new ExtensionAwareQueryMethodEvaluationContextProvider( + Collections.emptyList()); /** * Returns an {@link EvaluationContext} built using the given {@link Parameters} and parameter values. diff --git a/src/main/java/org/springframework/data/repository/query/spi/EvaluationContextExtension.java b/src/main/java/org/springframework/data/repository/query/spi/EvaluationContextExtension.java index 4c88ad388..4ba048f0c 100644 --- a/src/main/java/org/springframework/data/repository/query/spi/EvaluationContextExtension.java +++ b/src/main/java/org/springframework/data/repository/query/spi/EvaluationContextExtension.java @@ -15,20 +15,23 @@ */ package org.springframework.data.repository.query.spi; +import java.util.Collections; import java.util.Map; -import org.springframework.data.repository.query.ExtensionAwareEvaluationContextProvider; +import org.springframework.data.repository.query.ExtensionAwareQueryMethodEvaluationContextProvider; import org.springframework.expression.EvaluationContext; import org.springframework.lang.Nullable; /** * SPI to allow adding a set of properties and function definitions accessible via the root of an - * {@link EvaluationContext} provided by a {@link ExtensionAwareEvaluationContextProvider}. + * {@link EvaluationContext} provided by a {@link ExtensionAwareQueryMethodEvaluationContextProvider}. * * @author Thomas Darimont * @author Oliver Gierke * @since 1.9 + * @deprecated since 2.1. Switch to same types in {@code org.springframework.data.spel.spi}. */ +@Deprecated public interface EvaluationContextExtension { /** @@ -44,14 +47,18 @@ public interface EvaluationContextExtension { * * @return the properties */ - Map getProperties(); + default Map getProperties() { + return Collections.emptyMap(); + } /** * Returns the functions exposed by the extension. * * @return the functions */ - Map getFunctions(); + default Map getFunctions() { + return Collections.emptyMap(); + } /** * Returns the root object to be exposed by the extension. It's strongly recommended to declare the most concrete type @@ -61,5 +68,7 @@ public interface EvaluationContextExtension { * @return */ @Nullable - Object getRootObject(); + default Object getRootObject() { + return null; + } } diff --git a/src/main/java/org/springframework/data/repository/query/spi/EvaluationContextExtensionSupport.java b/src/main/java/org/springframework/data/repository/query/spi/EvaluationContextExtensionSupport.java index 885030df8..e8264e054 100644 --- a/src/main/java/org/springframework/data/repository/query/spi/EvaluationContextExtensionSupport.java +++ b/src/main/java/org/springframework/data/repository/query/spi/EvaluationContextExtensionSupport.java @@ -26,7 +26,9 @@ import org.springframework.lang.Nullable; * @author Thomas Darimont * @author Oliver Gierke * @see 1.9 + * @deprecated since 2.1. Implement {@link EvaluationContextExtension} directly. */ +@Deprecated public abstract class EvaluationContextExtensionSupport implements EvaluationContextExtension { /* diff --git a/src/main/java/org/springframework/data/repository/query/spi/Function.java b/src/main/java/org/springframework/data/repository/query/spi/Function.java index 17e3cb92e..27d2b5842 100644 --- a/src/main/java/org/springframework/data/repository/query/spi/Function.java +++ b/src/main/java/org/springframework/data/repository/query/spi/Function.java @@ -33,7 +33,9 @@ import org.springframework.util.TypeUtils; * @author Oliver Gierke * @author Jens Schauder * @since 1.9 + * @deprecated since 2.1. Switch to same types in {@code org.springframework.data.spel.spi}. */ +@Deprecated public class Function { private final Method method; @@ -67,6 +69,15 @@ public class Function { this.target = target; } + /** + * Helper method to create a {@link org.springframework.data.spel.spi.Function} from the legacy instance. + * + * @return will never be {@literal null}. + */ + public org.springframework.data.spel.spi.Function toNewFunction() { + return new org.springframework.data.spel.spi.Function(method, target); + } + /** * Invokes the function with the given arguments. * diff --git a/src/main/java/org/springframework/data/repository/query/EvaluationContextExtensionInformation.java b/src/main/java/org/springframework/data/spel/EvaluationContextExtensionInformation.java similarity index 95% rename from src/main/java/org/springframework/data/repository/query/EvaluationContextExtensionInformation.java rename to src/main/java/org/springframework/data/spel/EvaluationContextExtensionInformation.java index d3560a823..b3bd4200d 100644 --- a/src/main/java/org/springframework/data/repository/query/EvaluationContextExtensionInformation.java +++ b/src/main/java/org/springframework/data/spel/EvaluationContextExtensionInformation.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.repository.query; +package org.springframework.data.spel; import static org.springframework.data.util.StreamUtils.*; @@ -33,10 +33,10 @@ import java.util.Map; import java.util.Optional; import org.springframework.beans.BeanUtils; -import org.springframework.data.repository.query.EvaluationContextExtensionInformation.ExtensionTypeInformation.PublicMethodAndFieldFilter; -import org.springframework.data.repository.query.Functions.NameAndArgumentCount; -import org.springframework.data.repository.query.spi.EvaluationContextExtension; -import org.springframework.data.repository.query.spi.Function; +import org.springframework.data.spel.EvaluationContextExtensionInformation.ExtensionTypeInformation.PublicMethodAndFieldFilter; +import org.springframework.data.spel.Functions.NameAndArgumentCount; +import org.springframework.data.spel.spi.EvaluationContextExtension; +import org.springframework.data.spel.spi.Function; import org.springframework.data.util.Streamable; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; @@ -58,7 +58,7 @@ import org.springframework.util.ReflectionUtils.MethodFilter; * @author Oliver Gierke * @author Christoph Strobl * @author Jens Schauder - * @since 1.9 + * @since 2.1 */ class EvaluationContextExtensionInformation { diff --git a/src/main/java/org/springframework/data/repository/query/DefaultEvaluationContextProvider.java b/src/main/java/org/springframework/data/spel/EvaluationContextProvider.java similarity index 52% rename from src/main/java/org/springframework/data/repository/query/DefaultEvaluationContextProvider.java rename to src/main/java/org/springframework/data/spel/EvaluationContextProvider.java index a0eda55ae..a05551351 100644 --- a/src/main/java/org/springframework/data/repository/query/DefaultEvaluationContextProvider.java +++ b/src/main/java/org/springframework/data/spel/EvaluationContextProvider.java @@ -13,32 +13,34 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.repository.query; +package org.springframework.data.spel; import org.springframework.expression.EvaluationContext; import org.springframework.expression.spel.support.StandardEvaluationContext; -import org.springframework.util.ObjectUtils; /** - * Default implementation of {@link EvaluationContextProvider} that always creates a new {@link EvaluationContext}. + * Provides a way to access a centrally defined potentially shared {@link StandardEvaluationContext}. * * @author Thomas Darimont * @author Oliver Gierke * @author Christoph Strobl - * @since 1.9 + * @since 2.1 */ -public enum DefaultEvaluationContextProvider implements EvaluationContextProvider { +public interface EvaluationContextProvider { - INSTANCE; - - /* - * (non-Javadoc) - * @see org.springframework.data.repository.query.EvaluationContextProvider#getEvaluationContext(org.springframework.data.repository.query.Parameters, java.lang.Object[]) + /** + * A simple default {@link EvaluationContextProvider} returning a {@link StandardEvaluationContext} with the given + * root object. */ - @Override - public > EvaluationContext getEvaluationContext(T parameters, Object[] parameterValues) { + static EvaluationContextProvider DEFAULT = rootObject -> rootObject == null // + ? new StandardEvaluationContext() // + : new StandardEvaluationContext(rootObject); - return ObjectUtils.isEmpty(parameterValues) ? new StandardEvaluationContext() : new StandardEvaluationContext( - parameterValues); - } + /** + * Returns an {@link EvaluationContext} built using the given parameter values. + * + * @param rootObject the root object to set in the {@link EvaluationContext}. + * @return + */ + EvaluationContext getEvaluationContext(Object rootObject); } diff --git a/src/main/java/org/springframework/data/repository/query/ExtensionAwareEvaluationContextProvider.java b/src/main/java/org/springframework/data/spel/ExtensionAwareEvaluationContextProvider.java similarity index 78% rename from src/main/java/org/springframework/data/repository/query/ExtensionAwareEvaluationContextProvider.java rename to src/main/java/org/springframework/data/spel/ExtensionAwareEvaluationContextProvider.java index 3ee45429b..0f675bc87 100644 --- a/src/main/java/org/springframework/data/repository/query/ExtensionAwareEvaluationContextProvider.java +++ b/src/main/java/org/springframework/data/spel/ExtensionAwareEvaluationContextProvider.java @@ -13,33 +13,29 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.repository.query; +package org.springframework.data.spel; import lombok.NonNull; import lombok.RequiredArgsConstructor; -import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.function.Supplier; import java.util.stream.Collectors; -import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.ListableBeanFactory; -import org.springframework.context.ApplicationContext; -import org.springframework.context.ApplicationContextAware; import org.springframework.context.expression.BeanFactoryResolver; import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.core.convert.TypeDescriptor; -import org.springframework.data.repository.query.EvaluationContextExtensionInformation.ExtensionTypeInformation; -import org.springframework.data.repository.query.EvaluationContextExtensionInformation.RootObjectInformation; -import org.springframework.data.repository.query.spi.EvaluationContextExtension; -import org.springframework.data.repository.query.spi.Function; -import org.springframework.data.util.Lazy; +import org.springframework.data.spel.EvaluationContextExtensionInformation.ExtensionTypeInformation; +import org.springframework.data.spel.EvaluationContextExtensionInformation.RootObjectInformation; +import org.springframework.data.spel.spi.EvaluationContextExtension; +import org.springframework.data.spel.spi.Function; import org.springframework.data.util.Optionals; import org.springframework.expression.AccessException; import org.springframework.expression.EvaluationContext; @@ -53,7 +49,6 @@ import org.springframework.expression.spel.support.ReflectivePropertyAccessor; import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.lang.Nullable; import org.springframework.util.Assert; -import org.springframework.util.StringUtils; /** * An {@link EvaluationContextProvider} that assembles an {@link EvaluationContext} from a list of @@ -63,21 +58,30 @@ import org.springframework.util.StringUtils; * @author Oliver Gierke * @author Christoph Strobl * @author Jens Schauder - * @since 1.9 + * @since 2.1 */ -public class ExtensionAwareEvaluationContextProvider implements EvaluationContextProvider, ApplicationContextAware { +@RequiredArgsConstructor +public class ExtensionAwareEvaluationContextProvider implements EvaluationContextProvider { private final Map, EvaluationContextExtensionInformation> extensionInformationCache = new HashMap<>(); - private final Lazy> extensions; - private Optional beanFactory = Optional.empty(); + private final Supplier> extensions; + private ListableBeanFactory beanFactory; + + ExtensionAwareEvaluationContextProvider() { + this(Collections.emptyList()); + } /** - * Creates a new {@link ExtensionAwareEvaluationContextProvider}. Extensions are being looked up lazily from the - * {@link BeanFactory} configured. + * Creates a new {@link ExtensionAwareEvaluationContextProvider} with extensions looked up lazily from the given + * {@link BeanFactory}. + * + * @param beanFactory the {@link ListableBeanFactory} to lookup extensions from. */ - public ExtensionAwareEvaluationContextProvider() { - this.extensions = Lazy.of(() -> getExtensionsFrom(beanFactory)); + public ExtensionAwareEvaluationContextProvider(ListableBeanFactory beanFactory) { + + this(() -> getExtensionsFrom(beanFactory)); + this.setBeanFactory(beanFactory); } /** @@ -85,31 +89,32 @@ public class ExtensionAwareEvaluationContextProvider implements EvaluationContex * * @param extensions must not be {@literal null}. */ - public ExtensionAwareEvaluationContextProvider(List extensions) { - - Assert.notNull(extensions, "List of EvaluationContextExtensions must not be null!"); - this.extensions = Lazy.of(() -> extensions); + public ExtensionAwareEvaluationContextProvider(Collection extensions) { + this(() -> extensions); } - /* - * (non-Javadoc) - * @see org.springframework.context.ApplicationContextAware#setApplicationContext(org.springframework.context.ApplicationContext) + /** + * Sets the {@link ListableBeanFactory} to be used on the {@link EvaluationContext} to be created. + * + * @param beanFactory + * @deprecated only exists to temporarily mitigate from the old APIs. Do not use! */ - @Override - public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { - this.beanFactory = Optional.of(applicationContext); + @Deprecated + public void setBeanFactory(ListableBeanFactory beanFactory) { + this.beanFactory = beanFactory; } /* (non-Javadoc) * @see org.springframework.data.jpa.repository.support.EvaluationContextProvider#getEvaluationContext() */ @Override - public > StandardEvaluationContext getEvaluationContext(T parameters, - Object[] parameterValues) { + public StandardEvaluationContext getEvaluationContext(Object rootObject) { StandardEvaluationContext ec = new StandardEvaluationContext(); - beanFactory.ifPresent(it -> ec.setBeanResolver(new BeanFactoryResolver(it))); + if (beanFactory != null) { + ec.setBeanResolver(new BeanFactoryResolver(beanFactory)); + } ExtensionAwarePropertyAccessor accessor = new ExtensionAwarePropertyAccessor(extensions.get()); @@ -117,54 +122,21 @@ public class ExtensionAwareEvaluationContextProvider implements EvaluationContex ec.addPropertyAccessor(new ReflectivePropertyAccessor()); ec.addMethodResolver(accessor); - // Add parameters for indexed access - ec.setRootObject(parameterValues); - ec.setVariables(collectVariables(parameters, parameterValues)); + if (rootObject != null) { + ec.setRootObject(rootObject); + } return ec; } - /** - * Exposes variables for all named parameters for the given arguments. Also exposes non-bindable parameters under the - * names of their types. - * - * @param parameters must not be {@literal null}. - * @param arguments must not be {@literal null}. - * @return - */ - private > Map collectVariables(T parameters, Object[] arguments) { - - Map variables = new HashMap<>(); - - parameters.stream()// - .filter(Parameter::isSpecialParameter)// - .forEach(it -> variables.put(// - StringUtils.uncapitalize(it.getType().getSimpleName()), // - arguments[it.getIndex()])); - - parameters.stream()// - .filter(Parameter::isNamedParameter)// - .forEach(it -> variables.put(// - it.getName().orElseThrow(() -> new IllegalStateException("Should never occur!")), // - arguments[it.getIndex()])); - - return variables; - } - /** * Looks up all {@link EvaluationContextExtension} instances from the given {@link ListableBeanFactory}. * * @param beanFactory must not be {@literal null}. * @return */ - private static List getExtensionsFrom( - Optional beanFactory) { - - Collection extensions = beanFactory// - .map(it -> it.getBeansOfType(EvaluationContextExtension.class, true, false).values())// - .orElseGet(() -> Collections.emptyList()); - - return new ArrayList<>(extensions); + private static Collection getExtensionsFrom(ListableBeanFactory beanFactory) { + return beanFactory.getBeansOfType(EvaluationContextExtension.class, true, false).values(); } /** @@ -188,7 +160,8 @@ public class ExtensionAwareEvaluationContextProvider implements EvaluationContex * @param extensions * @return */ - private List toAdapters(List extensions) { + private List toAdapters( + Collection extensions) { return extensions.stream()// .sorted(AnnotationAwareOrderComparator.INSTANCE)// @@ -211,7 +184,7 @@ public class ExtensionAwareEvaluationContextProvider implements EvaluationContex * * @param extensions must not be {@literal null}. */ - public ExtensionAwarePropertyAccessor(List extensions) { + public ExtensionAwarePropertyAccessor(Collection extensions) { Assert.notNull(extensions, "Extensions must not be null!"); diff --git a/src/main/java/org/springframework/data/repository/query/Functions.java b/src/main/java/org/springframework/data/spel/Functions.java similarity index 97% rename from src/main/java/org/springframework/data/repository/query/Functions.java rename to src/main/java/org/springframework/data/spel/Functions.java index 0b14c8af4..6b809f0ca 100644 --- a/src/main/java/org/springframework/data/repository/query/Functions.java +++ b/src/main/java/org/springframework/data/spel/Functions.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.repository.query; +package org.springframework.data.spel; import lombok.AccessLevel; import lombok.AllArgsConstructor; @@ -28,7 +28,7 @@ import java.util.stream.Collectors; import java.util.stream.Stream; import org.springframework.core.convert.TypeDescriptor; -import org.springframework.data.repository.query.spi.Function; +import org.springframework.data.spel.spi.Function; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; @@ -39,7 +39,7 @@ import org.springframework.util.MultiValueMap; * * @author Jens Schauder * @author Oliver Gierke - * @since 2.0 + * @since 2.1 */ class Functions { diff --git a/src/main/java/org/springframework/data/spel/spi/EvaluationContextExtension.java b/src/main/java/org/springframework/data/spel/spi/EvaluationContextExtension.java new file mode 100644 index 000000000..b46e1ef93 --- /dev/null +++ b/src/main/java/org/springframework/data/spel/spi/EvaluationContextExtension.java @@ -0,0 +1,72 @@ +/* + * Copyright 2014-2018 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.spel.spi; + +import java.util.Collections; +import java.util.Map; + +import org.springframework.data.repository.query.ExtensionAwareQueryMethodEvaluationContextProvider; +import org.springframework.expression.EvaluationContext; +import org.springframework.lang.Nullable; + +/** + * SPI to allow adding a set of properties and function definitions accessible via the root of an + * {@link EvaluationContext} provided by a {@link ExtensionAwareQueryMethodEvaluationContextProvider}. + * + * @author Thomas Darimont + * @author Oliver Gierke + * @since 1.9 + */ +public interface EvaluationContextExtension { + + /** + * Returns the identifier of the extension. The id can be leveraged by users to fully qualify property lookups and + * thus overcome ambiguities in case multiple extensions expose properties with the same name. + * + * @return the extension id, must not be {@literal null}. + */ + String getExtensionId(); + + /** + * Returns the properties exposed by the extension. + * + * @return the properties + */ + default Map getProperties() { + return Collections.emptyMap(); + } + + /** + * Returns the functions exposed by the extension. + * + * @return the functions + */ + default Map getFunctions() { + return Collections.emptyMap(); + } + + /** + * Returns the root object to be exposed by the extension. It's strongly recommended to declare the most concrete type + * possible as return type of the implementation method. This will allow us to obtain the necessary metadata once and + * not for every evaluation. + * + * @return + */ + @Nullable + default Object getRootObject() { + return null; + } +} diff --git a/src/main/java/org/springframework/data/spel/spi/Function.java b/src/main/java/org/springframework/data/spel/spi/Function.java new file mode 100644 index 000000000..edf689fec --- /dev/null +++ b/src/main/java/org/springframework/data/spel/spi/Function.java @@ -0,0 +1,165 @@ +/* + * Copyright 2014-2018 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.spel.spi; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.List; + +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.TypeUtils; + +/** + * Value object to represent a function. Can either be backed by a static {@link Method} invocation (see + * {@link #Function(Method)}) or a method invocation on an instance (see {@link #Function(Method, Object)}. + * + * @author Thomas Darimont + * @author Oliver Gierke + * @author Jens Schauder + * @since 1.9 + */ +public class Function { + + private final Method method; + private final @Nullable Object target; + + /** + * Creates a new {@link Function} to statically invoke the given {@link Method}. + * + * @param method + */ + public Function(Method method) { + + this(method, null); + + Assert.isTrue(Modifier.isStatic(method.getModifiers()), "Method must be static!"); + } + + /** + * Creates a new {@link Function} for the given method on the given target instance. + * + * @param method must not be {@literal null}. + * @param target can be {@literal null}, if so, the method + */ + public Function(Method method, @Nullable Object target) { + + Assert.notNull(method, "Method must not be null!"); + Assert.isTrue(target != null || Modifier.isStatic(method.getModifiers()), + "Method must either be static or a non-static one with a target object!"); + + this.method = method; + this.target = target; + } + + /** + * Invokes the function with the given arguments. + * + * @param arguments must not be {@literal null}. + * @return + * @throws Exception + */ + public Object invoke(Object[] arguments) throws Exception { + return method.invoke(target, arguments); + } + + /** + * Returns the name of the function. + * + * @return + */ + public String getName() { + return method.getName(); + } + + /** + * Returns the type declaring the {@link Function}. + * + * @return + */ + public Class getDeclaringClass() { + return method.getDeclaringClass(); + } + + /** + * Returns {@literal true} if the function can be called with the given {@code argumentTypes}. + * + * @param argumentTypes + * @return + */ + public boolean supports(List argumentTypes) { + + if (method.getParameterCount() != argumentTypes.size()) { + return false; + } + + Class[] parameterTypes = method.getParameterTypes(); + + for (int i = 0; i < parameterTypes.length; i++) { + if (!TypeUtils.isAssignable(parameterTypes[i], argumentTypes.get(i).getType())) { + return false; + } + } + + return true; + } + + /** + * Returns the number of parameters required by the underlying method. + * + * @return + */ + public int getParameterCount() { + return method.getParameterCount(); + } + + /** + * Checks if the encapsulated method has exactly the argument types as those passed as an argument. + * + * @param argumentTypes a list of {@link TypeDescriptor}s to compare with the argument types of the method + * @return {@code true} if the types are equal, {@code false} otherwise. + */ + public boolean supportsExact(List argumentTypes) { + + if (method.getParameterCount() != argumentTypes.size()) { + return false; + } + + Class[] parameterTypes = method.getParameterTypes(); + + for (int i = 0; i < parameterTypes.length; i++) { + if (parameterTypes[i] != argumentTypes.get(i).getType()) { + return false; + } + } + + return true; + } + + /** + * Checks whether this {@code Function} has the same signature as another {@code Function}. + * + * @param other the {@code Function} to compare {@code this} with. + * @return {@code true} if name and argument list are the same. + */ + public boolean isSignatureEqual(Function other) { + + return getName().equals(other.getName()) // + && Arrays.equals(method.getParameterTypes(), other.method.getParameterTypes()); + } +} diff --git a/src/main/java/org/springframework/data/spel/spi/package-info.java b/src/main/java/org/springframework/data/spel/spi/package-info.java new file mode 100644 index 000000000..64a9cc8ab --- /dev/null +++ b/src/main/java/org/springframework/data/spel/spi/package-info.java @@ -0,0 +1,5 @@ +/** + * Service provider interfaces to extend the query execution mechanism. + */ +@org.springframework.lang.NonNullApi +package org.springframework.data.spel.spi; diff --git a/src/test/java/org/springframework/data/repository/cdi/CdiRepositoryBeanUnitTests.java b/src/test/java/org/springframework/data/repository/cdi/CdiRepositoryBeanUnitTests.java index 7784df06c..69c584cb4 100755 --- a/src/test/java/org/springframework/data/repository/cdi/CdiRepositoryBeanUnitTests.java +++ b/src/test/java/org/springframework/data/repository/cdi/CdiRepositoryBeanUnitTests.java @@ -16,6 +16,7 @@ package org.springframework.data.repository.cdi; import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; import java.io.Serializable; @@ -42,9 +43,8 @@ import org.springframework.data.repository.config.CustomRepositoryImplementation import org.springframework.data.repository.core.NamedQueries; import org.springframework.data.repository.core.support.PropertiesBasedNamedQueries; import org.springframework.data.repository.core.support.RepositoryFactorySupport; -import org.springframework.data.repository.query.DefaultEvaluationContextProvider; -import org.springframework.data.repository.query.EvaluationContextProvider; import org.springframework.data.repository.query.QueryLookupStrategy.Key; +import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; /** * Unit tests for {@link CdiRepositoryBean}. @@ -175,7 +175,7 @@ public class CdiRepositoryBeanUnitTests { bean.applyConfiguration(repositoryFactory); - verify(repositoryFactory).setEvaluationContextProvider(DefaultEvaluationContextProvider.INSTANCE); + verify(repositoryFactory).setEvaluationContextProvider(QueryMethodEvaluationContextProvider.DEFAULT); verify(repositoryFactory).setNamedQueries(PropertiesBasedNamedQueries.EMPTY); verify(repositoryFactory).setRepositoryBaseClass(String.class); verify(repositoryFactory).setQueryLookupStrategyKey(Key.CREATE); @@ -209,8 +209,8 @@ public class CdiRepositoryBeanUnitTests { INSTANCE; @Override - public Optional getEvaluationContextProvider() { - return Optional.of(DefaultEvaluationContextProvider.INSTANCE); + public Optional getEvaluationContextProvider() { + return Optional.of(QueryMethodEvaluationContextProvider.DEFAULT); } @Override 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 f8a23c0b3..722873ae7 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 @@ -29,7 +29,7 @@ import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.core.support.RepositoryComposition.RepositoryFragments; import org.springframework.data.repository.core.support.RepositoryFactorySupportUnitTests.MyRepositoryQuery; -import org.springframework.data.repository.query.EvaluationContextProvider; +import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.QueryLookupStrategy; import org.springframework.data.repository.query.QueryLookupStrategy.Key; import org.springframework.data.repository.query.RepositoryQuery; @@ -92,7 +92,7 @@ public class DummyRepositoryFactory extends RepositoryFactorySupport { */ @Override protected Optional getQueryLookupStrategy(Key key, - EvaluationContextProvider evaluationContextProvider) { + QueryMethodEvaluationContextProvider evaluationContextProvider) { return Optional.of(strategy); } diff --git a/src/test/java/org/springframework/data/repository/query/ExtensionAwareEvaluationContextProviderUnitTests.java b/src/test/java/org/springframework/data/repository/query/ExtensionAwareEvaluationContextProviderUnitTests.java index 209d93582..240d06b96 100755 --- a/src/test/java/org/springframework/data/repository/query/ExtensionAwareEvaluationContextProviderUnitTests.java +++ b/src/test/java/org/springframework/data/repository/query/ExtensionAwareEvaluationContextProviderUnitTests.java @@ -35,14 +35,13 @@ import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort.Direction; -import org.springframework.data.repository.query.spi.EvaluationContextExtension; -import org.springframework.data.repository.query.spi.EvaluationContextExtensionSupport; -import org.springframework.data.repository.query.spi.Function; +import org.springframework.data.spel.spi.EvaluationContextExtension; +import org.springframework.data.spel.spi.Function; import org.springframework.expression.EvaluationContext; import org.springframework.expression.spel.standard.SpelExpressionParser; /** - * Unit tests {@link ExtensionAwareEvaluationContextProvider}. + * Unit tests {@link ExtensionAwareQueryMethodEvaluationContextProvider}. * * @author Oliver Gierke * @author Thomas Darimont @@ -51,19 +50,19 @@ import org.springframework.expression.spel.standard.SpelExpressionParser; public class ExtensionAwareEvaluationContextProviderUnitTests { Method method; - EvaluationContextProvider provider; + QueryMethodEvaluationContextProvider provider; @Before public void setUp() throws Exception { this.method = SampleRepo.class.getMethod("findByFirstname", String.class); - this.provider = new ExtensionAwareEvaluationContextProvider(Collections.emptyList()); + this.provider = new ExtensionAwareQueryMethodEvaluationContextProvider(Collections.emptyList()); } @Test // DATACMNS-533 public void usesPropertyDefinedByExtension() { - this.provider = new ExtensionAwareEvaluationContextProvider( + this.provider = new ExtensionAwareQueryMethodEvaluationContextProvider( Collections.singletonList(new DummyExtension("_first", "first"))); assertThat(evaluateExpression("key")).isEqualTo("first"); @@ -76,7 +75,7 @@ public class ExtensionAwareEvaluationContextProviderUnitTests { extensions.add(new DummyExtension("_first", "first")); extensions.add(new DummyExtension("_second", "second")); - this.provider = new ExtensionAwareEvaluationContextProvider(extensions); + this.provider = new ExtensionAwareQueryMethodEvaluationContextProvider(extensions); assertThat(evaluateExpression("key")).isEqualTo("second"); } @@ -88,7 +87,7 @@ public class ExtensionAwareEvaluationContextProviderUnitTests { extensions.add(new DummyExtension("_first", "first")); extensions.add(new DummyExtension("_second", "second")); - this.provider = new ExtensionAwareEvaluationContextProvider(extensions); + this.provider = new ExtensionAwareQueryMethodEvaluationContextProvider(extensions); assertThat(evaluateExpression("_first.key")).isEqualTo("first"); } @@ -100,7 +99,7 @@ public class ExtensionAwareEvaluationContextProviderUnitTests { @Test // DATACMNS-533 public void exposesMethodDefinedByExtension() { - this.provider = new ExtensionAwareEvaluationContextProvider( + this.provider = new ExtensionAwareQueryMethodEvaluationContextProvider( Collections.singletonList(new DummyExtension("_first", "first"))); assertThat(evaluateExpression("aliasedMethod()")).isEqualTo("methodResult"); @@ -112,7 +111,7 @@ public class ExtensionAwareEvaluationContextProviderUnitTests { @Test // DATACMNS-533 public void exposesPropertiesDefinedByExtension() { - this.provider = new ExtensionAwareEvaluationContextProvider( + this.provider = new ExtensionAwareQueryMethodEvaluationContextProvider( Collections.singletonList(new DummyExtension("_first", "first"))); assertThat(evaluateExpression("DUMMY_KEY")).isEqualTo("dummy"); @@ -151,7 +150,7 @@ public class ExtensionAwareEvaluationContextProviderUnitTests { @Test // DATACMNS-533 public void shouldBeAbleToAccessCustomRootObjectPropertiesAndFunctions() { - this.provider = new ExtensionAwareEvaluationContextProvider(Collections.singletonList( // + this.provider = new ExtensionAwareQueryMethodEvaluationContextProvider(Collections.singletonList( // new DummyExtension("_first", "first") { @Override public CustomExtensionRootObject1 getRootObject() { @@ -173,7 +172,7 @@ public class ExtensionAwareEvaluationContextProviderUnitTests { @Test // DATACMNS-533 public void shouldBeAbleToAccessCustomRootObjectPropertiesAndFunctionsInMultipleExtensions() { - this.provider = new ExtensionAwareEvaluationContextProvider(Arrays.asList( // + this.provider = new ExtensionAwareQueryMethodEvaluationContextProvider(Arrays.asList( // new DummyExtension("_first", "first") { @Override public CustomExtensionRootObject1 getRootObject() { @@ -201,7 +200,7 @@ public class ExtensionAwareEvaluationContextProviderUnitTests { final AtomicInteger counter = new AtomicInteger(); - this.provider = new ExtensionAwareEvaluationContextProvider(Collections.singletonList( // + this.provider = new ExtensionAwareQueryMethodEvaluationContextProvider(Collections.singletonList( // new DummyExtension("_first", "first") { @Override @@ -272,9 +271,9 @@ public class ExtensionAwareEvaluationContextProviderUnitTests { .withMessageContaining("(java.lang.Integer)"); } - private static ExtensionAwareEvaluationContextProvider createContextProviderWithOverloads() { + private static ExtensionAwareQueryMethodEvaluationContextProvider createContextProviderWithOverloads() { - return new ExtensionAwareEvaluationContextProvider(Collections.singletonList( // + return new ExtensionAwareQueryMethodEvaluationContextProvider(Collections.singletonList( // new DummyExtension("_first", "first") { @Override public Object getRootObject() { @@ -284,7 +283,7 @@ public class ExtensionAwareEvaluationContextProviderUnitTests { } @RequiredArgsConstructor - public static class DummyExtension extends EvaluationContextExtensionSupport { + public static class DummyExtension implements org.springframework.data.spel.spi.EvaluationContextExtension { public static String DUMMY_KEY = "dummy"; @@ -306,7 +305,7 @@ public class ExtensionAwareEvaluationContextProviderUnitTests { @Override public Map getProperties() { - Map properties = new HashMap<>(super.getProperties()); + Map properties = new HashMap<>(); properties.put("key", value); @@ -320,7 +319,7 @@ public class ExtensionAwareEvaluationContextProviderUnitTests { @Override public Map getFunctions() { - Map functions = new HashMap<>(super.getFunctions()); + Map functions = new HashMap<>(); try { functions.put("aliasedMethod", new Function(getClass().getMethod("extensionMethod"))); diff --git a/src/test/java/org/springframework/data/repository/query/EvaluationContextExtensionInformationUnitTests.java b/src/test/java/org/springframework/data/spel/EvaluationContextExtensionInformationUnitTests.java similarity index 91% rename from src/test/java/org/springframework/data/repository/query/EvaluationContextExtensionInformationUnitTests.java rename to src/test/java/org/springframework/data/spel/EvaluationContextExtensionInformationUnitTests.java index 81cc8ce90..0bbc2c268 100644 --- a/src/test/java/org/springframework/data/repository/query/EvaluationContextExtensionInformationUnitTests.java +++ b/src/test/java/org/springframework/data/spel/EvaluationContextExtensionInformationUnitTests.java @@ -13,12 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.repository.query; +package org.springframework.data.spel; import java.util.Optional; import org.junit.Test; -import org.springframework.data.repository.query.spi.EvaluationContextExtension; +import org.springframework.data.spel.spi.EvaluationContextExtension; /** * Unit tests for {@link EvaluationContextExtensionInformation}.