diff --git a/spring-core/src/main/java/org/springframework/aot/generate/MethodReference.java b/spring-core/src/main/java/org/springframework/aot/generate/MethodReference.java new file mode 100644 index 0000000000..313b679e59 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/aot/generate/MethodReference.java @@ -0,0 +1,237 @@ +/* + * Copyright 2002-2022 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 + * + * https://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.aot.generate; + +import org.springframework.javapoet.ClassName; +import org.springframework.javapoet.CodeBlock; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * A reference to a static or instance method. + * + * @author Phillip Webb + * @since 6.0 + */ +public final class MethodReference { + + private final Kind kind; + + private final ClassName declaringClass; + + private final String methodName; + + + private MethodReference(Kind kind, @Nullable ClassName declaringClass, + String methodName) { + this.kind = kind; + this.declaringClass = declaringClass; + this.methodName = methodName; + } + + + /** + * Create a new method reference that refers to the given instance method. + * @param methodName the method name + * @return a new {@link MethodReference} instance + */ + public static MethodReference of(String methodName) { + Assert.hasLength(methodName, "'methodName' must not be empty"); + return new MethodReference(Kind.INSTANCE, null, methodName); + } + + /** + * Create a new method reference that refers to the given instance method. + * @param declaringClass the declaring class + * @param methodName the method name + * @return a new {@link MethodReference} instance + */ + public static MethodReference of(Class declaringClass, String methodName) { + Assert.notNull(declaringClass, "'declaringClass' must not be null"); + Assert.hasLength(methodName, "'methodName' must not be empty"); + return new MethodReference(Kind.INSTANCE, ClassName.get(declaringClass), + methodName); + } + + /** + * Create a new method reference that refers to the given instance method. + * @param declaringClass the declaring class + * @param methodName the method name + * @return a new {@link MethodReference} instance + */ + public static MethodReference of(ClassName declaringClass, String methodName) { + Assert.notNull(declaringClass, "'declaringClass' must not be null"); + Assert.hasLength(methodName, "'methodName' must not be empty"); + return new MethodReference(Kind.INSTANCE, declaringClass, methodName); + } + + /** + * Create a new method reference that refers to the given static method. + * @param declaringClass the declaring class + * @param methodName the method name + * @return a new {@link MethodReference} instance + */ + public static MethodReference ofStatic(Class declaringClass, String methodName) { + Assert.notNull(declaringClass, "'declaringClass' must not be null"); + Assert.hasLength(methodName, "'methodName' must not be empty"); + return new MethodReference(Kind.STATIC, ClassName.get(declaringClass), + methodName); + } + + /** + * Create a new method reference that refers to the given static method. + * @param declaringClass the declaring class + * @param methodName the method name + * @return a new {@link MethodReference} instance + */ + public static MethodReference ofStatic(ClassName declaringClass, String methodName) { + Assert.notNull(declaringClass, "'declaringClass' must not be null"); + Assert.hasLength(methodName, "'methodName' must not be empty"); + return new MethodReference(Kind.STATIC, declaringClass, methodName); + } + + + /** + * Return the referenced declaring class. + * @return the declaring class + */ + public ClassName getDeclaringClass() { + return this.declaringClass; + } + + /** + * Return the referenced method name. + * @return the method name + */ + public String getMethodName() { + return this.methodName; + } + + /** + * Return this method reference as a {@link CodeBlock}. If the reference is + * to an instance method then {@code this::} will be returned. + * @return a code block for the method reference. + * @see #toCodeBlock(String) + */ + public CodeBlock toCodeBlock() { + return toCodeBlock(null); + } + + /** + * Return this method reference as a {@link CodeBlock}. If the reference is + * to an instance method and {@code instanceVariable} is {@code null} then + * {@code this::} will be returned. No {@code instanceVariable} + * can be specified for static method references. + * @param instanceVariable the instance variable or {@code null} + * @return a code block for the method reference. + * @see #toCodeBlock(String) + */ + public CodeBlock toCodeBlock(@Nullable String instanceVariable) { + return switch (this.kind) { + case INSTANCE -> toCodeBlockForInstance(instanceVariable); + case STATIC -> toCodeBlockForStatic(instanceVariable); + }; + } + + private CodeBlock toCodeBlockForInstance(String instanceVariable) { + instanceVariable = (instanceVariable != null) ? instanceVariable : "this"; + return CodeBlock.of("$L::$L", instanceVariable, this.methodName); + + } + + private CodeBlock toCodeBlockForStatic(@Nullable String instanceVariable) { + Assert.isTrue(instanceVariable == null, + "'instanceVariable' must be null for static method references"); + return CodeBlock.of("$T::$L", this.declaringClass, this.methodName); + } + + /** + * Return this method reference as an invocation {@link CodeBlock}. + * @param arguments the method arguments + * @return a code back to invoke the method + */ + public CodeBlock toInvokeCodeBlock(CodeBlock... arguments) { + return toInvokeCodeBlock(null, arguments); + } + + /** + * Return this method reference as an invocation {@link CodeBlock}. + * @param instanceVariable the instance variable or {@code null} + * @param arguments the method arguments + * @return a code back to invoke the method + */ + public CodeBlock toInvokeCodeBlock(@Nullable String instanceVariable, + CodeBlock... arguments) { + + return switch (this.kind) { + case INSTANCE -> toInvokeCodeBlockForInstance(instanceVariable, arguments); + case STATIC -> toInvokeCodeBlockForStatic(instanceVariable, arguments); + }; + } + + private CodeBlock toInvokeCodeBlockForInstance(@Nullable String instanceVariable, + CodeBlock[] arguments) { + + CodeBlock.Builder builder = CodeBlock.builder(); + if (instanceVariable != null) { + builder.add("$L.", instanceVariable); + } + else if (this.declaringClass != null) { + builder.add("new $T().", this.declaringClass); + } + builder.add("$L", this.methodName); + addArguments(builder, arguments); + return builder.build(); + } + + private CodeBlock toInvokeCodeBlockForStatic(@Nullable String instanceVariable, + CodeBlock[] arguments) { + + Assert.isTrue(instanceVariable == null, + "'instanceVariable' must be null for static method references"); + CodeBlock.Builder builder = CodeBlock.builder(); + builder.add("$T.$L", this.declaringClass, this.methodName); + addArguments(builder, arguments); + return builder.build(); + } + + private void addArguments(CodeBlock.Builder builder, CodeBlock[] arguments) { + builder.add("("); + for (int i = 0; i < arguments.length; i++) { + if (i != 0) { + builder.add(", "); + } + builder.add(arguments[i]); + } + builder.add(")"); + } + + @Override + public String toString() { + return switch (this.kind) { + case INSTANCE -> ((this.declaringClass != null) ? "<" + this.declaringClass + ">" + : "") + "::" + this.methodName; + case STATIC -> this.declaringClass + "::" + this.methodName; + }; + } + + + private enum Kind { + INSTANCE, STATIC + } + +} diff --git a/spring-core/src/test/java/org/springframework/aot/generate/MethodReferenceTests.java b/spring-core/src/test/java/org/springframework/aot/generate/MethodReferenceTests.java new file mode 100644 index 0000000000..de5c79667b --- /dev/null +++ b/spring-core/src/test/java/org/springframework/aot/generate/MethodReferenceTests.java @@ -0,0 +1,226 @@ +/* + * Copyright 2002-2022 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 + * + * https://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.aot.generate; + +import org.junit.jupiter.api.Test; + +import org.springframework.javapoet.ClassName; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link MethodReference}. + * + * @author Phillip Webb + */ +class MethodReferenceTests { + + private static final String EXPECTED_STATIC = "org.springframework.aot.generate.MethodReferenceTests::someMethod"; + + private static final String EXPECTED_ANONYMOUS_INSTANCE = "::someMethod"; + + private static final String EXPECTED_DECLARED_INSTANCE = "::someMethod"; + + + @Test + void ofWithStringWhenMethodNameIsNullThrowsException() { + String methodName = null; + assertThatIllegalArgumentException() + .isThrownBy(() -> MethodReference.of(methodName)) + .withMessage("'methodName' must not be empty"); + } + + @Test + void ofWithStringCreatesMethodReference() { + String methodName = "someMethod"; + MethodReference reference = MethodReference.of(methodName); + assertThat(reference).hasToString(EXPECTED_ANONYMOUS_INSTANCE); + } + + @Test + void ofWithClassAndStringWhenDeclaringClassIsNullThrowsException() { + Class declaringClass = null; + String methodName = "someMethod"; + assertThatIllegalArgumentException() + .isThrownBy(() -> MethodReference.of(declaringClass, methodName)) + .withMessage("'declaringClass' must not be null"); + } + + @Test + void ofWithClassAndStringWhenMethodNameIsNullThrowsException() { + Class declaringClass = MethodReferenceTests.class; + String methodName = null; + assertThatIllegalArgumentException() + .isThrownBy(() -> MethodReference.of(declaringClass, methodName)) + .withMessage("'methodName' must not be empty"); + } + + @Test + void ofWithClassAndStringCreatesMethodReference() { + Class declaringClass = MethodReferenceTests.class; + String methodName = "someMethod"; + MethodReference reference = MethodReference.of(declaringClass, methodName); + assertThat(reference).hasToString(EXPECTED_DECLARED_INSTANCE); + } + + @Test + void ofWithClassNameAndStringWhenDeclaringClassIsNullThrowsException() { + ClassName declaringClass = null; + String methodName = "someMethod"; + assertThatIllegalArgumentException() + .isThrownBy(() -> MethodReference.of(declaringClass, methodName)) + .withMessage("'declaringClass' must not be null"); + } + + @Test + void ofWithClassNameAndStringWhenMethodNameIsNullThrowsException() { + ClassName declaringClass = ClassName.get(MethodReferenceTests.class); + String methodName = null; + assertThatIllegalArgumentException() + .isThrownBy(() -> MethodReference.of(declaringClass, methodName)) + .withMessage("'methodName' must not be empty"); + } + + @Test + void ofWithClassNameAndStringCreateMethodReference() { + ClassName declaringClass = ClassName.get(MethodReferenceTests.class); + String methodName = "someMethod"; + MethodReference reference = MethodReference.of(declaringClass, methodName); + assertThat(reference).hasToString(EXPECTED_DECLARED_INSTANCE); + } + + @Test + void ofStaticWithClassAndStringWhenDeclaringClassIsNullThrowsException() { + Class declaringClass = null; + String methodName = "someMethod"; + assertThatIllegalArgumentException() + .isThrownBy(() -> MethodReference.ofStatic(declaringClass, methodName)) + .withMessage("'declaringClass' must not be null"); + } + + @Test + void ofStaticWithClassAndStringWhenMethodNameIsEmptyThrowsException() { + Class declaringClass = MethodReferenceTests.class; + String methodName = null; + assertThatIllegalArgumentException() + .isThrownBy(() -> MethodReference.ofStatic(declaringClass, methodName)) + .withMessage("'methodName' must not be empty"); + } + + @Test + void ofStaticWithClassAndStringCreatesMethodReference() { + Class declaringClass = MethodReferenceTests.class; + String methodName = "someMethod"; + MethodReference reference = MethodReference.ofStatic(declaringClass, methodName); + assertThat(reference).hasToString(EXPECTED_STATIC); + } + + @Test + void ofStaticWithClassNameAndGeneratedMethodNameWhenDeclaringClassIsNullThrowsException() { + ClassName declaringClass = null; + String methodName = "someMethod"; + assertThatIllegalArgumentException() + .isThrownBy(() -> MethodReference.ofStatic(declaringClass, methodName)) + .withMessage("'declaringClass' must not be null"); + } + + @Test + void ofStaticWithClassNameAndGeneratedMethodNameWhenMethodNameIsEmptyThrowsException() { + ClassName declaringClass = ClassName.get(MethodReferenceTests.class); + String methodName = null; + assertThatIllegalArgumentException() + .isThrownBy(() -> MethodReference.ofStatic(declaringClass, methodName)) + .withMessage("'methodName' must not be empty"); + } + + @Test + void ofStaticWithClassNameAndGeneratedMethodNameCreatesMethodReference() { + ClassName declaringClass = ClassName.get(MethodReferenceTests.class); + String methodName = "someMethod"; + MethodReference reference = MethodReference.ofStatic(declaringClass, methodName); + assertThat(reference).hasToString(EXPECTED_STATIC); + } + + @Test + void toCodeBlockWhenInstanceMethodReferenceAndInstanceVariableIsNull() { + MethodReference reference = MethodReference.of("someMethod"); + assertThat(reference.toCodeBlock(null)).hasToString("this::someMethod"); + } + + @Test + void toCodeBlockWhenInstanceMethodReferenceAndInstanceVariableIsNotNull() { + MethodReference reference = MethodReference.of("someMethod"); + assertThat(reference.toCodeBlock("myInstance")) + .hasToString("myInstance::someMethod"); + } + + @Test + void toCodeBlockWhenStaticMethodReferenceAndInstanceVariableIsNull() { + MethodReference reference = MethodReference.ofStatic(MethodReferenceTests.class, + "someMethod"); + assertThat(reference.toCodeBlock(null)).hasToString(EXPECTED_STATIC); + } + + @Test + void toCodeBlockWhenStaticMethodReferenceAndInstanceVariableIsNotNullThrowsException() { + MethodReference reference = MethodReference.ofStatic(MethodReferenceTests.class, + "someMethod"); + assertThatIllegalArgumentException() + .isThrownBy(() -> reference.toCodeBlock("myInstance")).withMessage( + "'instanceVariable' must be null for static method references"); + } + + @Test + void toInvokeCodeBlockWhenInstanceMethodReferenceAndInstanceVariableIsNull() { + MethodReference reference = MethodReference.of("someMethod"); + assertThat(reference.toInvokeCodeBlock()).hasToString("someMethod()"); + } + + @Test + void toInvokeCodeBlockWhenInstanceMethodReferenceAndInstanceVariableIsNullAndHasDecalredClass() { + MethodReference reference = MethodReference.of(MethodReferenceTests.class, + "someMethod"); + assertThat(reference.toInvokeCodeBlock()).hasToString( + "new org.springframework.aot.generate.MethodReferenceTests().someMethod()"); + } + + @Test + void toInvokeCodeBlockWhenInstanceMethodReferenceAndInstanceVariableIsNotNull() { + MethodReference reference = MethodReference.of("someMethod"); + assertThat(reference.toInvokeCodeBlock("myInstance")) + .hasToString("myInstance.someMethod()"); + } + + @Test + void toInvokeCodeBlockWhenStaticMethodReferenceAndInstanceVariableIsNull() { + MethodReference reference = MethodReference.ofStatic(MethodReferenceTests.class, + "someMethod"); + assertThat(reference.toInvokeCodeBlock()).hasToString( + "org.springframework.aot.generate.MethodReferenceTests.someMethod()"); + } + + @Test + void toInvokeCodeBlockWhenStaticMethodReferenceAndInstanceVariableIsNotNullThrowsException() { + MethodReference reference = MethodReference.ofStatic(MethodReferenceTests.class, + "someMethod"); + assertThatIllegalArgumentException() + .isThrownBy(() -> reference.toInvokeCodeBlock("myInstance")).withMessage( + "'instanceVariable' must be null for static method references"); + } + +}