From a6215fbe0f5c9a254cddacb12763737f2c286ad5 Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Wed, 14 Feb 2018 19:05:59 +0100 Subject: [PATCH] DATACMNS-1260 - Extract EvaluationContextProvider and corresponding SPIs into dedicated package. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We now have a refined replica of the EvaluationContextProvider API and SPIs in the org.springframework.data.spel package. It has seen a bit of a Java 8 overhaul by removing the SPI support class in favor of turning most methods in EvaluationContextExtension into default ones. The already existing API has been renamed to QueryMethodEvaluationContextProvider to indicate it's working with additional semantics specific to query methods (i.e. the Parameters metadata). The internals have been refactored to use the new API but still detect implementations of the old EvaluationContextExtension interface. The implementations get wrapped into an adapting proxy to satisfy the new API so that the actual inspection and usage of the extension is now already done using the new APIs. The repository configuration has slightly change so that the creation of the EvaluationContextProvider is now taking place within RepositoryFactoryBeanSupport's implementation of BeanFactoryAware. AbstractMappingContext is now ApplicationContextAware and holds an ExtensionAwareEvaluationContextProvider using the configured ApplicationContext. That EvaluationContextProvider is forwarded to all MutablePersistentEntity instances. BasicPersistentEntity now exposes getEvaluationContext(…) to subclasses to easily create an EvaluationContext using the extension aware infrastructure. Removed DefaultEvaluationContextProvider in favor of a simple constant in QueryMethodEvaluationContextProvider. Related tickets: DATACMNS-1258, DATACMNS-1108. --- .../context/AbstractMappingContext.java | 23 +- .../mapping/model/BasicPersistentEntity.java | 20 ++ .../model/MutablePersistentEntity.java | 8 + .../repository/cdi/CdiRepositoryBean.java | 12 +- .../cdi/CdiRepositoryConfiguration.java | 10 +- .../repository/cdi/CdiRepositoryContext.java | 11 +- .../RepositoryBeanDefinitionBuilder.java | 8 - .../support/RepositoryFactoryBeanSupport.java | 27 +- .../support/RepositoryFactorySupport.java | 20 +- ...eQueryMethodEvaluationContextProvider.java | 236 ++++++++++++++++++ ...QueryMethodEvaluationContextProvider.java} | 7 +- .../query/spi/EvaluationContextExtension.java | 19 +- .../EvaluationContextExtensionSupport.java | 2 + .../data/repository/query/spi/Function.java | 11 + ...EvaluationContextExtensionInformation.java | 12 +- .../EvaluationContextProvider.java} | 32 +-- ...tensionAwareEvaluationContextProvider.java | 117 ++++----- .../{repository/query => spel}/Functions.java | 6 +- .../spel/spi/EvaluationContextExtension.java | 72 ++++++ .../data/spel/spi/Function.java | 165 ++++++++++++ .../data/spel/spi/package-info.java | 5 + .../cdi/CdiRepositoryBeanUnitTests.java | 10 +- .../core/support/DummyRepositoryFactory.java | 4 +- ...areEvaluationContextProviderUnitTests.java | 37 ++- ...nContextExtensionInformationUnitTests.java | 4 +- 25 files changed, 707 insertions(+), 171 deletions(-) create mode 100644 src/main/java/org/springframework/data/repository/query/ExtensionAwareQueryMethodEvaluationContextProvider.java rename src/main/java/org/springframework/data/repository/query/{EvaluationContextProvider.java => QueryMethodEvaluationContextProvider.java} (86%) rename src/main/java/org/springframework/data/{repository/query => spel}/EvaluationContextExtensionInformation.java (95%) rename src/main/java/org/springframework/data/{repository/query/DefaultEvaluationContextProvider.java => spel/EvaluationContextProvider.java} (52%) rename src/main/java/org/springframework/data/{repository/query => spel}/ExtensionAwareEvaluationContextProvider.java (78%) rename src/main/java/org/springframework/data/{repository/query => spel}/Functions.java (97%) create mode 100644 src/main/java/org/springframework/data/spel/spi/EvaluationContextExtension.java create mode 100644 src/main/java/org/springframework/data/spel/spi/Function.java create mode 100644 src/main/java/org/springframework/data/spel/spi/package-info.java rename src/test/java/org/springframework/data/{repository/query => spel}/EvaluationContextExtensionInformationUnitTests.java (91%) 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}.