allArguments = new ArrayList<>(nameArguments);
+
+ if (hasArguments) {
+ allArguments.add(argsBuilder.build());
+ }
+
+ builder.add(name, allArguments.toArray());
+ }
+ }
+
+ /**
+ * An extended variant of {@link CodeBlock.Builder} that supports building statements in a fluent way and extended for
+ * functional {@link #addStatement(Consumer) statement creation}.
+ *
+ * This builder provides additional methods for creating and managing code blocks, including support for control flow,
+ * named arguments, and conditional statements. It is designed to enhance the readability and flexibility of code
+ * block construction.
+ *
+ * Use this builder to create complex code structures in a fluent and intuitive manner.
+ *
+ * @see CodeBlock.Builder
+ */
+ public static class CodeBlockBuilder {
+
+ private final CodeBlock.Builder builder;
+
+ CodeBlockBuilder(CodeBlock.Builder builder) {
+ this.builder = builder;
+ }
+
+ /**
+ * Determine whether this builder is empty.
+ *
+ * @return {@code true} if the builder is empty; {@code false} otherwise.
+ * @see CodeBlock.Builder#isEmpty()
+ */
+ public boolean isEmpty() {
+ return builder.isEmpty();
+ }
+
+ /**
+ * Add a formatted statement to the code block.
+ *
+ * @param format the format string.
+ * @param args the arguments for the format string.
+ * @return {@code this} builder.
+ * @see CodeBlock.Builder#add(String, Object...)
+ */
+ @Contract("_, _ -> this")
+ public CodeBlockBuilder add(String format, @Nullable Object... args) {
+
+ builder.add(format, args);
+ return this;
+ }
+
+ /**
+ * Add a {@link CodeBlock} as a statement to the code block.
+ *
+ * @param codeBlock the {@link CodeBlock} to add.
+ * @return {@code this} builder.
+ * @see CodeBlock.Builder#addStatement(CodeBlock)
+ */
+ @Contract("_ -> this")
+ public CodeBlockBuilder addStatement(CodeBlock codeBlock) {
+
+ builder.addStatement(codeBlock);
+ return this;
+ }
+
+ /**
+ * Add a statement to the code block using a {@link Consumer} to configure it.
+ *
+ * @param consumer the {@link Consumer} to configure the statement.
+ * @return {@code this} builder.
+ */
+ @Contract("null -> fail; _ -> this")
+ public CodeBlockBuilder addStatement(Consumer consumer) {
+
+ Assert.notNull(consumer, "Consumer must not be null");
+
+ StatementBuilder statementBuilder = new StatementBuilder();
+ consumer.accept(statementBuilder);
+
+ if (!statementBuilder.isEmpty()) {
+
+ this.add("$[");
+
+ for (CodeBlock block : statementBuilder.blocks) {
+ builder.add(block);
+ }
+
+ this.add(";\n$]");
+
+ }
+ return this;
+ }
+
+ /**
+ * Add a {@link CodeBlock} to the code block.
+ *
+ * @param codeBlock the {@link CodeBlock} to add.
+ * @return {@code this} builder.
+ * @see CodeBlock.Builder#addStatement(CodeBlock)
+ */
+ @Contract("_ -> this")
+ public CodeBlockBuilder add(CodeBlock codeBlock) {
+
+ builder.add(codeBlock);
+ return this;
+ }
+
+ /**
+ * Add a formatted statement to the code block.
+ *
+ * @param format the format string.
+ * @param args the arguments for the format string.
+ * @return {@code this} builder.
+ * @see CodeBlock.Builder#addStatement(String, Object...)
+ */
+ @Contract("_, _ -> this")
+ public CodeBlockBuilder addStatement(String format, @Nullable Object... args) {
+
+ builder.addStatement(format, args);
+ return this;
+ }
+
+ /**
+ * Add named arguments to the code block.
+ *
+ * @param format the format string.
+ * @param arguments the named arguments.
+ * @return {@code this} builder.
+ * @see CodeBlock.Builder#addNamed(String, Map)
+ */
+ @Contract("_, _ -> this")
+ public CodeBlockBuilder addNamed(String format, Map arguments) {
+
+ builder.addNamed(format, arguments);
+ return this;
+ }
+
+ /**
+ * Begin a control flow block with the specified format and arguments.
+ *
+ * @param controlFlow the control flow format string.
+ * @param args the arguments for the control flow format string.
+ * @return {@code this} builder.
+ * @see CodeBlock.Builder#beginControlFlow(String, Object...)
+ */
+ @Contract("_, _ -> this")
+ public CodeBlockBuilder beginControlFlow(String controlFlow, @Nullable Object... args) {
+
+ builder.beginControlFlow(controlFlow, args);
+ return this;
+ }
+
+ /**
+ * End the current control flow block with the specified format and arguments.
+ *
+ * @param controlFlow the control flow format string.
+ * @param args the arguments for the control flow format string.
+ * @return {@code this} builder.
+ * @see CodeBlock.Builder#endControlFlow(String, Object...)
+ */
+ @Contract("_, _ -> this")
+ public CodeBlockBuilder endControlFlow(String controlFlow, @Nullable Object... args) {
+
+ builder.endControlFlow(controlFlow, args);
+ return this;
+ }
+
+ /**
+ * End the current control flow block.
+ *
+ * @return {@code this} builder.
+ * @see CodeBlock.Builder#endControlFlow()
+ */
+ @Contract("-> this")
+ public CodeBlockBuilder endControlFlow() {
+
+ builder.endControlFlow();
+ return this;
+ }
+
+ /**
+ * Begin the next control flow block with the specified format and arguments.
+ *
+ * @param controlFlow the control flow format string.
+ * @param args the arguments for the control flow format string.
+ * @return {@code this} builder.
+ * @see CodeBlock.Builder#nextControlFlow(String, Object...)
+ */
+ @Contract("_, _ -> this")
+ public CodeBlockBuilder nextControlFlow(String controlFlow, @Nullable Object... args) {
+
+ builder.nextControlFlow(controlFlow, args);
+ return this;
+ }
+
+ /**
+ * Indent the current code block.
+ *
+ * @return {@code this} builder.
+ * @see CodeBlock.Builder#indent()
+ */
+ @Contract("-> this")
+ public CodeBlockBuilder indent() {
+
+ builder.indent();
+ return this;
+ }
+
+ /**
+ * Unindent the current code block.
+ *
+ * @return {@code this} builder.
+ * @see CodeBlock.Builder#unindent()
+ */
+ @Contract("-> this")
+ public CodeBlockBuilder unindent() {
+
+ builder.unindent();
+ return this;
+ }
+
+ /**
+ * Build the {@link CodeBlock} from the current state of the builder.
+ *
+ * @return the constructed {@link CodeBlock}.
+ */
+ @CheckReturnValue
+ public CodeBlock build() {
+ return builder.build();
+ }
+
+ /**
+ * Clear the current state of the builder.
+ *
+ * @return {@code this} builder.
+ */
+ @Contract("-> this")
+ public CodeBlockBuilder clear() {
+
+ builder.clear();
+ return this;
+ }
+
+ }
+
+ /**
+ * Builder for creating statements including conditional and concatenated variants.
+ *
+ * This builder allows for the creation of complex statements with conditional logic and concatenated elements. It is
+ * designed to simplify the construction of dynamic code blocks.
+ *
+ * Use this builder to handle conditional inclusion in a structured and fluent manner instead of excessive conditional
+ * nesting that would be required otherwise in the calling code.
+ */
+ public static class StatementBuilder {
+
+ private final List blocks = new ArrayList<>();
+
+ StatementBuilder() {}
+
+ /**
+ * Determine whether this builder is empty.
+ *
+ * @return {@code true} if the builder is empty; {@code false} otherwise.
+ */
+ public boolean isEmpty() {
+ return blocks.isEmpty();
+ }
+
+ /**
+ * Add a conditional statement to the builder if the condition is met.
+ *
+ * @param state the condition to evaluate.
+ * @return a {@link ConditionalStatementStep} for further configuration.
+ */
+ public ConditionalStatementStep when(boolean state) {
+ return whenNot(!state);
+ }
+
+ /**
+ * Add a conditional statement to the builder if the condition is not met.
+ *
+ * @param state the condition to evaluate.
+ * @return a {@link ConditionalStatementStep} for further configuration.
+ */
+ public ConditionalStatementStep whenNot(boolean state) {
+
+ return (format, args) -> {
+
+ if (!state) {
+ add(format, args);
+ }
+ return this;
+ };
+ }
+
+ /**
+ * Add a formatted statement to the builder.
+ *
+ * @param format the format string.
+ * @param args the arguments for the format string.
+ * @return {@code this} builder.
+ */
+ @Contract("_, _ -> this")
+ public StatementBuilder add(String format, @Nullable Object... args) {
+ return add(CodeBlock.of(format, args));
+ }
+
+ /**
+ * Add a {@link CodeBlock} to the statement builder.
+ *
+ * @param codeBlock the code block to add.
+ * @return {@code this} builder.
+ */
+ @Contract("null -> fail; _ -> this")
+ public StatementBuilder add(CodeBlock codeBlock) {
+
+ Assert.notNull(codeBlock, "CodeBlock must not be null");
+ blocks.add(codeBlock);
+ return this;
+ }
+
+ /**
+ * Concatenate elements into the builder with a delimiter.
+ *
+ * @param elements the elements to concatenate.
+ * @param delim the delimiter to use between elements.
+ * @param mapper the mapping function to apply to each element returning a {@link CodeBlock} to add.
+ * @param the type of the elements.
+ * @return {@code this} builder.
+ */
+ @Contract("null, _ -> fail; _, _ -> this")
+ public StatementBuilder addAll(Iterable extends T> elements, String delim,
+ Function super T, CodeBlock> mapper) {
+ return addAll(elements, t -> delim, mapper);
+ }
+
+ /**
+ * Concatenate elements into the builder with a custom delimiter function.
+ *
+ * @param elements the elements to concatenate.
+ * @param delim the function to determine the delimiter for each element. Delimiters are applied beginning with the
+ * second iteration element and obtain from the current element.
+ * @param mapper the mapping function to apply to each element returning a {@link CodeBlock} to add.
+ * @param the type of the elements.
+ * @return {@code this} builder.
+ */
+ @Contract("null, _, _ -> fail; _, _, _ -> this")
+ public StatementBuilder addAll(Iterable extends T> elements, Function super T, String> delim,
+ Function super T, CodeBlock> mapper) {
+
+ Assert.notNull(elements, "Elements must not be null");
+
+ boolean first = true;
+ for (T element : elements) {
+
+ if (first) {
+ first = false;
+ } else {
+ blocks.add(CodeBlock.of(delim.apply(element)));
+ }
+
+ add(mapper.apply(element));
+ }
+
+ return this;
+ }
+
+ /**
+ * Functional interface for conditional statement steps.
+ */
+ public interface ConditionalStatementStep {
+
+ /**
+ * Add a statement to the builder if the condition is met.
+ *
+ * @param format the format string.
+ * @param args the arguments for the format string.
+ * @return the {@link StatementBuilder}.
+ */
+ StatementBuilder then(String format, @Nullable Object... args);
+
+ }
+
+ }
+
+ /**
+ * Builder for constructing return statements based on the target return type. The resulting {@link #build()
+ * CodeBlock} must be added as a {@link CodeBlock.Builder#addStatement(CodeBlock)}.
+ */
+ public abstract static class ReturnBuilderSupport {
+
+ private final List rules = new ArrayList<>();
+ private final List fallback = new ArrayList<>();
+
+ /**
+ * Create a new builder.
+ */
+ ReturnBuilderSupport() {}
+
+ /**
+ * Add a return statement if the given condition is {@code true}.
+ *
+ * @param condition the condition to evaluate.
+ * @param format the code format string.
+ * @param args the format arguments.
+ * @return {@code this} builder.
+ */
+ @Contract("_, _, _ -> this")
+ public ReturnBuilderSupport when(boolean condition, String format, @Nullable Object... args) {
+ this.rules.add(ruleOf(condition, format, args));
+ return this;
+ }
+
+ /**
+ * Add a fallback return statement if no previous return statement was added.
+ *
+ * @param format the code format string.
+ * @param args the format arguments.
+ * @return {@code this} builder.
+ */
+ @Contract("_, _ -> this")
+ public ReturnBuilderSupport otherwise(String format, @Nullable Object... args) {
+ this.fallback.add(ruleOf(true, format, args));
+ return this;
+ }
+
+ /**
+ * Add a fallback return statement if no previous return statement was added.
+ *
+ * @param builderConsumer the code block builder consumer to apply.
+ * @return {@code this} builder.
+ */
+ ReturnBuilderSupport otherwise(Consumer builderConsumer) {
+ this.fallback.add(new ReturnRule(true, "", new Object[] {}, builderConsumer));
+ return this;
+ }
+
+ /**
+ * Build the code block representing the return statement.
+ *
+ * @return the resulting {@code CodeBlock}
+ */
+ @CheckReturnValue
+ public CodeBlock build() {
+
+ CodeBlock.Builder builder = CodeBlock.builder();
+
+ for (ReturnRule rule : (Iterable extends ReturnRule>) () -> Stream
+ .concat(this.rules.stream(), this.fallback.stream()).iterator()) {
+ if (rule.condition()) {
+ builder.add("return");
+ rule.accept(builder);
+ return builder.build();
+ }
+ }
+
+ return builder.build();
+ }
+
+ /**
+ * Add a return statement if the given condition is {@code true}.
+ *
+ * @param condition the condition to evaluate.
+ * @param format the code format string.
+ * @param args the format arguments.
+ * @return {@code this} builder.
+ */
+ static ReturnRule ruleOf(boolean condition, String format, @Nullable Object... args) {
+
+ Assert.notNull(format, "Format must not be null");
+
+ if (format.startsWith("return")) {
+ throw new IllegalArgumentException("Return value format '%s' must not contain 'return'".formatted(format));
+ }
+
+ return new ReturnRule(condition, format, args, null);
+ }
+
+ }
+
+ record ReturnRule(boolean condition, String format, @Nullable Object[] args,
+ @Nullable Consumer builderCustomizer) {
+
+ public void accept(CodeBlock.Builder builder) {
+
+ if (StringUtils.hasText(format()) || builderCustomizer() != null) {
+
+ builder.add(" ");
+
+ if (StringUtils.hasText(format())) {
+ builder.add(format(), args());
+ }
+
+ if (builderCustomizer() != null) {
+ builderCustomizer().accept(builder);
+ }
+ }
+ }
+
+ }
+
+ /**
+ * Builder for constructing return statements based conditionally on the target return type. The resulting
+ * {@link #build() CodeBlock} must be added as a {@link CodeBlock.Builder#addStatement(CodeBlock)}.
+ */
+ public static class TypedReturnBuilder extends ReturnBuilderSupport {
+
+ private final ResolvableType returnType;
+
+ /**
+ * Create a new builder for the given return type.
+ *
+ * @param returnType the method return type
+ */
+ TypedReturnBuilder(ResolvableType returnType) {
+
+ Assert.notNull(returnType, "Return type must not be null");
+
+ this.returnType = returnType;
+
+ // consider early return cases for Void and void.
+ whenBoxed(Void.class, "null");
+ when(ReflectionUtils.isVoid(returnType.toClass()), "");
+ }
+
+ /**
+ * Add return statements for numeric types if the given {@code resultToReturn} points to a {@link Number}. Considers
+ * primitive and boxed {@code int} and {@code long} type return paths and that {@code resultToReturn} can be
+ * {@literal null}.
+ *
+ * @param resultToReturn the argument or variable name holding the result.
+ * @return {@code this} builder.
+ */
+ @Contract("_ -> this")
+ public TypedReturnBuilder number(String resultToReturn) {
+ return whenBoxedLong("$1L != null ? $1L.longValue() : null", resultToReturn)
+ .whenLong("$1L != null ? $1L.longValue() : 0L", resultToReturn)
+ .whenBoxedInteger("$1L != null ? $1L.intValue() : null", resultToReturn)
+ .whenInt("$1L != null ? $1L.intValue() : 0", resultToReturn);
+ }
+
+ /**
+ * Add a return statement if the return type is boolean (primitive or box type).
+ *
+ * @param format the code format string.
+ * @param args the format arguments.
+ * @return {@code this} builder.
+ */
+ @Contract("_, _ -> this")
+ public TypedReturnBuilder whenBoolean(String format, @Nullable Object... args) {
+ return when(returnType.isAssignableFrom(boolean.class) || returnType.isAssignableFrom(Boolean.class), format,
+ args);
+ }
+
+ /**
+ * Add a return statement if the return type is {@link Long} (boxed {@code long} type).
+ *
+ * @param format the code format string.
+ * @param args the format arguments.
+ * @return {@code this} builder.
+ */
+ @Contract("_, _ -> this")
+ public TypedReturnBuilder whenBoxedLong(String format, @Nullable Object... args) {
+ return whenBoxed(long.class, format, args);
+ }
+
+ /**
+ * Add a return statement if the return type is a primitive {@code long} type.
+ *
+ * @param format the code format string.
+ * @param args the format arguments.
+ * @return {@code this} builder.
+ */
+ @Contract("_, _ -> this")
+ public TypedReturnBuilder whenLong(String format, @Nullable Object... args) {
+ return when(returnType.toClass() == long.class, format, args);
+ }
+
+ /**
+ * Add a return statement if the return type is {@link Integer} (boxed {@code int} type).
+ *
+ * @param format the code format string.
+ * @param args the format arguments.
+ * @return {@code this} builder.
+ */
+ @Contract("_, _ -> this")
+ public TypedReturnBuilder whenBoxedInteger(String format, @Nullable Object... args) {
+ return whenBoxed(int.class, format, args);
+ }
+
+ /**
+ * Add a return statement if the return type is a primitive {@code int} type.
+ *
+ * @param format the code format string.
+ * @param args the format arguments.
+ * @return {@code this} builder.
+ */
+ @Contract("_, _ -> this")
+ public TypedReturnBuilder whenInt(String format, @Nullable Object... args) {
+ return when(returnType.toClass() == int.class, format, args);
+ }
+
+ /**
+ * Add a return statement if the return type matches the given boxed wrapper type.
+ *
+ * @param primitiveOrWrapper the primitive or wrapper type.
+ * @param format the code format string.
+ * @param args the format arguments.
+ * @return {@code this} builder.
+ */
+ @Contract("null, _, _ -> fail; _, _, _ -> this")
+ public TypedReturnBuilder whenBoxed(Class> primitiveOrWrapper, String format, @Nullable Object... args) {
+
+ Class> primitiveWrapper = ClassUtils.resolvePrimitiveIfNecessary(primitiveOrWrapper);
+ return when(returnType.toClass() == primitiveWrapper, format, args);
+ }
+
+ /**
+ * Add a return statement if the return type matches the given primitive or boxed wrapper type.
+ *
+ * @param primitiveType the primitive or wrapper type.
+ * @param format the code format string.
+ * @param args the format arguments.
+ * @return {@code this} builder.
+ */
+ @Contract("null, _, _ -> fail; _, _, _ -> this")
+ public TypedReturnBuilder whenPrimitiveOrBoxed(Class> primitiveType, String format, @Nullable Object... args) {
+
+ Class> primitiveWrapper = ClassUtils.resolvePrimitiveIfNecessary(primitiveType);
+ return when(
+ ClassUtils.isAssignable(ClassUtils.resolvePrimitiveIfNecessary(returnType.toClass()), primitiveWrapper),
+ format, args);
+ }
+
+ /**
+ * Add a return statement if the declared return type is assignable from the given {@code returnType}.
+ *
+ * @param returnType the candidate return type.
+ * @param format the code format string.
+ * @param args the format arguments
+ * @return {@code this} builder.
+ */
+ @Contract("null, _, _ -> fail; _, _, _ -> this")
+ public TypedReturnBuilder when(Class> returnType, String format, @Nullable Object... args) {
+
+ Assert.notNull(returnType, "Return type must not be null");
+ return when(this.returnType.isAssignableFrom(returnType), format, args);
+ }
+
+ /**
+ * Add a return statement if the given condition is {@code true}.
+ *
+ * @param condition the condition to evaluate.
+ * @param format the code format string.
+ * @param args the format arguments.
+ * @return {@code this} builder.
+ */
+ @Override
+ @Contract("_, _, _ -> this")
+ public TypedReturnBuilder when(boolean condition, String format, @Nullable Object... args) {
+ super.when(condition, format, args);
+ return this;
+ }
+
+ /**
+ * Add a fallback return statement considering that the returned value might be nullable and apply conditionally
+ * {@link Optional#ofNullable(Object)} wrapping if the return type is {@code Optional}.
+ *
+ * @param codeBlock the code block result to be returned.
+ * @return {@code this} builder.
+ */
+ @Contract("_ -> this")
+ public TypedReturnBuilder optional(CodeBlock codeBlock) {
+ return optional("$L", codeBlock);
+ }
+
+ /**
+ * Add a fallback return statement considering that the returned value might be nullable and apply conditionally
+ * {@link Optional#ofNullable(Object)} wrapping if the return type is {@code Optional}.
+ *
+ * @param format the code format string.
+ * @param args the format arguments.
+ * @return {@code this} builder.
+ */
+ @Contract("null, _ -> fail; _, _ -> this")
+ public TypedReturnBuilder optional(String format, @Nullable Object... args) {
+
+ if (Optional.class.isAssignableFrom(returnType.toClass())) {
+
+ Assert.hasText(format, "Format must not be null or empty");
+ if (format.startsWith("return")) {
+ throw new IllegalArgumentException("Return value format '%s' must not contain 'return'".formatted(format));
+ }
+
+ otherwise(builder -> {
+
+ builder.add("$T.ofNullable(", Optional.class);
+ builder.add(format, args);
+ builder.add(")");
+ });
+
+ return this;
+ }
+
+ return otherwise(format, args);
+ }
+
+ /**
+ * Add a fallback return statement if no previous return statement was added.
+ *
+ * @param codeBlock the code block result to be returned.
+ * @return {@code this} builder.
+ */
+ @Contract("_ -> this")
+ public TypedReturnBuilder otherwise(CodeBlock codeBlock) {
+ return otherwise("$L", codeBlock);
+ }
+
+ /**
+ * Add a fallback return statement if no previous return statement was added.
+ *
+ * @param format the code format string.
+ * @param args the format arguments.
+ * @return {@code this} builder.
+ */
+ @Override
+ @Contract("_, _ -> this")
+ public TypedReturnBuilder otherwise(String format, @Nullable Object... args) {
+ super.otherwise(format, args);
+ return this;
+ }
+
+ }
+
+ record CodeTuple(String format, @Nullable Object... args) {
+
+ }
+
+}
diff --git a/src/main/java/org/springframework/data/javapoet/TypeNames.java b/src/main/java/org/springframework/data/javapoet/TypeNames.java
new file mode 100644
index 0000000000..eb9db1a9b9
--- /dev/null
+++ b/src/main/java/org/springframework/data/javapoet/TypeNames.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2025 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.data.javapoet;
+
+import org.springframework.core.ResolvableType;
+import org.springframework.javapoet.TypeName;
+import org.springframework.util.ClassUtils;
+
+/**
+ * Collection of {@link org.springframework.javapoet.TypeName} transformation utilities.
+ *
+ * This class delivers some simple functionality that should be provided by the JavaPoet framework. It also provides
+ * easy-to-use methods to convert between types.
+ *
+ * Mainly for internal use within the framework
+ *
+ * @author Mark Paluch
+ * @since 4.0
+ */
+public abstract class TypeNames {
+
+ /**
+ * Obtain a {@link TypeName class name} for the given type, resolving primitive wrappers as necessary.
+ *
+ * @param type the class to use.
+ * @return the corresponding {@link TypeName}.
+ */
+ public static TypeName classNameOrWrapper(Class> type) {
+ return ClassUtils.isPrimitiveOrWrapper(type) ? TypeName.get(ClassUtils.resolvePrimitiveIfNecessary(type))
+ : TypeName.get(type);
+ }
+
+ /**
+ * Obtain a {@link TypeName class name} for the given {@link ResolvableType}, resolving primitive wrappers as
+ * necessary. Ideal to represent a type name used as {@code Class} value as generic parameters are not considered.
+ *
+ * @param resolvableType the resolvable type to use.
+ * @return the corresponding {@link TypeName}.
+ */
+ public static TypeName classNameOrWrapper(ResolvableType resolvableType) {
+ return classNameOrWrapper(resolvableType.toClass());
+ }
+
+ /**
+ * Obtain a {@link TypeName} for the given {@link ResolvableType}. Ideal to represent a type name used as
+ * {@code Class} value as generic parameters are not considered.
+ *
+ * @param resolvableType the resolvable type to use.
+ * @return the corresponding {@link TypeName}.
+ */
+ public static TypeName className(ResolvableType resolvableType) {
+ return TypeName.get(resolvableType.toClass());
+ }
+
+ /**
+ * Obtain a {@link TypeName} for the underlying type of the given {@link ResolvableType}. Can render a class name, a
+ * type signature or a generic type variable.
+ *
+ * @param resolvableType the resolvable type represent.
+ * @return the corresponding {@link TypeName}.
+ */
+ public static TypeName typeName(ResolvableType resolvableType) {
+ return TypeName.get(resolvableType.getType());
+ }
+
+ /**
+ * Obtain a {@link TypeName} for the given type, resolving primitive wrappers as necessary. Ideal to represent a type
+ * parameter for parametrized types as primitive boxing is considered.
+ *
+ * @param type the class to be represented.
+ * @return the corresponding {@link TypeName}.
+ */
+ public static TypeName typeNameOrWrapper(Class> type) {
+ return typeNameOrWrapper(ResolvableType.forClass(type));
+ }
+
+ /**
+ * Obtain a {@link TypeName} for the given {@link ResolvableType}, resolving primitive wrappers as necessary. Can
+ * render a class name, a type signature or a generic type variable. Ideal to represent a type parameter for
+ * parametrized types as primitive boxing is considered.
+ *
+ * @param resolvableType the resolvable type to be represented.
+ * @return the corresponding {@link TypeName}.
+ */
+ public static TypeName typeNameOrWrapper(ResolvableType resolvableType) {
+ return ClassUtils.isPrimitiveOrWrapper(resolvableType.toClass())
+ ? TypeName.get(ClassUtils.resolvePrimitiveIfNecessary(resolvableType.toClass()))
+ : typeName(resolvableType);
+ }
+
+ private TypeNames() {}
+
+}
diff --git a/src/main/java/org/springframework/data/javapoet/package-info.java b/src/main/java/org/springframework/data/javapoet/package-info.java
new file mode 100644
index 0000000000..c0a6b8c24e
--- /dev/null
+++ b/src/main/java/org/springframework/data/javapoet/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * Opinionated extensions to JavaPoet to support Spring Data specific use cases.
+ */
+@org.jspecify.annotations.NullMarked
+package org.springframework.data.javapoet;
diff --git a/src/main/java/org/springframework/data/repository/aot/generate/AotQueryMethodGenerationContext.java b/src/main/java/org/springframework/data/repository/aot/generate/AotQueryMethodGenerationContext.java
index 34fc7f8ff9..2720f992a6 100644
--- a/src/main/java/org/springframework/data/repository/aot/generate/AotQueryMethodGenerationContext.java
+++ b/src/main/java/org/springframework/data/repository/aot/generate/AotQueryMethodGenerationContext.java
@@ -47,6 +47,7 @@ public class AotQueryMethodGenerationContext {
private final QueryMethod queryMethod;
private final RepositoryInformation repositoryInformation;
private final AotRepositoryFragmentMetadata targetTypeMetadata;
+ private final MethodReturn methodReturn;
private final MethodMetadata targetMethodMetadata;
private final VariableNameFactory variableNameFactory;
private final ExpressionMarker expressionMarker;
@@ -60,6 +61,8 @@ protected AotQueryMethodGenerationContext(RepositoryInformation repositoryInform
this.repositoryInformation = repositoryInformation;
this.targetTypeMetadata = new AotRepositoryFragmentMetadata();
this.targetMethodMetadata = new MethodMetadata(repositoryInformation, method);
+ this.methodReturn = new MethodReturn(queryMethod.getResultProcessor().getReturnedType(),
+ targetMethodMetadata.getReturnType());
this.variableNameFactory = LocalVariableNameFactory.forMethod(targetMethodMetadata);
this.expressionMarker = new ExpressionMarker();
}
@@ -73,6 +76,8 @@ protected AotQueryMethodGenerationContext(RepositoryInformation repositoryInform
this.repositoryInformation = repositoryInformation;
this.targetTypeMetadata = targetTypeMetadata;
this.targetMethodMetadata = new MethodMetadata(repositoryInformation, method);
+ this.methodReturn = new MethodReturn(queryMethod.getResultProcessor().getReturnedType(),
+ targetMethodMetadata.getReturnType());
this.variableNameFactory = LocalVariableNameFactory.forMethod(targetMethodMetadata);
this.expressionMarker = new ExpressionMarker();
}
@@ -135,6 +140,13 @@ public Class> getDomainType() {
return getRepositoryInformation().getDomainType();
}
+ /**
+ * @return the method return information.
+ */
+ public MethodReturn getMethodReturn() {
+ return methodReturn;
+ }
+
/**
* @return the returned type without considering dynamic projections.
*/
@@ -146,6 +158,7 @@ public ReturnedType getReturnedType() {
* @return the actual returned domain type.
* @see org.springframework.data.repository.core.RepositoryMetadata#getReturnedDomainClass(Method)
*/
+ @Deprecated(forRemoval = true)
public ResolvableType getActualReturnType() {
return targetMethodMetadata.getActualReturnType();
}
@@ -154,6 +167,7 @@ public ResolvableType getActualReturnType() {
* @return the query method return type.
* @see org.springframework.data.repository.core.RepositoryMetadata#getReturnType(Method)
*/
+ @Deprecated(forRemoval = true)
public ResolvableType getReturnType() {
return targetMethodMetadata.getReturnType();
}
@@ -161,6 +175,7 @@ public ResolvableType getReturnType() {
/**
* @return the {@link TypeName} representing the method return type.
*/
+ @Deprecated(forRemoval = true)
public TypeName getReturnTypeName() {
return TypeName.get(getReturnType().getType());
}
@@ -168,6 +183,7 @@ public TypeName getReturnTypeName() {
/**
* @return the {@link TypeName} representing the actual (component) method return type.
*/
+ @Deprecated(forRemoval = true)
public TypeName getActualReturnTypeName() {
return TypeName.get(getActualReturnType().getType());
}
diff --git a/src/main/java/org/springframework/data/repository/aot/generate/MethodReturn.java b/src/main/java/org/springframework/data/repository/aot/generate/MethodReturn.java
new file mode 100644
index 0000000000..c8b4c4f296
--- /dev/null
+++ b/src/main/java/org/springframework/data/repository/aot/generate/MethodReturn.java
@@ -0,0 +1,195 @@
+/*
+ * Copyright 2025 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.data.repository.aot.generate;
+
+import java.util.Optional;
+import java.util.stream.Stream;
+
+import org.springframework.core.ResolvableType;
+import org.springframework.data.javapoet.TypeNames;
+import org.springframework.data.repository.query.ReturnedType;
+import org.springframework.data.util.ReflectionUtils;
+import org.springframework.data.util.TypeInformation;
+import org.springframework.javapoet.TypeName;
+
+/**
+ * Value object that encapsulates introspection of a method's return type, providing convenient access to its
+ * characteristics such as projection, optionality, array status, and actual type information.
+ *
+ * Designed to support repository method analysis in the context of Ahead-of-Time (AOT) processing, this class leverages
+ * {@link ReturnedType}, {@link ResolvableType}, and {@link TypeInformation} to expose both the declared and actual
+ * return types, including handling of wrapper types, projections, and primitive types.
+ *
+ * Typical usage involves querying the return type characteristics to drive code generation or runtime behavior in
+ * repository infrastructure.
+ *
+ * @author Mark Paluch
+ * @since 4.0
+ */
+public class MethodReturn {
+
+ private final ReturnedType returnedType;
+ private final Class> actualReturnClass;
+ private final ResolvableType returnType;
+ private final ResolvableType actualType;
+ private final TypeName typeName;
+ private final TypeName className;
+ private final TypeName actualTypeName;
+ private final TypeName actualClassName;
+
+ /**
+ * Create a new {@code MethodReturn} instance based on the given {@link ReturnedType} and its {@link ResolvableType
+ * method return type}.
+ *
+ * @param returnedType the returned type to inspect.
+ * @param returnType the method return type.
+ */
+ public MethodReturn(ReturnedType returnedType, ResolvableType returnType) {
+
+ this.returnedType = returnedType;
+ this.returnType = returnType;
+ this.typeName = TypeNames.typeName(returnType);
+ this.className = TypeNames.className(returnType);
+
+ Class> returnClass = returnType.toClass();
+ TypeInformation> typeInformation = TypeInformation.of(returnType);
+ TypeInformation> actualType = typeInformation.isMap() ? typeInformation
+ : (typeInformation.getType().equals(Stream.class) ? typeInformation.getComponentType()
+ : typeInformation.getActualType());
+
+ if (actualType != null) {
+ this.actualType = actualType.toResolvableType();
+ this.actualTypeName = TypeNames.typeName(this.actualType);
+ this.actualClassName = TypeNames.className(this.actualType);
+ this.actualReturnClass = actualType.getType();
+ } else {
+ this.actualType = returnType;
+ this.actualTypeName = typeName;
+ this.actualClassName = className;
+ this.actualReturnClass = returnClass;
+ }
+ }
+
+ /**
+ * Returns whether the method return type is a projection. Query projections (e.g. returning {@code String} or
+ * {@code int} are not considered.
+ *
+ * @return {@literal true} if the return type is a projection.
+ */
+ public boolean isProjecting() {
+ return returnedType.isProjecting();
+ }
+
+ /**
+ * Returns whether the method return type is an interface-based projection.
+ *
+ * @return {@literal true} if the return type is an interface-based projection.
+ */
+ public boolean isInterfaceProjection() {
+ return isProjecting() && returnedType.getReturnedType().isInterface();
+ }
+
+ /**
+ * Returns whether the method return type is {@code Optional}.
+ *
+ * @return {@literal true} if the return type is {@code Optional}.
+ */
+ public boolean isOptional() {
+ return Optional.class.isAssignableFrom(toClass());
+ }
+
+ /**
+ * Returns whether the method return type is an array.
+ *
+ * @return {@literal true} if the return type is an array.
+ */
+ public boolean isArray() {
+ return toClass().isArray();
+ }
+
+ /**
+ * Returns whether the method return type is {@code void}. Considers also {@link Void} and Kotlin's {@code Unit}.
+ *
+ * @return {@literal true} if the return type is {@code void}.
+ */
+ public boolean isVoid() {
+ return ReflectionUtils.isVoid(toClass());
+ }
+
+ /**
+ * Returns the {@link Class} representing the declared return type.
+ *
+ * @return the declared return class.
+ */
+ public Class> toClass() {
+ return returnType.toClass();
+ }
+
+ /**
+ * Returns the actual type (i.e. component type of a collection).
+ *
+ * @return the actual type.
+ */
+ public ResolvableType getActualType() {
+ return actualType;
+ }
+
+ /**
+ * Returns the {@link TypeName} representing the declared return type.
+ *
+ * @return the declared return type name.
+ */
+ public TypeName getTypeName() {
+ return typeName;
+ }
+
+ /**
+ * Returns the {@link TypeName} representing the declared return class (i.e. without generics).
+ *
+ * @return the declared return class name.
+ */
+ public TypeName getClassName() {
+ return className;
+ }
+
+ /**
+ * Returns the actual {@link TypeName} representing the declared return type (component type of collections).
+ *
+ * @return the actual return type name.
+ */
+ public TypeName getActualTypeName() {
+ return actualTypeName;
+ }
+
+ /**
+ * Returns the actual {@link TypeName} representing the declared return class (component type of collections).
+ *
+ * @return the actual return class name.
+ */
+ public TypeName getActualClassName() {
+ return actualClassName;
+ }
+
+ /**
+ * Returns the {@link Class} representing the actual return type.
+ *
+ * @return the actual return class.
+ */
+ public Class> getActualReturnClass() {
+ return actualReturnClass;
+ }
+
+}
diff --git a/src/test/java/org/springframework/data/javapoet/JavaPoetUnitTests.java b/src/test/java/org/springframework/data/javapoet/JavaPoetUnitTests.java
new file mode 100644
index 0000000000..c549b91a56
--- /dev/null
+++ b/src/test/java/org/springframework/data/javapoet/JavaPoetUnitTests.java
@@ -0,0 +1,203 @@
+/*
+ * Copyright 2025 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.data.javapoet;
+
+import static org.assertj.core.api.Assertions.*;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.javapoet.CodeBlock;
+
+/**
+ * Unit tests for {@link LordOfTheStrings}.
+ *
+ * @author Mark Paluch
+ */
+class JavaPoetUnitTests {
+
+ @Test // GH-3357
+ void shouldConsiderConditionals() {
+
+ assertThat(LordOfTheStrings.builder().addStatement(statementBuilder -> {
+ statementBuilder.when(true).then("return $S", "The Return of the King");
+ statementBuilder.when(false).then("return $S", "The Two Towers");
+ }).build()).hasToString("return \"The Return of the King\";\n");
+
+ assertThat(LordOfTheStrings.builder().addStatement(statementBuilder -> {
+ assertThat(statementBuilder.isEmpty()).isTrue();
+ statementBuilder.whenNot(true).then("return $S", "The Return of the King");
+ assertThat(statementBuilder.isEmpty()).isTrue();
+
+ statementBuilder.whenNot(false).then("return $S", "The Two Towers");
+ assertThat(statementBuilder.isEmpty()).isFalse();
+ }).build()).hasToString("return \"The Two Towers\";\n");
+
+ assertThat(LordOfTheStrings.builder().addStatement(statementBuilder -> {
+
+ assertThat(statementBuilder.isEmpty()).isTrue();
+ statementBuilder.add("foo");
+ assertThat(statementBuilder.isEmpty()).isFalse();
+ }).build()).hasToString("foo;\n");
+ }
+
+ @Test // GH-3357
+ void shouldConcatenateCollections() {
+
+ assertThat(LordOfTheStrings.builder().addStatement(statementBuilder -> {
+ statementBuilder.addAll(Arrays.asList("foo", "bar"), ", ", it -> CodeBlock.of(it + " $S", it));
+ }).build()).hasToString("foo \"foo\", bar \"bar\";\n");
+
+ assertThat(LordOfTheStrings.builder().addStatement(statementBuilder -> {
+ statementBuilder.addAll(Arrays.asList("foo", "barrrr"), it -> "" + it.length(),
+ it -> CodeBlock.of(it + " $S", it));
+ }).build()).hasToString("foo \"foo\"6barrrr \"barrrr\";\n");
+ }
+
+ @Test // GH-3357
+ void youShallNotPass() {
+
+ // without expecting arguments, the second $L is superfluous.
+ assertThatIllegalArgumentException().isThrownBy(() -> LordOfTheStrings.invoke("$L.run($L)", "runnable").build());
+ }
+
+ @Test // GH-3357
+ void shouldRenderMethodCall() {
+
+ CodeBlock block = LordOfTheStrings.invoke("$L.run()", "runnable").build();
+ assertThat(block).hasToString("runnable.run()");
+ }
+
+ @Test // GH-3357
+ void shouldRenderMethodCallWithArguments() {
+
+ CodeBlock block = LordOfTheStrings.invoke("$L.run($L)", "runnable").argument("foo").build();
+ assertThat(block).hasToString("runnable.run(foo)");
+
+ block = LordOfTheStrings.invoke("$L.run($L)", "runnable").argument("foo").argument("bar").build();
+ assertThat(block).hasToString("runnable.run(foo, bar)");
+
+ block = LordOfTheStrings.invoke("$L.run($L)", "runnable").argument("$L", "foo").argument("bar").build();
+ assertThat(block).hasToString("runnable.run(foo, bar)");
+
+ block = LordOfTheStrings.invoke("$L.run($L)", "runnable").arguments(Arrays.asList("foo", "bar")).build();
+ assertThat(block).hasToString("runnable.run(foo, bar)");
+
+ block = LordOfTheStrings.invoke("$L.run($L)", "runnable").arguments(List.of()).build();
+ assertThat(block).hasToString("runnable.run()");
+
+ block = LordOfTheStrings.invoke("$L.run($L)", "runnable")
+ .arguments(List.of("foo", "bar"), it -> CodeBlock.of("$S", it)).build();
+ assertThat(block).hasToString("runnable.run(\"foo\", \"bar\")");
+ }
+
+ @Test // GH-3357
+ void shouldRenderAssignTo() {
+
+ CodeBlock block = LordOfTheStrings.invoke("$L.run()", "runnable").assignTo("$T result", String.class);
+ assertThat(block).hasToString("java.lang.String result = runnable.run()");
+ }
+
+ @Test // GH-3357
+ void shouldRenderSimpleReturn() {
+
+ CodeBlock block = LordOfTheStrings.returning(Long.class).otherwise("1L").build();
+ assertThat(block).hasToString("return 1L");
+ }
+
+ @Test // GH-3357
+ void shouldRenderConditionalLongReturn() {
+
+ CodeBlock block = LordOfTheStrings.returning(Long.class).whenLong("1L").whenBoxedLong("$T.valueOf(1)", Long.class)
+ .otherwise("😫").build();
+ assertThat(block).hasToString("return java.lang.Long.valueOf(1)");
+
+ block = LordOfTheStrings.returning(Long.class).whenBoxedLong("$T.valueOf(1)", Long.class).whenLong("1L")
+ .otherwise("😫").build();
+ assertThat(block).hasToString("return java.lang.Long.valueOf(1)");
+
+ block = LordOfTheStrings.returning(long.class).whenBoxedLong("$T.valueOf(1)", Long.class).whenLong("1L")
+ .otherwise("😫").build();
+ assertThat(block).hasToString("return 1L");
+
+ block = LordOfTheStrings.returning(Long.class).whenBoxed(Long.class, "$T.valueOf(1)", Long.class).otherwise("😫")
+ .build();
+ assertThat(block).hasToString("return java.lang.Long.valueOf(1)");
+
+ block = LordOfTheStrings.returning(Long.class).whenBoxed(long.class, "$T.valueOf(1)", Long.class).otherwise("😫")
+ .build();
+ assertThat(block).hasToString("return java.lang.Long.valueOf(1)");
+ }
+
+ @Test // GH-3357
+ void shouldRenderConditionalIntReturn() {
+
+ CodeBlock block = LordOfTheStrings.returning(Integer.class).whenBoxed(long.class, "$T.valueOf(1)", Long.class)
+ .otherwise("😫").build();
+ assertThat(block).hasToString("return 😫");
+
+ block = LordOfTheStrings.returning(Integer.class).whenBoxedInteger("$T.valueOf(1)", Integer.class).otherwise("😫")
+ .build();
+ assertThat(block).hasToString("return java.lang.Integer.valueOf(1)");
+
+ block = LordOfTheStrings.returning(int.class).whenBoxedInteger("$T.valueOf(1)", Integer.class).whenInt("1")
+ .otherwise("😫").build();
+ assertThat(block).hasToString("return 1");
+ }
+
+ @Test // GH-3357
+ void shouldRenderConditionalBooleanReturn() {
+
+ CodeBlock block = LordOfTheStrings.returning(boolean.class).whenBoolean("$L", true).otherwise("😫").build();
+ assertThat(block).hasToString("return true");
+
+ block = LordOfTheStrings.returning(Boolean.class).whenBoolean("$L", true).otherwise("😫").build();
+ assertThat(block).hasToString("return true");
+ }
+
+ @Test // GH-3357
+ void shouldRenderConditionalNumericReturn() {
+
+ CodeBlock block = LordOfTheStrings.returning(boolean.class).number("someNumericVariable").otherwise("😫").build();
+ assertThat(block).hasToString("return 😫");
+
+ block = LordOfTheStrings.returning(long.class).number("someNumericVariable").otherwise("😫").build();
+ assertThat(block).hasToString("return someNumericVariable != null ? someNumericVariable.longValue() : 0L");
+
+ block = LordOfTheStrings.returning(Long.class).number("someNumericVariable").otherwise("😫").build();
+ assertThat(block).hasToString("return someNumericVariable != null ? someNumericVariable.longValue() : null");
+
+ block = LordOfTheStrings.returning(int.class).number("someNumericVariable").otherwise("😫").build();
+ assertThat(block).hasToString("return someNumericVariable != null ? someNumericVariable.intValue() : 0");
+
+ block = LordOfTheStrings.returning(Integer.class).number("someNumericVariable").otherwise("😫").build();
+ assertThat(block).hasToString("return someNumericVariable != null ? someNumericVariable.intValue() : null");
+ }
+
+ @Test // GH-3357
+ void shouldRenderConditionalOptional() {
+
+ CodeBlock block = LordOfTheStrings.returning(Optional.class).optional(CodeBlock.of("$S", "foo")).build();
+ assertThat(block).hasToString("return java.util.Optional.ofNullable(\"foo\")");
+
+ block = LordOfTheStrings.returning(String.class).optional(CodeBlock.of("$S", "foo")).build();
+ assertThat(block).hasToString("return \"foo\"");
+ }
+
+}
diff --git a/src/test/java/org/springframework/data/repository/aot/generate/AotRepositoryCreatorUnitTests.java b/src/test/java/org/springframework/data/repository/aot/generate/AotRepositoryCreatorUnitTests.java
index 3dbff067fe..5ed0352f7d 100644
--- a/src/test/java/org/springframework/data/repository/aot/generate/AotRepositoryCreatorUnitTests.java
+++ b/src/test/java/org/springframework/data/repository/aot/generate/AotRepositoryCreatorUnitTests.java
@@ -27,6 +27,7 @@
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
+import org.mockito.Answers;
import org.springframework.aot.generate.Generated;
import org.springframework.aot.hint.TypeReference;
@@ -153,7 +154,7 @@ void appliesQueryMethodContributor() {
repositoryCreator.contributeMethods((method) -> {
- return new MethodContributor<>(mock(QueryMethod.class), null) {
+ return new MethodContributor<>(mock(QueryMethod.class, Answers.RETURNS_MOCKS), null) {
@Override
public MethodSpec contribute(AotQueryMethodGenerationContext context) {