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 5817f23ec..0679c2919 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,7 +15,6 @@ */ package org.springframework.data.repository.query.spi; -import java.lang.reflect.Method; import java.util.Map; import org.springframework.data.repository.query.ExtensionAwareEvaluationContextProvider; @@ -51,5 +50,14 @@ public interface EvaluationContextExtension { * * @return the functions */ - Map getFunctions(); + Map getFunctions(); + + /** + * 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 + */ + Object getRootObject(); } 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 new file mode 100644 index 000000000..54b9d1d1e --- /dev/null +++ b/src/main/java/org/springframework/data/repository/query/spi/Function.java @@ -0,0 +1,118 @@ +/* + * 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.lang.reflect.Modifier; +import java.util.List; + +import org.springframework.core.convert.TypeDescriptor; +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 + * @since 1.9 + */ +public class Function { + + private final Method method; + private final 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, 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) { + + Class[] parameterTypes = method.getParameterTypes(); + + if (parameterTypes.length != argumentTypes.size()) { + return false; + } + + for (int i = 0; i < parameterTypes.length; i++) { + if (!TypeUtils.isAssignable(parameterTypes[i], argumentTypes.get(i).getType())) { + return false; + } + } + + return true; + } +} 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 cef4bd418..ce3eeca45 100644 --- a/src/test/java/org/springframework/data/repository/query/ExtensionAwareEvaluationContextProviderUnitTests.java +++ b/src/test/java/org/springframework/data/repository/query/ExtensionAwareEvaluationContextProviderUnitTests.java @@ -20,10 +20,12 @@ import static org.junit.Assert.*; import java.lang.reflect.Method; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; import org.junit.Before; import org.junit.Test; @@ -33,6 +35,7 @@ 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.expression.EvaluationContext; import org.springframework.expression.spel.standard.SpelExpressionParser; @@ -169,6 +172,88 @@ public class ExtensionAwareEvaluationContextProviderUnitTests { assertThat(evaluateExpression("#sort?.toString()", new Object[] { "test", null }), is(nullValue())); } + /** + * @see DATACMNS-533 + */ + @Test + public void shouldBeAbleToAccessCustomRootObjectPropertiesAndFunctions() { + + this.provider = new ExtensionAwareEvaluationContextProvider(Collections.singletonList( // + new DummyExtension("_first", "first") { + @Override + public CustomExtensionRootObject1 getRootObject() { + return new CustomExtensionRootObject1(); + } + })); + + assertThat(evaluateExpression("rootObjectInstanceField1"), is((Object) "rootObjectInstanceF1")); + assertThat(evaluateExpression("rootObjectInstanceMethod1()"), is((Object) true)); + assertThat(evaluateExpression("getStringProperty()"), is((Object) "stringProperty")); + assertThat(evaluateExpression("stringProperty"), is((Object) "stringProperty")); + + assertThat(evaluateExpression("_first.rootObjectInstanceField1"), is((Object) "rootObjectInstanceF1")); + assertThat(evaluateExpression("_first.rootObjectInstanceMethod1()"), is((Object) true)); + assertThat(evaluateExpression("_first.getStringProperty()"), is((Object) "stringProperty")); + assertThat(evaluateExpression("_first.stringProperty"), is((Object) "stringProperty")); + } + + /** + * @see DATACMNS-533 + */ + @Test + public void shouldBeAbleToAccessCustomRootObjectPropertiesAndFunctionsInMultipleExtensions() { + + this.provider = new ExtensionAwareEvaluationContextProvider(Arrays.asList( // + new DummyExtension("_first", "first") { + @Override + public CustomExtensionRootObject1 getRootObject() { + return new CustomExtensionRootObject1(); + } + }, // + new DummyExtension("_second", "second") { + @Override + public CustomExtensionRootObject2 getRootObject() { + return new CustomExtensionRootObject2(); + } + })); + + assertThat(evaluateExpression("rootObjectInstanceField1"), is((Object) "rootObjectInstanceF1")); + assertThat(evaluateExpression("rootObjectInstanceMethod1()"), is((Object) true)); + + assertThat(evaluateExpression("rootObjectInstanceField2"), is((Object) 42)); + assertThat(evaluateExpression("rootObjectInstanceMethod2()"), is((Object) "rootObjectInstanceMethod2")); + + assertThat(evaluateExpression("[0]"), is((Object) "parameterValue")); + } + + /** + * @see DATACMNS-533 + */ + @Test + public void shouldBeAbleToAccessCustomRootObjectPropertiesAndFunctionsFromDynamicTargetSource() { + + final AtomicInteger counter = new AtomicInteger(); + + this.provider = new ExtensionAwareEvaluationContextProvider(Arrays.asList( // + new DummyExtension("_first", "first") { + + @Override + public CustomExtensionRootObject1 getRootObject() { + counter.incrementAndGet(); + return new CustomExtensionRootObject1(); + } + }) // + ); + + // inc counter / property access + assertThat(evaluateExpression("rootObjectInstanceField1"), is((Object) "rootObjectInstanceF1")); + + // inc counter / function invocation + assertThat(evaluateExpression("rootObjectInstanceMethod1()"), is((Object) true)); + + assertThat(counter.get(), is(2)); + } + public static class DummyExtension extends EvaluationContextExtensionSupport { public static String DUMMY_KEY = "dummy"; @@ -177,7 +262,6 @@ public class ExtensionAwareEvaluationContextProviderUnitTests { private final String value; public DummyExtension(String key, String value) { - this.key = key; this.value = value; } @@ -210,12 +294,12 @@ public class ExtensionAwareEvaluationContextProviderUnitTests { * @see org.springframework.data.repository.query.spi.EvaluationContextExtensionSupport#getFunctions() */ @Override - public Map getFunctions() { + public Map getFunctions() { - Map functions = new HashMap(super.getFunctions()); + Map functions = new HashMap(super.getFunctions()); try { - functions.put("aliasedMethod", getClass().getMethod("extensionMethod")); + functions.put("aliasedMethod", new Function(getClass().getMethod("extensionMethod"))); return functions; } catch (Exception o_O) { throw new RuntimeException(o_O); @@ -246,4 +330,26 @@ public class ExtensionAwareEvaluationContextProviderUnitTests { List findByFirstname(@Param("firstname") String firstname, Sort sort); } + + public static class CustomExtensionRootObject1 { + + public String rootObjectInstanceField1 = "rootObjectInstanceF1"; + + public boolean rootObjectInstanceMethod1() { + return true; + } + + public String getStringProperty() { + return "stringProperty"; + } + } + + public static class CustomExtensionRootObject2 { + + public Integer rootObjectInstanceField2 = 42; + + public String rootObjectInstanceMethod2() { + return "rootObjectInstanceMethod2"; + } + } }