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); + } +}