From d303f6ac375408c18850313f38c8786d2db6e505 Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Sat, 5 Jul 2014 22:41:49 +0200 Subject: [PATCH] DATACMNS-533 - Infrastructure support for SpEL expressions in manually defined queries. Extracted SPI build for DATAJPA-564 into Spring Data Commons. Simplified the configuration setup code and removed the ability to define a custom EvaluationContextProvider for now. We directly declare a bean definition which automatically picks up EvaluationContextExtensions. RepositoryFactory(Bean)Support is now aware of the EvaluationContextProvider to hand it to the template method that creates the QueryLookupStrategy. Deprecated the previously existing template method in favor of an extended one. Removed variable declarations from the extension interface for now as usually properties serve the same purposes as variables which makes the latter superfluous for now. Applied some JavaDoc and API signature polish. Added unit tests for ExtensionAwareEvaluationContextProvider. Related pull request: spring-projects/spring-data-jpa#101 Related ticket: DATAJPA-564 --- .../RepositoryBeanDefinitionBuilder.java | 8 + .../support/RepositoryFactoryBeanSupport.java | 15 + .../support/RepositoryFactorySupport.java | 31 +- .../DefaultEvaluationContextProvider.java | 41 +++ .../query/EvaluationContextProvider.java | 39 +++ ...tensionAwareEvaluationContextProvider.java | 309 ++++++++++++++++++ .../query/spi/EvaluationContextExtension.java | 55 ++++ .../EvaluationContextExtensionSupport.java | 48 +++ .../repository/query/spi/package-info.java | 5 + ...bleEvaluationContextProviderUnitTests.java | 177 ++++++++++ 10 files changed, 727 insertions(+), 1 deletion(-) create mode 100644 src/main/java/org/springframework/data/repository/query/DefaultEvaluationContextProvider.java create mode 100644 src/main/java/org/springframework/data/repository/query/EvaluationContextProvider.java create mode 100644 src/main/java/org/springframework/data/repository/query/ExtensionAwareEvaluationContextProvider.java create mode 100644 src/main/java/org/springframework/data/repository/query/spi/EvaluationContextExtension.java create mode 100644 src/main/java/org/springframework/data/repository/query/spi/EvaluationContextExtensionSupport.java create mode 100644 src/main/java/org/springframework/data/repository/query/spi/package-info.java create mode 100644 src/test/java/org/springframework/data/repository/query/ExtensibleEvaluationContextProviderUnitTests.java 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 55f5f05c1..cfe587ef2 100644 --- a/src/main/java/org/springframework/data/repository/config/RepositoryBeanDefinitionBuilder.java +++ b/src/main/java/org/springframework/data/repository/config/RepositoryBeanDefinitionBuilder.java @@ -27,12 +27,14 @@ import org.springframework.beans.factory.config.BeanDefinition; 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.context.annotation.ClassPathScanningCandidateComponentProvider; import org.springframework.core.env.Environment; import org.springframework.core.io.ResourceLoader; import org.springframework.core.type.classreading.CachingMetadataReaderFactory; import org.springframework.core.type.classreading.MetadataReaderFactory; import org.springframework.core.type.filter.RegexPatternTypeFilter; +import org.springframework.data.repository.query.ExtensionAwareEvaluationContextProvider; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -116,6 +118,12 @@ class RepositoryBeanDefinitionBuilder { builder.addDependsOn(customImplementationBeanName); } + 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 58e54c6ba..4879c5430 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 @@ -29,9 +29,11 @@ import org.springframework.data.repository.core.EntityInformation; 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.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.DefaultEvaluationContextProvider; import org.springframework.util.Assert; /** @@ -54,6 +56,7 @@ public abstract class RepositoryFactoryBeanSupport, private MappingContext mappingContext; private ClassLoader classLoader; private boolean lazyInit = false; + private EvaluationContextProvider evaluationContextProvider = DefaultEvaluationContextProvider.INSTANCE; private T repository; @@ -108,6 +111,17 @@ public abstract class RepositoryFactoryBeanSupport, this.mappingContext = mappingContext; } + /** + * Sets the {@link EvaluationContextProvider} to be used to evaluate SpEL expressions in manually defined queries. + * + * @param evaluationContextProvider can be {@literal null}, defaults to + * {@link DefaultEvaluationContextProvider#INSTANCE}. + */ + public void setEvaluationContextProvider(EvaluationContextProvider evaluationContextProvider) { + this.evaluationContextProvider = evaluationContextProvider == null ? DefaultEvaluationContextProvider.INSTANCE + : evaluationContextProvider; + } + /** * Configures whether to initialize the repository proxy lazily. This defaults to {@literal false}. * @@ -203,6 +217,7 @@ public abstract class RepositoryFactoryBeanSupport, this.factory.setQueryLookupStrategyKey(queryLookupStrategyKey); this.factory.setNamedQueries(namedQueries); this.factory.setBeanClassLoader(classLoader); + this.factory.setEvaluationContextProvider(evaluationContextProvider); this.repositoryMetadata = this.factory.getRepositoryMetadata(repositoryInterface); diff --git a/src/main/java/org/springframework/data/repository/core/support/RepositoryFactorySupport.java b/src/main/java/org/springframework/data/repository/core/support/RepositoryFactorySupport.java index 02cd13b43..a164e053e 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 @@ -42,6 +42,8 @@ import org.springframework.data.repository.core.EntityInformation; 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.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; @@ -72,6 +74,7 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware { private List> queryPostProcessors = new ArrayList>(); private NamedQueries namedQueries = PropertiesBasedNamedQueries.EMPTY; private ClassLoader classLoader = org.springframework.util.ClassUtils.getDefaultClassLoader(); + private EvaluationContextProvider evaluationContextProvider = DefaultEvaluationContextProvider.INSTANCE; private QueryCollectingQueryCreationListener collectingListener = new QueryCollectingQueryCreationListener(); @@ -106,6 +109,17 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware { this.classLoader = classLoader == null ? org.springframework.util.ClassUtils.getDefaultClassLoader() : classLoader; } + /** + * Sets the {@link EvaluationContextProvider} to be used to evaluate SpEL expressions in manually defined queries. + * + * @param evaluationContextProvider can be {@literal null}, defaults to + * {@link DefaultEvaluationContextProvider#INSTANCE}. + */ + public void setEvaluationContextProvider(EvaluationContextProvider evaluationContextProvider) { + this.evaluationContextProvider = evaluationContextProvider == null ? DefaultEvaluationContextProvider.INSTANCE + : evaluationContextProvider; + } + /** * Adds a {@link QueryCreationListener} to the factory to plug in functionality triggered right after creation of * {@link RepositoryQuery} instances. @@ -248,6 +262,7 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware { /** * Returns the {@link QueryLookupStrategy} for the given {@link Key}. * + * @deprecated favor {@link #getQueryLookupStrategy(Key, EvaluationContextProvider)} * @param key can be {@literal null} * @return the {@link QueryLookupStrategy} to use or {@literal null} if no queries should be looked up. */ @@ -255,6 +270,18 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware { return null; } + /** + * Returns the {@link QueryLookupStrategy} for the given {@link Key} and {@link EvaluationContextProvider}. + * + * @param key can be {@literal null}. + * @param evaluationContextProvider will never be {@literal null}. + * @return the {@link QueryLookupStrategy} to use or {@literal null} if no queries should be looked up. + * @since 1.9 + */ + protected QueryLookupStrategy getQueryLookupStrategy(Key key, EvaluationContextProvider evaluationContextProvider) { + return null; + } + /** * Validates the given repository interface as well as the given custom implementation. * @@ -311,7 +338,9 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware { this.customImplementation = customImplementation; this.target = target; - QueryLookupStrategy lookupStrategy = getQueryLookupStrategy(queryLookupStrategyKey); + QueryLookupStrategy lookupStrategy = getQueryLookupStrategy(queryLookupStrategyKey, + RepositoryFactorySupport.this.evaluationContextProvider); + lookupStrategy = lookupStrategy == null ? getQueryLookupStrategy(queryLookupStrategyKey) : lookupStrategy; Iterable queryMethods = repositoryInformation.getQueryMethods(); if (lookupStrategy == null) { diff --git a/src/main/java/org/springframework/data/repository/query/DefaultEvaluationContextProvider.java b/src/main/java/org/springframework/data/repository/query/DefaultEvaluationContextProvider.java new file mode 100644 index 000000000..670a10bc0 --- /dev/null +++ b/src/main/java/org/springframework/data/repository/query/DefaultEvaluationContextProvider.java @@ -0,0 +1,41 @@ +/* + * Copyright 2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.repository.query; + +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.spel.support.StandardEvaluationContext; + +/** + * Default implementation of {@link EvaluationContextProvider} that always creates a new {@link EvaluationContext}. + * + * @author Thomas Darimont + * @author Oliver Gierke + * @since 1.9 + */ +public enum DefaultEvaluationContextProvider implements EvaluationContextProvider { + + INSTANCE; + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.EvaluationContextProvider#getEvaluationContext(org.springframework.data.repository.query.Parameters, java.lang.Object[]) + */ + @Override + public > EvaluationContext getEvaluationContext(T parameters, + Object[] parameterValues) { + return new StandardEvaluationContext(); + } +} diff --git a/src/main/java/org/springframework/data/repository/query/EvaluationContextProvider.java b/src/main/java/org/springframework/data/repository/query/EvaluationContextProvider.java new file mode 100644 index 000000000..d831e1d86 --- /dev/null +++ b/src/main/java/org/springframework/data/repository/query/EvaluationContextProvider.java @@ -0,0 +1,39 @@ +/* + * Copyright 2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.repository.query; + +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.spel.support.StandardEvaluationContext; + +/** + * Provides a way to access a centrally defined potentially shared {@link StandardEvaluationContext}. + * + * @author Thomas Darimont + * @author Oliver Gierke + * @since 1.9 + */ +public interface EvaluationContextProvider { + + /** + * Returns an {@link EvaluationContext} built using the given {@link Parameters} and parameter values. + * + * @param parameters the {@link Parameters} instance obtained from the query method the context is built for. + * @param parameterValues the values for the parameters. + * @return + */ + > EvaluationContext getEvaluationContext(T parameters, + Object[] parameterValues); +} diff --git a/src/main/java/org/springframework/data/repository/query/ExtensionAwareEvaluationContextProvider.java b/src/main/java/org/springframework/data/repository/query/ExtensionAwareEvaluationContextProvider.java new file mode 100644 index 000000000..b2f421d66 --- /dev/null +++ b/src/main/java/org/springframework/data/repository/query/ExtensionAwareEvaluationContextProvider.java @@ -0,0 +1,309 @@ +/* + * Copyright 2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.repository.query; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +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.spi.EvaluationContextExtension; +import org.springframework.expression.AccessException; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.MethodExecutor; +import org.springframework.expression.MethodResolver; +import org.springframework.expression.PropertyAccessor; +import org.springframework.expression.TypedValue; +import org.springframework.expression.spel.SpelEvaluationException; +import org.springframework.expression.spel.SpelMessage; +import org.springframework.expression.spel.support.ReflectivePropertyAccessor; +import org.springframework.expression.spel.support.StandardEvaluationContext; +import org.springframework.util.Assert; +import org.springframework.util.TypeUtils; + +/** + * An {@link EvaluationContextProvider} that assembles an {@link EvaluationContext} from a list of + * {@link EvaluationContextExtension} instances. + * + * @author Thomas Darimont + * @author Oliver Gierke + * @since 1.9 + */ +public class ExtensionAwareEvaluationContextProvider implements EvaluationContextProvider, ApplicationContextAware { + + private List extensions; + private ListableBeanFactory beanFactory; + + /** + * Creates a new {@link ExtensionAwareEvaluationContextProvider}. Extensions are being looked up lazily from the + * {@link BeanFactory} configured. + */ + public ExtensionAwareEvaluationContextProvider() {} + + /** + * Creates a new {@link ExtensionAwareEvaluationContextProvider} for the given {@link EvaluationContextExtension}s. + * + * @param extensions must not be {@literal null}. + */ + public ExtensionAwareEvaluationContextProvider(List extensions) { + + Assert.notNull(extensions, "List of EvaluationContextExtensions must not be null!"); + + List extensionsToSet = new ArrayList(extensions); + Collections.sort(extensionsToSet, AnnotationAwareOrderComparator.INSTANCE); + + this.extensions = Collections.unmodifiableList(extensionsToSet); + } + + /* + * (non-Javadoc) + * @see org.springframework.context.ApplicationContextAware#setApplicationContext(org.springframework.context.ApplicationContext) + */ + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.beanFactory = applicationContext; + } + + /* (non-Javadoc) + * @see org.springframework.data.jpa.repository.support.EvaluationContextProvider#getEvaluationContext() + */ + @Override + public > StandardEvaluationContext getEvaluationContext(T parameters, + Object[] parameterValues) { + + StandardEvaluationContext ec = new StandardEvaluationContext(); + + if (beanFactory != null) { + ec.setBeanResolver(new BeanFactoryResolver(beanFactory)); + } + + ExtensionAwarePropertyAccessor accessor = new ExtensionAwarePropertyAccessor(getExtensions()); + + ec.addPropertyAccessor(accessor); + ec.addPropertyAccessor(new ReflectivePropertyAccessor()); + ec.addMethodResolver(accessor); + + // Add parameters for indexed access + ec.setRootObject(parameterValues); + + // Add parameters as variables if named + for (Parameter param : parameters) { + if (param.isNamedParameter()) { + ec.setVariable(param.getName(), parameterValues[param.getIndex()]); + } + } + + return ec; + } + + /** + * Returns the {@link EvaluationContextExtension} to be used. Either from the current configuration or the configured + * {@link BeanFactory}. + * + * @return + */ + private List getExtensions() { + + if (this.extensions != null) { + return this.extensions; + } + + if (beanFactory == null) { + this.extensions = Collections.emptyList(); + return this.extensions; + } + + List extensions = new ArrayList(beanFactory.getBeansOfType( + EvaluationContextExtension.class, true, false).values()); + Collections.sort(extensions, AnnotationAwareOrderComparator.INSTANCE); + this.extensions = extensions; + + return extensions; + } + + /** + * @author Thomas Darimont + * @author Oliver Gierke + * @see 1.9 + */ + private static class ExtensionAwarePropertyAccessor implements PropertyAccessor, MethodResolver { + + private final Map extensionMap; + private final List extensions; + private final Map functions; + + /** + * Creates a new {@link ExtensionAwarePropertyAccessor} for the given {@link EvaluationContextExtension}s. + * + * @param extensions must not be {@literal null}. + */ + public ExtensionAwarePropertyAccessor(List extensions) { + + Assert.notNull(extensions, "Extensions must not be null!"); + + Map functions = new HashMap(); + + for (EvaluationContextExtension ext : extensions) { + + if (ext.getExtensionId() != null) { + functions.put(ext.getExtensionId(), ext.getFunctions()); + } + + functions.putAll(ext.getFunctions()); + } + + this.functions = functions; + + this.extensions = new ArrayList(extensions); + Collections.reverse(this.extensions); + + this.extensionMap = new HashMap(extensions.size()); + + for (EvaluationContextExtension extension : extensions) { + this.extensionMap.put(extension.getExtensionId(), extension); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.ExtensionAwareEvaluationContextProvider.ReadOnlyPropertyAccessor#canRead(org.springframework.expression.EvaluationContext, java.lang.Object, java.lang.String) + */ + @Override + public boolean canRead(EvaluationContext context, Object target, String name) throws AccessException { + + if (target instanceof EvaluationContextExtension) { + return true; + } + + if (extensionMap.containsKey(name)) { + return true; + } + + for (EvaluationContextExtension extension : extensions) { + if (extension.getProperties().containsKey(name)) { + return true; + } + } + + return false; + } + + /* + * (non-Javadoc) + * @see org.springframework.expression.PropertyAccessor#read(org.springframework.expression.EvaluationContext, java.lang.Object, java.lang.String) + */ + @Override + public TypedValue read(EvaluationContext context, Object target, String name) throws AccessException { + + if (target instanceof EvaluationContextExtension) { + return new TypedValue(((EvaluationContextExtension) target).getProperties().get(name)); + } + + if (extensionMap.containsKey(name)) { + return new TypedValue(extensionMap.get(name)); + } + + for (EvaluationContextExtension extension : extensions) { + + Map properties = extension.getProperties(); + + if (properties.containsKey(name)) { + return new TypedValue(properties.get(name)); + } + } + + return null; + } + + /* + * (non-Javadoc) + * @see org.springframework.expression.MethodResolver#resolve(org.springframework.expression.EvaluationContext, java.lang.Object, java.lang.String, java.util.List) + */ + @Override + public MethodExecutor resolve(EvaluationContext context, Object targetObject, String name, + List argumentTypes) throws AccessException { + + final Method function = targetObject instanceof Map && ((Map) targetObject).containsKey(name) ? (Method) ((Map) targetObject) + .get(name) : (Method) functions.get(name); + + Class[] parameterTypes = function.getParameterTypes(); + if (parameterTypes.length != argumentTypes.size()) { + return null; + } + + for (int i = 0; i < parameterTypes.length; i++) { + if (!TypeUtils.isAssignable(parameterTypes[i], argumentTypes.get(i).getType())) { + return null; + } + } + + return new MethodExecutor() { + + /* + * (non-Javadoc) + * @see org.springframework.expression.MethodExecutor#execute(org.springframework.expression.EvaluationContext, java.lang.Object, java.lang.Object[]) + */ + @Override + public TypedValue execute(EvaluationContext context, Object target, Object... arguments) throws AccessException { + + try { + return new TypedValue(function.invoke(null, arguments)); + } catch (Exception e) { + throw new SpelEvaluationException(e, SpelMessage.FUNCTION_REFERENCE_CANNOT_BE_INVOKED, function.getName(), + function.getDeclaringClass()); + } + } + }; + } + + /* + * (non-Javadoc) + * @see org.springframework.expression.PropertyAccessor#canWrite(org.springframework.expression.EvaluationContext, java.lang.Object, java.lang.String) + */ + @Override + public boolean canWrite(EvaluationContext context, Object target, String name) throws AccessException { + return false; + } + + /* + * (non-Javadoc) + * @see org.springframework.expression.PropertyAccessor#write(org.springframework.expression.EvaluationContext, java.lang.Object, java.lang.String, java.lang.Object) + */ + @Override + public void write(EvaluationContext context, Object target, String name, Object newValue) throws AccessException { + // noop + } + + /* + * (non-Javadoc) + * @see org.springframework.expression.PropertyAccessor#getSpecificTargetClasses() + */ + @Override + public Class[] getSpecificTargetClasses() { + return null; + } + } +} 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 new file mode 100644 index 000000000..5817f23ec --- /dev/null +++ b/src/main/java/org/springframework/data/repository/query/spi/EvaluationContextExtension.java @@ -0,0 +1,55 @@ +/* + * Copyright 2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.repository.query.spi; + +import java.lang.reflect.Method; +import java.util.Map; + +import org.springframework.data.repository.query.ExtensionAwareEvaluationContextProvider; +import org.springframework.expression.EvaluationContext; + +/** + * SPI to allow adding a set of properties and function definitions accessible via the root of an + * {@link EvaluationContext} provided by a {@link ExtensionAwareEvaluationContextProvider}. + * + * @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 + */ + Map getProperties(); + + /** + * Returns the functions exposed by the extension. + * + * @return the functions + */ + Map getFunctions(); +} 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 new file mode 100644 index 000000000..c4032d8f1 --- /dev/null +++ b/src/main/java/org/springframework/data/repository/query/spi/EvaluationContextExtensionSupport.java @@ -0,0 +1,48 @@ +/* + * Copyright 2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.repository.query.spi; + +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.Map; + +/** + * A base class for {@link EvaluationContextExtension}s. + * + * @author Thomas Darimont + * @author Oliver Gierke + * @see 1.9 + */ +public abstract class EvaluationContextExtensionSupport implements EvaluationContextExtension { + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.EvaluationContextExtension#getProperties() + */ + @Override + public Map getProperties() { + return Collections.emptyMap(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.EvaluationContextExtension#getFunctions() + */ + @Override + public Map getFunctions() { + return Collections.emptyMap(); + } +} diff --git a/src/main/java/org/springframework/data/repository/query/spi/package-info.java b/src/main/java/org/springframework/data/repository/query/spi/package-info.java new file mode 100644 index 000000000..78d4d841d --- /dev/null +++ b/src/main/java/org/springframework/data/repository/query/spi/package-info.java @@ -0,0 +1,5 @@ +/** + * Service provider interfaces to extend the query execution mechanism. + */ +package org.springframework.data.repository.query.spi; + diff --git a/src/test/java/org/springframework/data/repository/query/ExtensibleEvaluationContextProviderUnitTests.java b/src/test/java/org/springframework/data/repository/query/ExtensibleEvaluationContextProviderUnitTests.java new file mode 100644 index 000000000..667ad9c34 --- /dev/null +++ b/src/test/java/org/springframework/data/repository/query/ExtensibleEvaluationContextProviderUnitTests.java @@ -0,0 +1,177 @@ +/* + * Copyright 2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.repository.query; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.data.repository.query.spi.EvaluationContextExtension; +import org.springframework.data.repository.query.spi.EvaluationContextExtensionSupport; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.spel.standard.SpelExpressionParser; + +/** + * Unit tests {@link ExtensionAwareEvaluationContextProvider}. + * + * @author Oliver Gierke + */ +public class ExtensibleEvaluationContextProviderUnitTests> { + + DefaultParameters parameters; + + @Before + public void setUp() throws Exception { + + Method method = SampleRepo.class.getMethod("findByFirstname", String.class); + this.parameters = new DefaultParameters(method); + } + + /** + * @see DATACMNS-533 + */ + @Test + public void usesPropertyDefinedByExtension() { + + List extensions = new ArrayList(); + extensions.add(new DummyExtension("_first", "first")); + + ExtensionAwareEvaluationContextProvider provider = new ExtensionAwareEvaluationContextProvider(extensions); + assertThat(evaluateExpression("key", provider), is((Object) "first")); + } + + /** + * @see DATACMNS-533 + */ + @Test + public void secondExtensionOverridesFirstOne() { + + List extensions = new ArrayList(); + extensions.add(new DummyExtension("_first", "first")); + extensions.add(new DummyExtension("_second", "second")); + + ExtensionAwareEvaluationContextProvider provider = new ExtensionAwareEvaluationContextProvider(extensions); + assertThat(evaluateExpression("key", provider), is((Object) "second")); + } + + /** + * @see DATACMNS-533 + */ + @Test + public void allowsDirectAccessToExtensionViaKey() { + + List extensions = new ArrayList(); + extensions.add(new DummyExtension("_first", "first")); + extensions.add(new DummyExtension("_second", "second")); + + EvaluationContextProvider provider = new ExtensionAwareEvaluationContextProvider(extensions); + assertThat(evaluateExpression("_first.key", provider), is((Object) "first")); + } + + /** + * @see DATACMNS-533 + */ + @Test + public void exposesParametersAsVariables() { + + ExtensionAwareEvaluationContextProvider provider = new ExtensionAwareEvaluationContextProvider( + Collections. emptyList()); + + assertThat(evaluateExpression("#firstname", provider), is((Object) "parameterValue")); + } + + /** + * @see DATACMNS-533 + */ + @Test + public void exposesMethodDefinedByExtension() { + + List extensions = new ArrayList(); + extensions.add(new DummyExtension("_first", "first")); + + EvaluationContextProvider provider = new ExtensionAwareEvaluationContextProvider(extensions); + assertThat(evaluateExpression("method()", provider), is((Object) "methodResult")); + } + + static class DummyExtension extends EvaluationContextExtensionSupport { + + private final String key; + private final String value; + + /** + * @param value + */ + public DummyExtension(String key, String value) { + + this.key = key; + this.value = value; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.spi.EvaluationContextExtension#getExtensionId() + */ + @Override + public String getExtensionId() { + return key; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.EvaluationContextExtensionAdapter#getProperties() + */ + @Override + public Map getProperties() { + return Collections. singletonMap("key", value); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.spi.EvaluationContextExtensionSupport#getFunctions() + */ + @Override + public Map getFunctions() { + + try { + return Collections.singletonMap("method", getClass().getMethod("extensionMethod")); + } catch (Exception o_O) { + throw new RuntimeException(o_O); + } + } + + public static String extensionMethod() { + return "methodResult"; + } + } + + private Object evaluateExpression(String expression, EvaluationContextProvider provider) { + + EvaluationContext evaluationContext = provider.getEvaluationContext(parameters, new Object[] { "parameterValue" }); + return new SpelExpressionParser().parseExpression(expression).getValue(evaluationContext); + } + + interface SampleRepo { + + List findByFirstname(@Param("firstname") String firstname); + } +}