From 3ff4f6637baa898861b2f16b113acb9c436c57b6 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 11 Sep 2025 10:20:24 +0200 Subject: [PATCH 1/3] Prepare issue branch. --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 13143c9f6f..79c5849bd0 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-commons - 4.0.0-SNAPSHOT + 4.0.0-POET-SNAPSHOT Spring Data Core Core Spring concepts underpinning every Spring Data module. From 649eead804b9ee9fd9d83f251aa39793ca1c9cb0 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 11 Sep 2025 11:39:34 +0200 Subject: [PATCH 2/3] Add JavaPoet enhancements. Add opinionated builders for return statements and invocations, add introspection type for MethodReturns to reduce checks for e.g. `Optional` and `void` and utilities to construct type names. --- .../data/javapoet/LordOfTheStrings.java | 973 ++++++++++++++++++ .../data/javapoet/TypeNames.java | 106 ++ .../data/javapoet/package-info.java | 5 + .../AotQueryMethodGenerationContext.java | 16 + .../repository/aot/generate/MethodReturn.java | 196 ++++ .../AotRepositoryCreatorUnitTests.java | 3 +- 6 files changed, 1298 insertions(+), 1 deletion(-) create mode 100644 src/main/java/org/springframework/data/javapoet/LordOfTheStrings.java create mode 100644 src/main/java/org/springframework/data/javapoet/TypeNames.java create mode 100644 src/main/java/org/springframework/data/javapoet/package-info.java create mode 100644 src/main/java/org/springframework/data/repository/aot/generate/MethodReturn.java diff --git a/src/main/java/org/springframework/data/javapoet/LordOfTheStrings.java b/src/main/java/org/springframework/data/javapoet/LordOfTheStrings.java new file mode 100644 index 0000000000..37f3e34d76 --- /dev/null +++ b/src/main/java/org/springframework/data/javapoet/LordOfTheStrings.java @@ -0,0 +1,973 @@ +/* + * 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 java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Function; + +import org.jspecify.annotations.Nullable; + +import org.springframework.core.ResolvableType; +import org.springframework.data.util.ReflectionUtils; +import org.springframework.javapoet.CodeBlock; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; + +/** + * Utility class for generating Java code blocks using a fluent API. This class provides a structured and extensible + * programming model to simplify the creation of method calls, return statements, and complex code structures. It is + * designed to reduce conditional nesting and improve readability in code generation scenarios. + *

+ * Built on top of JavaPoet, this class introduces additional abstractions such as {@link CodeBlockBuilder}, + * {@link InvocationBuilder}, and {@link TypedReturnBuilder} to facilitate the construction of dynamic code blocks. + * These abstractions enable developers to create code with conditional logic, argument concatenation, and control flow + * in a declarative and intuitive manner. + *

+ * This class is intended for internal use within the framework and is not meant to be used directly by application + * developers. + * + * @author Mark Paluch + * @since 4.0 + */ +public abstract class LordOfTheStrings { + + /** + * Create a new {@code CodeBlockBuilder} instance. + * + * @return a new {@code CodeBlockBuilder}. + */ + public static CodeBlockBuilder builder() { + return new CodeBlockBuilder(CodeBlock.builder()); + } + + /** + * Create a new {@code CodeBlockBuilder} instance using the given {@link CodeBlock.Builder}. + * + * @param builder the {@link CodeBlock.Builder} to use. + * @return a new {@code CodeBlockBuilder}. + */ + public static CodeBlockBuilder builder(CodeBlock.Builder builder) { + return new CodeBlockBuilder(builder); + } + + /** + * Create a new {@code CodeBlockBuilder} instance with an initial format and arguments. + * + * @param format the format string. + * @param args the arguments for the format string. + * @return a new initialized {@code CodeBlockBuilder}. + */ + public static CodeBlockBuilder builder(String format, @Nullable Object... args) { + return new CodeBlockBuilder(CodeBlock.builder().add(format, args)); + } + + /** + * Create a {@link InvocationBuilder} for building method invocation code. + *

+ * The given {@code methodCall} may contain format specifiers as defined in Java Poet. It must additionally contain a + * format specifier (last position) that is used to expand the method arguments, for example: + * + *

+	 * Sort sort = …;
+	 * MethodCallBuilder method = PoetrySlam.method("$T.by($L)", Sort.class);
+	 *
+	 * method.arguments(sort, (order, builder) -> {
+	 * 	builder.add("$T.asc($S)", Sort.Order.class, order.getProperty());
+	 * });
+	 *
+	 * method.build();
+	 * 
+ * + * @param methodCall the invocation (or method call) format string. + * @param arguments the arguments for the method call. + * @return a new {@code MethodCallBuilder}. + */ + public static InvocationBuilder invoke(String methodCall, Object... arguments) { + return new InvocationBuilder(methodCall, arguments); + } + + /** + * Create a builder for a return statement targeting the given return type. + * + * @param returnType the method return type. + * @return a new {@code ReturnStatementBuilder}. + */ + public static TypedReturnBuilder returning(ResolvableType returnType) { + return new TypedReturnBuilder(returnType); + } + + /** + * Create a builder for a return statement targeting the given return type. + * + * @param returnType the method return type. + * @return a new {@code ReturnStatementBuilder}. + */ + public static TypedReturnBuilder returning(Class returnType) { + return new TypedReturnBuilder(ResolvableType.forType(returnType)); + } + + private LordOfTheStrings() { + // you shall not instantiate + } + + /** + * Builder to create method invocation code supporting argument concatenation. + */ + public static class InvocationBuilder { + + private final String name; + private final List nameArguments; + private final List arguments = new ArrayList<>(); + + InvocationBuilder(String name, Object... arguments) { + this.name = name; + this.nameArguments = List.of(arguments); + } + + /** + * Add a single argument to the method call. + * + * @param argument the argument to add. + * @return {@code this} builder. + */ + public InvocationBuilder argument(String argument) { + + Assert.hasText(argument, "Argument must not be null or empty"); + return argument("$L", argument); + } + + /** + * Add multiple arguments to the method call creating a literal for each argument. + * + * @param arguments the collection of arguments to add. + * @return {@code this} builder. + */ + public InvocationBuilder arguments(Iterable arguments) { + + for (Object argument : arguments) { + argument("$L", argument); + } + return this; + } + + /** + * Add multiple arguments to the method call, applying a builder customizer for each argument. + * + * @param arguments the iterable of arguments to add. + * @param consumer the consumer to apply to each argument. + * @param the type of the arguments. + * @return {@code this} builder. + */ + public InvocationBuilder arguments(Iterable arguments, Function consumer) { + + for (T argument : arguments) { + argument(consumer.apply(argument)); + } + + return this; + } + + /** + * Add a {@link CodeBlock} as an argument to the method call. + * + * @param argument the {@link CodeBlock} to add. + * @return {@code this} builder. + */ + public InvocationBuilder argument(CodeBlock argument) { + + Assert.notNull(argument, "CodeBlock must not be null"); + + if (argument.isEmpty()) { + return this; + } + + return argument("$L", argument); + } + + /** + * Add a formatted argument to the method call. + * + * @param format the format string. + * @param args the arguments for the format string. + * @return {@code this} builder. + */ + public InvocationBuilder argument(String format, @Nullable Object... args) { + + Assert.hasText(format, "Format must not be null or empty"); + this.arguments.add(new CodeTuple(format, args)); + return this; + } + + /** + * Build the {@link CodeBlock} representing the method call. The resulting CodeBlock can be used inline or as a + * {@link CodeBlock.Builder#addStatement(CodeBlock) statement}. + * + * @return the constructed {@link CodeBlock}. + */ + public CodeBlock build() { + + CodeBlock.Builder builder = CodeBlock.builder(); + buildCall(builder); + + return builder.build(); + } + + /** + * Build the {@link CodeBlock} representing the method call and assign it to the given variable, for example: + * + *
+		 * CodeBlock.Builder builder = …;
+		 * InvocationBuilder invoke = LordOfTheStrings.invoke("getJdbcOperations().update($L)", …);
+		 * builder.addStatement(invoke.assignTo("int $L", result));
+		 * 
+ * + * The resulting CodeBlock should be used as {@link CodeBlock.Builder#addStatement(CodeBlock) statement}. + * + * @param format the format string for the assignment. + * @param args the arguments for the format string. + * @return the constructed {@link CodeBlock}. + */ + public CodeBlock assignTo(String format, @Nullable Object... args) { + + CodeBlock.Builder builder = CodeBlock.builder(); + + builder.add(format.trim() + " = ", args); + buildCall(builder); + + return builder.build(); + } + + private void buildCall(CodeBlock.Builder builder) { + + boolean first = true; + + CodeBlock.Builder argsBuilder = CodeBlock.builder(); + for (CodeTuple argument : arguments) { + + if (first) { + first = false; + } else { + argsBuilder.add(", "); + } + + argsBuilder.add(argument.format(), argument.args()); + } + + List allArguments = new ArrayList<>(nameArguments); + + if (!argsBuilder.isEmpty()) { + 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...) + */ + 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) + */ + 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. + */ + public CodeBlockBuilder addStatement(Consumer consumer) { + + StatementBuilder statementBuilder = new StatementBuilder(); + consumer.accept(statementBuilder); + + if (!statementBuilder.isEmpty()) { + + this.add("$["); + + for (CodeTuple tuple : statementBuilder.tuples) { + builder.add(tuple.format(), tuple.args()); + } + + 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) + */ + 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...) + */ + 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) + */ + 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...) + */ + 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...) + */ + 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() + */ + 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...) + */ + 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() + */ + public CodeBlockBuilder indent() { + + builder.indent(); + return this; + } + + /** + * Unindent the current code block. + * + * @return {@code this} builder. + * @see CodeBlock.Builder#unindent() + */ + public CodeBlockBuilder unindent() { + + builder.unindent(); + return this; + } + + /** + * Build the {@link CodeBlock} from the current state of the builder. + * + * @return the constructed {@link CodeBlock}. + */ + public CodeBlock build() { + return builder.build(); + } + + /** + * Clear the current state of the builder. + * + * @return {@code this} builder. + */ + 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 tuples = new ArrayList<>(); + + /** + * Determine whether this builder is empty. + * + * @return {@code true} if the builder is empty; {@code false} otherwise. + */ + public boolean isEmpty() { + return tuples.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. + */ + public StatementBuilder add(String format, @Nullable Object... args) { + tuples.add(new CodeTuple(format, args)); + 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 builderCustomizer the consumer to apply to each element and {@link CodeBlockBuilder}. + * @param the type of the elements. + * @return {@code this} builder. + */ + public StatementBuilder addAll(Iterable elements, String delim, + BiConsumer builderCustomizer) { + return addAll(elements, t -> delim, builderCustomizer); + } + + /** + * 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 builderCustomizer the consumer to apply to each element and {@link CodeBlockBuilder}. + * @param the type of the elements. + * @return {@code this} builder. + */ + public StatementBuilder addAll(Iterable elements, Function delim, + BiConsumer builderCustomizer) { + + boolean first = true; + for (T element : elements) { + + if (first) { + first = false; + } else { + tuples.add(new CodeTuple(delim.apply(element))); + } + + CodeBlockBuilder builder = new CodeBlockBuilder(CodeBlock.builder()); + builderCustomizer.accept(element, builder); + + tuples.add(new CodeTuple("$L", builder.build())); + } + + 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. + */ + private 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. + */ + 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. + */ + 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} + */ + public CodeBlock build() { + + CodeBlock.Builder builder = CodeBlock.builder(); + + for (ReturnRule rule : rules) { + if (rule.condition()) { + builder.add("return"); + rule.accept(builder); + return builder.build(); + } + } + + for (ReturnRule rule : fallback) { + 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) { + + 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 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 + */ + private TypedReturnBuilder(ResolvableType returnType) { + + 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. + */ + 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) returning {@code returnName}. + * + * @param returnName the argument or variable name holding the result. + * @return {@code this} builder. + */ + public TypedReturnBuilder whenBooleanReturn(String returnName) { + return whenBoolean("$L", returnName); + } + + /** + * 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. + */ + 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. + */ + 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. + */ + 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. + */ + 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. + */ + 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. + */ + 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. + */ + 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. + */ + public TypedReturnBuilder when(Class returnType, String format, @Nullable Object... args) { + 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. + */ + 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. + */ + 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. + */ + public TypedReturnBuilder optional(String format, @Nullable Object... args) { + + if (Optional.class.isAssignableFrom(returnType.toClass())) { + + 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. + */ + 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. + */ + 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..efc55abfba --- /dev/null +++ b/src/main/java/org/springframework/data/repository/aot/generate/MethodReturn.java @@ -0,0 +1,196 @@ +/* + * 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.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; + Class returnClass = returnType.toClass(); + + this.typeName = TypeName.get(returnType.getType()); + this.className = TypeName.get(returnClass); + + 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 = TypeName.get(actualType.toResolvableType().getType()); + this.actualClassName = TypeName.get(actualType.getType()); + 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/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) { From e84f70b5367df982796bf7bcd5d985e1178ce5ff Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 11 Sep 2025 18:09:24 +0200 Subject: [PATCH 3/3] Polishing. Add tests, refine API contracts and reflect it in the documentation, remove methods that seem useful but are rather polluting the API. --- .../data/javapoet/LordOfTheStrings.java | 197 ++++++++++++----- .../repository/aot/generate/MethodReturn.java | 13 +- .../data/javapoet/JavaPoetUnitTests.java | 203 ++++++++++++++++++ 3 files changed, 353 insertions(+), 60 deletions(-) create mode 100644 src/test/java/org/springframework/data/javapoet/JavaPoetUnitTests.java diff --git a/src/main/java/org/springframework/data/javapoet/LordOfTheStrings.java b/src/main/java/org/springframework/data/javapoet/LordOfTheStrings.java index 37f3e34d76..6b924d26e5 100644 --- a/src/main/java/org/springframework/data/javapoet/LordOfTheStrings.java +++ b/src/main/java/org/springframework/data/javapoet/LordOfTheStrings.java @@ -19,28 +19,30 @@ import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Function; +import java.util.stream.Stream; import org.jspecify.annotations.Nullable; import org.springframework.core.ResolvableType; import org.springframework.data.util.ReflectionUtils; import org.springframework.javapoet.CodeBlock; +import org.springframework.lang.CheckReturnValue; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; /** - * Utility class for generating Java code blocks using a fluent API. This class provides a structured and extensible - * programming model to simplify the creation of method calls, return statements, and complex code structures. It is - * designed to reduce conditional nesting and improve readability in code generation scenarios. + * Utility class for creating Java code blocks using a fluent API. This class provides a structured and extensible + * programming model to simplify the creation of method calls, return statements, and complex code structures on top of + * JavaPoet. It is designed to reduce conditional nesting and improve readability in code generation scenarios. *

- * Built on top of JavaPoet, this class introduces additional abstractions such as {@link CodeBlockBuilder}, - * {@link InvocationBuilder}, and {@link TypedReturnBuilder} to facilitate the construction of dynamic code blocks. - * These abstractions enable developers to create code with conditional logic, argument concatenation, and control flow - * in a declarative and intuitive manner. + * This class introduces additional abstractions such as {@link CodeBlockBuilder}, {@link InvocationBuilder}, and + * {@link TypedReturnBuilder} to facilitate the construction of dynamic code blocks. These abstractions enable + * developers to create code with conditional logic, argument concatenation, and control flow in a declarative and + * intuitive manner. *

* This class is intended for internal use within the framework and is not meant to be used directly by application * developers. @@ -84,7 +86,8 @@ public static CodeBlockBuilder builder(String format, @Nullable Object... args) * Create a {@link InvocationBuilder} for building method invocation code. *

* The given {@code methodCall} may contain format specifiers as defined in Java Poet. It must additionally contain a - * format specifier (last position) that is used to expand the method arguments, for example: + * format specifier (last position) that is used to expand the method arguments when intending to provide arguments, + * for example: * *

 	 * Sort sort = …;
@@ -97,6 +100,11 @@ public static CodeBlockBuilder builder(String format, @Nullable Object... args)
 	 * method.build();
 	 * 
* + * The presence of arguments is detected when calling any {@link InvocationBuilder#argument} or + * {@link InvocationBuilder#arguments} method. Providing an empty {@link CodeBlock} or {@link Iterable} activates + * argument processing for easier handling when calling this static method. Note that the argument placeholder in + * {@code methodCall} must be omitted if no arguments are added. + * * @param methodCall the invocation (or method call) format string. * @param arguments the arguments for the method call. * @return a new {@code MethodCallBuilder}. @@ -106,7 +114,18 @@ public static InvocationBuilder invoke(String methodCall, Object... arguments) { } /** - * Create a builder for a return statement targeting the given return type. + * Create a builder for a return statement targeting the given return type. Any formats provided to + * {@link ReturnBuilderSupport} must not contain the {@code return} keyword as this will be included when building the + * resulting {@link CodeBlock}, for example: + * + *
+	 * Method method = …;
+	 * CodeBlock block = LordOfTheStrings.returning(ResolvableType.forMethodReturnType(method))
+	 *     .whenBoxedLong("$T.valueOf(1)", Long.class)
+	 *     .whenLong("1L")
+	 *     .otherwise("0L")
+	 *     .build();
+	 * 
* * @param returnType the method return type. * @return a new {@code ReturnStatementBuilder}. @@ -116,7 +135,18 @@ public static TypedReturnBuilder returning(ResolvableType returnType) { } /** - * Create a builder for a return statement targeting the given return type. + * Create a builder for a return statement targeting the given return type. Any formats provided to + * {@link ReturnBuilderSupport} must not contain the {@code return} keyword as this will be included when building the + * resulting {@link CodeBlock}, for example: + * + *
+	 * Method method = …;
+	 * CodeBlock block = LordOfTheStrings.returning(method.getReturnType())
+	 *     .whenBoxedLong("$T.valueOf(1)", Long.class)
+	 *     .whenLong("1L")
+	 *     .otherwise("0L")
+	 *     .build();
+	 * 
* * @param returnType the method return type. * @return a new {@code ReturnStatementBuilder}. @@ -137,6 +167,7 @@ public static class InvocationBuilder { private final String name; private final List nameArguments; private final List arguments = new ArrayList<>(); + private boolean hasArguments = false; InvocationBuilder(String name, Object... arguments) { this.name = name; @@ -144,11 +175,12 @@ public static class InvocationBuilder { } /** - * Add a single argument to the method call. + * Add a single argument as literal to the method call. * * @param argument the argument to add. * @return {@code this} builder. */ + @Contract("null ->fail; _ -> this") public InvocationBuilder argument(String argument) { Assert.hasText(argument, "Argument must not be null or empty"); @@ -161,11 +193,15 @@ public InvocationBuilder argument(String argument) { * @param arguments the collection of arguments to add. * @return {@code this} builder. */ + @Contract("_ -> this") public InvocationBuilder arguments(Iterable arguments) { + this.hasArguments = true; + for (Object argument : arguments) { argument("$L", argument); } + return this; } @@ -177,8 +213,11 @@ public InvocationBuilder arguments(Iterable arguments) { * @param the type of the arguments. * @return {@code this} builder. */ + @Contract("_, _ -> this") public InvocationBuilder arguments(Iterable arguments, Function consumer) { + this.hasArguments = true; + for (T argument : arguments) { argument(consumer.apply(argument)); } @@ -192,10 +231,13 @@ public InvocationBuilder arguments(Iterable arguments, Function * @param argument the {@link CodeBlock} to add. * @return {@code this} builder. */ + @Contract("null -> fail; _ -> this") public InvocationBuilder argument(CodeBlock argument) { Assert.notNull(argument, "CodeBlock must not be null"); + this.hasArguments = true; + if (argument.isEmpty()) { return this; } @@ -210,9 +252,12 @@ public InvocationBuilder argument(CodeBlock argument) { * @param args the arguments for the format string. * @return {@code this} builder. */ + @Contract("null, _ -> fail; _, _ -> this") public InvocationBuilder argument(String format, @Nullable Object... args) { Assert.hasText(format, "Format must not be null or empty"); + + this.hasArguments = true; this.arguments.add(new CodeTuple(format, args)); return this; } @@ -223,11 +268,11 @@ public InvocationBuilder argument(String format, @Nullable Object... args) { * * @return the constructed {@link CodeBlock}. */ + @CheckReturnValue public CodeBlock build() { CodeBlock.Builder builder = CodeBlock.builder(); buildCall(builder); - return builder.build(); } @@ -246,6 +291,7 @@ public CodeBlock build() { * @param args the arguments for the format string. * @return the constructed {@link CodeBlock}. */ + @CheckReturnValue public CodeBlock assignTo(String format, @Nullable Object... args) { CodeBlock.Builder builder = CodeBlock.builder(); @@ -274,7 +320,7 @@ private void buildCall(CodeBlock.Builder builder) { List allArguments = new ArrayList<>(nameArguments); - if (!argsBuilder.isEmpty()) { + if (hasArguments) { allArguments.add(argsBuilder.build()); } @@ -320,6 +366,7 @@ public boolean isEmpty() { * @return {@code this} builder. * @see CodeBlock.Builder#add(String, Object...) */ + @Contract("_, _ -> this") public CodeBlockBuilder add(String format, @Nullable Object... args) { builder.add(format, args); @@ -333,6 +380,7 @@ public CodeBlockBuilder add(String format, @Nullable Object... args) { * @return {@code this} builder. * @see CodeBlock.Builder#addStatement(CodeBlock) */ + @Contract("_ -> this") public CodeBlockBuilder addStatement(CodeBlock codeBlock) { builder.addStatement(codeBlock); @@ -345,8 +393,11 @@ public CodeBlockBuilder addStatement(CodeBlock codeBlock) { * @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); @@ -354,8 +405,8 @@ public CodeBlockBuilder addStatement(Consumer consumer) { this.add("$["); - for (CodeTuple tuple : statementBuilder.tuples) { - builder.add(tuple.format(), tuple.args()); + for (CodeBlock block : statementBuilder.blocks) { + builder.add(block); } this.add(";\n$]"); @@ -371,6 +422,7 @@ public CodeBlockBuilder addStatement(Consumer consumer) { * @return {@code this} builder. * @see CodeBlock.Builder#addStatement(CodeBlock) */ + @Contract("_ -> this") public CodeBlockBuilder add(CodeBlock codeBlock) { builder.add(codeBlock); @@ -385,6 +437,7 @@ public CodeBlockBuilder add(CodeBlock codeBlock) { * @return {@code this} builder. * @see CodeBlock.Builder#addStatement(String, Object...) */ + @Contract("_, _ -> this") public CodeBlockBuilder addStatement(String format, @Nullable Object... args) { builder.addStatement(format, args); @@ -399,6 +452,7 @@ public CodeBlockBuilder addStatement(String format, @Nullable Object... args) { * @return {@code this} builder. * @see CodeBlock.Builder#addNamed(String, Map) */ + @Contract("_, _ -> this") public CodeBlockBuilder addNamed(String format, Map arguments) { builder.addNamed(format, arguments); @@ -413,6 +467,7 @@ public CodeBlockBuilder addNamed(String format, Map arguments) { * @return {@code this} builder. * @see CodeBlock.Builder#beginControlFlow(String, Object...) */ + @Contract("_, _ -> this") public CodeBlockBuilder beginControlFlow(String controlFlow, @Nullable Object... args) { builder.beginControlFlow(controlFlow, args); @@ -427,6 +482,7 @@ public CodeBlockBuilder beginControlFlow(String controlFlow, @Nullable Object... * @return {@code this} builder. * @see CodeBlock.Builder#endControlFlow(String, Object...) */ + @Contract("_, _ -> this") public CodeBlockBuilder endControlFlow(String controlFlow, @Nullable Object... args) { builder.endControlFlow(controlFlow, args); @@ -439,6 +495,7 @@ public CodeBlockBuilder endControlFlow(String controlFlow, @Nullable Object... a * @return {@code this} builder. * @see CodeBlock.Builder#endControlFlow() */ + @Contract("-> this") public CodeBlockBuilder endControlFlow() { builder.endControlFlow(); @@ -453,6 +510,7 @@ public CodeBlockBuilder endControlFlow() { * @return {@code this} builder. * @see CodeBlock.Builder#nextControlFlow(String, Object...) */ + @Contract("_, _ -> this") public CodeBlockBuilder nextControlFlow(String controlFlow, @Nullable Object... args) { builder.nextControlFlow(controlFlow, args); @@ -465,6 +523,7 @@ public CodeBlockBuilder nextControlFlow(String controlFlow, @Nullable Object... * @return {@code this} builder. * @see CodeBlock.Builder#indent() */ + @Contract("-> this") public CodeBlockBuilder indent() { builder.indent(); @@ -477,6 +536,7 @@ public CodeBlockBuilder indent() { * @return {@code this} builder. * @see CodeBlock.Builder#unindent() */ + @Contract("-> this") public CodeBlockBuilder unindent() { builder.unindent(); @@ -488,6 +548,7 @@ public CodeBlockBuilder unindent() { * * @return the constructed {@link CodeBlock}. */ + @CheckReturnValue public CodeBlock build() { return builder.build(); } @@ -497,6 +558,7 @@ public CodeBlock build() { * * @return {@code this} builder. */ + @Contract("-> this") public CodeBlockBuilder clear() { builder.clear(); @@ -516,7 +578,9 @@ public CodeBlockBuilder clear() { */ public static class StatementBuilder { - private final List tuples = new ArrayList<>(); + private final List blocks = new ArrayList<>(); + + StatementBuilder() {} /** * Determine whether this builder is empty. @@ -524,7 +588,7 @@ public static class StatementBuilder { * @return {@code true} if the builder is empty; {@code false} otherwise. */ public boolean isEmpty() { - return tuples.isEmpty(); + return blocks.isEmpty(); } /** @@ -547,7 +611,7 @@ public ConditionalStatementStep whenNot(boolean state) { return (format, args) -> { - if (state) { + if (!state) { add(format, args); } return this; @@ -561,8 +625,22 @@ public ConditionalStatementStep whenNot(boolean state) { * @param args the arguments for the format string. * @return {@code this} builder. */ + @Contract("_, _ -> this") public StatementBuilder add(String format, @Nullable Object... args) { - tuples.add(new CodeTuple(format, 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; } @@ -571,13 +649,14 @@ public StatementBuilder add(String format, @Nullable Object... args) { * * @param elements the elements to concatenate. * @param delim the delimiter to use between elements. - * @param builderCustomizer the consumer to apply to each element and {@link CodeBlockBuilder}. + * @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 elements, String delim, - BiConsumer builderCustomizer) { - return addAll(elements, t -> delim, builderCustomizer); + Function mapper) { + return addAll(elements, t -> delim, mapper); } /** @@ -586,12 +665,15 @@ public StatementBuilder addAll(Iterable elements, String delim, * @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 builderCustomizer the consumer to apply to each element and {@link CodeBlockBuilder}. + * @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 elements, Function delim, - BiConsumer builderCustomizer) { + Function mapper) { + + Assert.notNull(elements, "Elements must not be null"); boolean first = true; for (T element : elements) { @@ -599,13 +681,10 @@ public StatementBuilder addAll(Iterable elements, Function this") public ReturnBuilderSupport when(boolean condition, String format, @Nullable Object... args) { this.rules.add(ruleOf(condition, format, args)); return this; @@ -661,6 +743,7 @@ public ReturnBuilderSupport when(boolean condition, String format, @Nullable Obj * @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; @@ -682,19 +765,13 @@ ReturnBuilderSupport otherwise(Consumer builderConsumer) { * * @return the resulting {@code CodeBlock} */ + @CheckReturnValue public CodeBlock build() { CodeBlock.Builder builder = CodeBlock.builder(); - for (ReturnRule rule : rules) { - if (rule.condition()) { - builder.add("return"); - rule.accept(builder); - return builder.build(); - } - } - - for (ReturnRule rule : fallback) { + for (ReturnRule rule : (Iterable) () -> Stream + .concat(this.rules.stream(), this.fallback.stream()).iterator()) { if (rule.condition()) { builder.add("return"); rule.accept(builder); @@ -715,6 +792,8 @@ public CodeBlock build() { */ 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)); } @@ -746,8 +825,8 @@ public void accept(CodeBlock.Builder builder) { } /** - * 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)}. + * 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 { @@ -758,7 +837,9 @@ public static class TypedReturnBuilder extends ReturnBuilderSupport { * * @param returnType the method return type */ - private TypedReturnBuilder(ResolvableType returnType) { + TypedReturnBuilder(ResolvableType returnType) { + + Assert.notNull(returnType, "Return type must not be null"); this.returnType = returnType; @@ -775,6 +856,7 @@ private TypedReturnBuilder(ResolvableType returnType) { * @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) @@ -782,16 +864,6 @@ public TypedReturnBuilder number(String resultToReturn) { .whenInt("$1L != null ? $1L.intValue() : 0", resultToReturn); } - /** - * Add a return statement if the return type is boolean (primitive or box type) returning {@code returnName}. - * - * @param returnName the argument or variable name holding the result. - * @return {@code this} builder. - */ - public TypedReturnBuilder whenBooleanReturn(String returnName) { - return whenBoolean("$L", returnName); - } - /** * Add a return statement if the return type is boolean (primitive or box type). * @@ -799,6 +871,7 @@ public TypedReturnBuilder whenBooleanReturn(String returnName) { * @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); @@ -811,6 +884,7 @@ public TypedReturnBuilder whenBoolean(String format, @Nullable Object... args) { * @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); } @@ -822,6 +896,7 @@ public TypedReturnBuilder whenBoxedLong(String format, @Nullable Object... args) * @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); } @@ -833,6 +908,7 @@ public TypedReturnBuilder whenLong(String format, @Nullable Object... args) { * @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); } @@ -844,6 +920,7 @@ public TypedReturnBuilder whenBoxedInteger(String format, @Nullable Object... ar * @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); } @@ -856,7 +933,9 @@ public TypedReturnBuilder whenInt(String format, @Nullable Object... args) { * @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); } @@ -869,6 +948,7 @@ public TypedReturnBuilder whenBoxed(Class primitiveOrWrapper, String format, * @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); @@ -885,7 +965,10 @@ public TypedReturnBuilder whenPrimitiveOrBoxed(Class primitiveType, String fo * @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); } @@ -897,6 +980,8 @@ public TypedReturnBuilder when(Class returnType, String format, @Nullable Obj * @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; @@ -909,6 +994,7 @@ public TypedReturnBuilder when(boolean condition, String format, @Nullable Objec * @param codeBlock the code block result to be returned. * @return {@code this} builder. */ + @Contract("_ -> this") public TypedReturnBuilder optional(CodeBlock codeBlock) { return optional("$L", codeBlock); } @@ -921,10 +1007,12 @@ public TypedReturnBuilder optional(CodeBlock codeBlock) { * @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)); } @@ -948,6 +1036,7 @@ public TypedReturnBuilder optional(String format, @Nullable Object... args) { * @param codeBlock the code block result to be returned. * @return {@code this} builder. */ + @Contract("_ -> this") public TypedReturnBuilder otherwise(CodeBlock codeBlock) { return otherwise("$L", codeBlock); } @@ -959,6 +1048,8 @@ public TypedReturnBuilder otherwise(CodeBlock codeBlock) { * @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; 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 index efc55abfba..c8b4c4f296 100644 --- a/src/main/java/org/springframework/data/repository/aot/generate/MethodReturn.java +++ b/src/main/java/org/springframework/data/repository/aot/generate/MethodReturn.java @@ -19,6 +19,7 @@ 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; @@ -60,21 +61,19 @@ public MethodReturn(ReturnedType returnedType, ResolvableType returnType) { this.returnedType = returnedType; this.returnType = returnType; - Class returnClass = returnType.toClass(); - - this.typeName = TypeName.get(returnType.getType()); - this.className = TypeName.get(returnClass); + 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 = TypeName.get(actualType.toResolvableType().getType()); - this.actualClassName = TypeName.get(actualType.getType()); + this.actualTypeName = TypeNames.typeName(this.actualType); + this.actualClassName = TypeNames.className(this.actualType); this.actualReturnClass = actualType.getType(); } else { this.actualType = returnType; 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\""); + } + +}