Extract value code generation to make it reusable

This commit introduces ValueCodeGenerator and its Delegate interface
as a way to generate the code for a particular value. Implementations
in spring-core provides support for common value types such a String,
primitives, Collections, etc.

Additional implementations are provided for code generation of bean
definition property values.

Closes gh-28999
This commit is contained in:
Stéphane Nicoll
2023-12-13 07:05:50 +01:00
parent 75da9c3c47
commit 3c2c9ca186
14 changed files with 1479 additions and 833 deletions

View File

@@ -0,0 +1,33 @@
/*
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.aot.generate;
/**
* Thrown when a {@link ValueCodeGenerator} could not generate the code for a
* given value.
*
* @author Stephane Nicoll
* @since 6.1.2
*/
@SuppressWarnings("serial")
public class UnsupportedTypeValueCodeGenerationException extends ValueCodeGenerationException {
public UnsupportedTypeValueCodeGenerationException(Object value) {
super("Code generation does not support " + value.getClass().getName(), value, null);
}
}

View File

@@ -0,0 +1,61 @@
/*
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.aot.generate;
import org.springframework.lang.Nullable;
/**
* Thrown when value code generation fails.
*
* @author Stephane Nicoll
* @since 6.1.2
*/
@SuppressWarnings("serial")
public class ValueCodeGenerationException extends RuntimeException {
@Nullable
private final Object value;
protected ValueCodeGenerationException(String message, @Nullable Object value, @Nullable Throwable cause) {
super(message, cause);
this.value = value;
}
public ValueCodeGenerationException(@Nullable Object value, Throwable cause) {
super(buildErrorMessage(value), cause);
this.value = value;
}
private static String buildErrorMessage(@Nullable Object value) {
StringBuilder message = new StringBuilder("Failed to generate code for '");
message.append(value).append("'");
if (value != null) {
message.append(" with type ").append(value.getClass());
}
return message.toString();
}
/**
* Return the value that failed to be generated.
* @return the value
*/
@Nullable
public Object getValue() {
return this.value;
}
}

View File

@@ -0,0 +1,152 @@
/*
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.aot.generate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.springframework.javapoet.CodeBlock;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* Code generator for a single value. Delegates code generation to a list of
* configurable {@link Delegate} implementations.
*
* @author Stephane Nicoll
* @since 6.1.2
*/
public final class ValueCodeGenerator {
private static final ValueCodeGenerator INSTANCE = new ValueCodeGenerator(ValueCodeGeneratorDelegates.INSTANCES, null);
private static final CodeBlock NULL_VALUE_CODE_BLOCK = CodeBlock.of("null");
private final List<Delegate> delegates;
@Nullable
private final GeneratedMethods generatedMethods;
private ValueCodeGenerator(List<Delegate> delegates, @Nullable GeneratedMethods generatedMethods) {
this.delegates = delegates;
this.generatedMethods = generatedMethods;
}
/**
* Return an instance that provides support for {@linkplain
* ValueCodeGeneratorDelegates#INSTANCES common value types}.
* @return an instance with support for common value types
*/
public static ValueCodeGenerator withDefaults() {
return INSTANCE;
}
/**
* Create an instance with the specified {@link Delegate} implementations.
* @param delegates the delegates to use
* @return an instance with the specified delegates
*/
public static ValueCodeGenerator with(Delegate... delegates) {
return with(Arrays.asList(delegates));
}
/**
* Create an instance with the specified {@link Delegate} implementations.
* @param delegates the delegates to use
* @return an instance with the specified delegates
*/
public static ValueCodeGenerator with(List<Delegate> delegates) {
Assert.notEmpty(delegates, "Delegates must not be empty");
return new ValueCodeGenerator(new ArrayList<>(delegates), null);
}
public ValueCodeGenerator add(List<Delegate> additionalDelegates) {
Assert.notEmpty(additionalDelegates, "AdditionalDelegates must not be empty");
List<Delegate> allDelegates = new ArrayList<>(this.delegates);
allDelegates.addAll(additionalDelegates);
return new ValueCodeGenerator(allDelegates, this.generatedMethods);
}
/**
* Return a {@link ValueCodeGenerator} that is scoped for the specified
* {@link GeneratedMethods}. This allows code generation to generate
* additional methods if necessary, or perform some optimization in
* case of visibility issues.
* @param generatedMethods the generated methods to use
* @return an instance scoped to the specified generated methods
*/
public ValueCodeGenerator scoped(GeneratedMethods generatedMethods) {
return new ValueCodeGenerator(this.delegates, generatedMethods);
}
/**
* Generate the code that represents the specified {@code value}.
* @param value the value to generate
* @return the code that represents the specified value
*/
public CodeBlock generateCode(@Nullable Object value) {
if (value == null) {
return NULL_VALUE_CODE_BLOCK;
}
try {
for (Delegate delegate : this.delegates) {
CodeBlock code = delegate.generateCode(this, value);
if (code != null) {
return code;
}
}
throw new UnsupportedTypeValueCodeGenerationException(value);
}
catch (Exception ex) {
throw new ValueCodeGenerationException(value, ex);
}
}
/**
* Return the {@link GeneratedMethods} that represents the scope
* in which code generated by this instance will be added, or
* {@code null} if no specific scope is set.
* @return the generated methods to use for code generation
*/
@Nullable
public GeneratedMethods getGeneratedMethods() {
return this.generatedMethods;
}
/**
* Strategy interface that can be used to implement code generation for a
* particular value type.
*/
public interface Delegate {
/**
* Generate the code for the specified non-null {@code value}. If this
* instance does not support the value, it should return {@code null} to
* indicate so.
* @param valueCodeGenerator the code generator to use for embedded values
* @param value the value to generate
* @return the code that represents the specified value or {@code null} if
* the specified value is not supported.
*/
@Nullable
CodeBlock generateCode(ValueCodeGenerator valueCodeGenerator, Object value);
}
}

View File

@@ -0,0 +1,418 @@
/*
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.aot.generate;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.stream.Stream;
import org.springframework.aot.generate.ValueCodeGenerator.Delegate;
import org.springframework.core.ResolvableType;
import org.springframework.javapoet.CodeBlock;
import org.springframework.javapoet.CodeBlock.Builder;
import org.springframework.lang.Nullable;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
/**
* Code generator {@link Delegate} for well known value types.
*
* @author Stephane Nicoll
* @since 6.1.2
*/
public abstract class ValueCodeGeneratorDelegates {
/**
* Return the {@link Delegate} implementations for common value types.
* These are:
* <ul>
* <li>Primitive types,</li>
* <li>String,</li>
* <li>Charset,</li>
* <li>Enum,</li>
* <li>Class,</li>
* <li>{@link ResolvableType},</li>
* <li>Array,</li>
* <li>List via {@code List.of},</li>
* <li>Set via {@code Set.of} and support of {@link LinkedHashSet},</li>
* <li>Map via {@code Map.of} or {@code Map.ofEntries}.</li>
* </ul>
* Those implementations do not require the {@link ValueCodeGenerator} to be
* {@linkplain ValueCodeGenerator#scoped(GeneratedMethods) scoped}.
*/
public static final List<Delegate> INSTANCES = List.of(
new PrimitiveDelegate(),
new StringDelegate(),
new CharsetDelegate(),
new EnumDelegate(),
new ClassDelegate(),
new ResolvableTypeDelegate(),
new ArrayDelegate(),
new ListDelegate(),
new SetDelegate(),
new MapDelegate());
/**
* Abstract {@link Delegate} for {@code Collection} types.
* @param <T> type the collection type
*/
public abstract static class CollectionDelegate<T extends Collection<?>> implements Delegate {
private final Class<?> collectionType;
private final CodeBlock emptyResult;
protected CollectionDelegate(Class<?> collectionType, CodeBlock emptyResult) {
this.collectionType = collectionType;
this.emptyResult = emptyResult;
}
@Override
@SuppressWarnings("unchecked")
public CodeBlock generateCode(ValueCodeGenerator valueCodeGenerator, Object value) {
if (this.collectionType.isInstance(value)) {
T collection = (T) value;
if (collection.isEmpty()) {
return this.emptyResult;
}
return generateCollectionCode(valueCodeGenerator, collection);
}
return null;
}
protected CodeBlock generateCollectionCode(ValueCodeGenerator valueCodeGenerator, T collection) {
return generateCollectionOf(valueCodeGenerator, collection, this.collectionType);
}
protected final CodeBlock generateCollectionOf(ValueCodeGenerator valueCodeGenerator,
Collection<?> collection, Class<?> collectionType) {
Builder code = CodeBlock.builder();
code.add("$T.of(", collectionType);
Iterator<?> iterator = collection.iterator();
while (iterator.hasNext()) {
Object element = iterator.next();
code.add("$L", valueCodeGenerator.generateCode(element));
if (iterator.hasNext()) {
code.add(", ");
}
}
code.add(")");
return code.build();
}
}
/**
* {@link Delegate} for {@link Map} types.
*/
public static class MapDelegate implements Delegate {
private static final CodeBlock EMPTY_RESULT = CodeBlock.of("$T.emptyMap()", Collections.class);
@Override
public CodeBlock generateCode(ValueCodeGenerator valueCodeGenerator, Object value) {
if (value instanceof Map<?, ?> map) {
if (map.isEmpty()) {
return EMPTY_RESULT;
}
return generateMapCode(valueCodeGenerator, map);
}
return null;
}
/**
* Generate the code for a non-empty {@link Map}.
* @param valueCodeGenerator the code generator to use for embedded values
* @param map the value to generate
* @return the code that represents the specified map or {@code null} if
* the specified map is not supported.
*/
@Nullable
protected CodeBlock generateMapCode(ValueCodeGenerator valueCodeGenerator, Map<?, ?> map) {
map = orderForCodeConsistency(map);
boolean useOfEntries = map.size() > 10;
CodeBlock.Builder code = CodeBlock.builder();
code.add("$T" + ((!useOfEntries) ? ".of(" : ".ofEntries("), Map.class);
Iterator<? extends Entry<?, ?>> iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
Entry<?, ?> entry = iterator.next();
CodeBlock keyCode = valueCodeGenerator.generateCode(entry.getKey());
CodeBlock valueCode = valueCodeGenerator.generateCode(entry.getValue());
if (!useOfEntries) {
code.add("$L, $L", keyCode, valueCode);
}
else {
code.add("$T.entry($L,$L)", Map.class, keyCode, valueCode);
}
if (iterator.hasNext()) {
code.add(", ");
}
}
code.add(")");
return code.build();
}
private <K, V> Map<K, V> orderForCodeConsistency(Map<K, V> map) {
try {
return new TreeMap<>(map);
}
catch (ClassCastException ex) {
// If elements are not comparable, just keep the original map
return map;
}
}
}
/**
* {@link Delegate} for {@code primitive} types.
*/
private static class PrimitiveDelegate implements Delegate {
private static final Map<Character, String> CHAR_ESCAPES = Map.of(
'\b', "\\b",
'\t', "\\t",
'\n', "\\n",
'\f', "\\f",
'\r', "\\r",
'\"', "\"",
'\'', "\\'",
'\\', "\\\\"
);
@Override
@Nullable
public CodeBlock generateCode(ValueCodeGenerator codeGenerator, Object value) {
if (value instanceof Boolean || value instanceof Integer) {
return CodeBlock.of("$L", value);
}
if (value instanceof Byte) {
return CodeBlock.of("(byte) $L", value);
}
if (value instanceof Short) {
return CodeBlock.of("(short) $L", value);
}
if (value instanceof Long) {
return CodeBlock.of("$LL", value);
}
if (value instanceof Float) {
return CodeBlock.of("$LF", value);
}
if (value instanceof Double) {
return CodeBlock.of("(double) $L", value);
}
if (value instanceof Character character) {
return CodeBlock.of("'$L'", escape(character));
}
return null;
}
private String escape(char ch) {
String escaped = CHAR_ESCAPES.get(ch);
if (escaped != null) {
return escaped;
}
return (!Character.isISOControl(ch)) ? Character.toString(ch)
: String.format("\\u%04x", (int) ch);
}
}
/**
* {@link Delegate} for {@link String} types.
*/
private static class StringDelegate implements Delegate {
@Override
@Nullable
public CodeBlock generateCode(ValueCodeGenerator codeGenerator, Object value) {
if (value instanceof String) {
return CodeBlock.of("$S", value);
}
return null;
}
}
/**
* {@link Delegate} for {@link Charset} types.
*/
private static class CharsetDelegate implements Delegate {
@Override
@Nullable
public CodeBlock generateCode(ValueCodeGenerator codeGenerator, Object value) {
if (value instanceof Charset charset) {
return CodeBlock.of("$T.forName($S)", Charset.class, charset.name());
}
return null;
}
}
/**
* {@link Delegate} for {@link Enum} types.
*/
private static class EnumDelegate implements Delegate {
@Override
@Nullable
public CodeBlock generateCode(ValueCodeGenerator codeGenerator, Object value) {
if (value instanceof Enum<?> enumValue) {
return CodeBlock.of("$T.$L", enumValue.getDeclaringClass(),
enumValue.name());
}
return null;
}
}
/**
* {@link Delegate} for {@link Class} types.
*/
private static class ClassDelegate implements Delegate {
@Override
@Nullable
public CodeBlock generateCode(ValueCodeGenerator codeGenerator, Object value) {
if (value instanceof Class<?> clazz) {
return CodeBlock.of("$T.class", ClassUtils.getUserClass(clazz));
}
return null;
}
}
/**
* {@link Delegate} for {@link ResolvableType} types.
*/
private static class ResolvableTypeDelegate implements Delegate {
@Override
@Nullable
public CodeBlock generateCode(ValueCodeGenerator codeGenerator, Object value) {
if (value instanceof ResolvableType resolvableType) {
return generateCode(resolvableType, false);
}
return null;
}
private static CodeBlock generateCode(ResolvableType resolvableType, boolean allowClassResult) {
if (ResolvableType.NONE.equals(resolvableType)) {
return CodeBlock.of("$T.NONE", ResolvableType.class);
}
Class<?> type = ClassUtils.getUserClass(resolvableType.toClass());
if (resolvableType.hasGenerics() && !resolvableType.hasUnresolvableGenerics()) {
return generateCodeWithGenerics(resolvableType, type);
}
if (allowClassResult) {
return CodeBlock.of("$T.class", type);
}
return CodeBlock.of("$T.forClass($T.class)", ResolvableType.class, type);
}
private static CodeBlock generateCodeWithGenerics(ResolvableType target, Class<?> type) {
ResolvableType[] generics = target.getGenerics();
boolean hasNoNestedGenerics = Arrays.stream(generics).noneMatch(ResolvableType::hasGenerics);
CodeBlock.Builder code = CodeBlock.builder();
code.add("$T.forClassWithGenerics($T.class", ResolvableType.class, type);
for (ResolvableType generic : generics) {
code.add(", $L", generateCode(generic, hasNoNestedGenerics));
}
code.add(")");
return code.build();
}
}
/**
* {@link Delegate} for {@code array} types.
*/
private static class ArrayDelegate implements Delegate {
@Override
@Nullable
public CodeBlock generateCode(ValueCodeGenerator codeGenerator, Object value) {
if (value.getClass().isArray()) {
Stream<CodeBlock> elements = Arrays.stream(ObjectUtils.toObjectArray(value))
.map(codeGenerator::generateCode);
CodeBlock.Builder code = CodeBlock.builder();
code.add("new $T {", value.getClass());
code.add(elements.collect(CodeBlock.joining(", ")));
code.add("}");
return code.build();
}
return null;
}
}
/**
* {@link Delegate} for {@link List} types.
*/
private static class ListDelegate extends CollectionDelegate<List<?>> {
ListDelegate() {
super(List.class, CodeBlock.of("$T.emptyList()", Collections.class));
}
}
/**
* {@link Delegate} for {@link Set} types.
*/
private static class SetDelegate extends CollectionDelegate<Set<?>> {
SetDelegate() {
super(Set.class, CodeBlock.of("$T.emptySet()", Collections.class));
}
@Override
protected CodeBlock generateCollectionCode(ValueCodeGenerator valueCodeGenerator, Set<?> collection) {
if (collection instanceof LinkedHashSet) {
return CodeBlock.of("new $T($L)", LinkedHashSet.class,
generateCollectionOf(valueCodeGenerator, collection, List.class));
}
return super.generateCollectionCode(valueCodeGenerator,
orderForCodeConsistency(collection));
}
private Set<?> orderForCodeConsistency(Set<?> set) {
try {
return new TreeSet<Object>(set);
}
catch (ClassCastException ex) {
// If elements are not comparable, just keep the original set
return set;
}
}
}
}

View File

@@ -0,0 +1,499 @@
/*
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.aot.generate;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.StringWriter;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.time.temporal.ChronoUnit;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.assertj.core.api.AbstractAssert;
import org.assertj.core.api.AssertProvider;
import org.assertj.core.api.StringAssert;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.mockito.InOrder;
import org.springframework.aot.generate.ValueCodeGenerator.Delegate;
import org.springframework.core.ResolvableType;
import org.springframework.core.testfixture.aot.generate.value.EnumWithClassBody;
import org.springframework.core.testfixture.aot.generate.value.ExampleClass;
import org.springframework.core.testfixture.aot.generate.value.ExampleClass$$GeneratedBy;
import org.springframework.javapoet.ClassName;
import org.springframework.javapoet.CodeBlock;
import org.springframework.javapoet.FieldSpec;
import org.springframework.javapoet.JavaFile;
import org.springframework.javapoet.TypeSpec;
import org.springframework.lang.Nullable;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
/**
* Tests for {@link ValueCodeGenerator}.
*
* @author Stephane Nicoll
*/
class ValueCodeGeneratorTests {
@Nested
class ConfigurationTests {
@Test
void createWithListOfDelegatesInvokeThemInOrder() {
Delegate first = mock(Delegate.class);
Delegate second = mock(Delegate.class);
Delegate third = mock(Delegate.class);
ValueCodeGenerator codeGenerator = ValueCodeGenerator
.with(List.of(first, second, third));
Object value = "";
given(third.generateCode(codeGenerator, value))
.willReturn(CodeBlock.of("test"));
CodeBlock code = codeGenerator.generateCode(value);
assertThat(code).hasToString("test");
InOrder ordered = inOrder(first, second, third);
ordered.verify(first).generateCode(codeGenerator, value);
ordered.verify(second).generateCode(codeGenerator, value);
ordered.verify(third).generateCode(codeGenerator, value);
}
@Test
void generateCodeWithMatchingDelegateStops() {
Delegate first = mock(Delegate.class);
Delegate second = mock(Delegate.class);
ValueCodeGenerator codeGenerator = ValueCodeGenerator
.with(List.of(first, second));
Object value = "";
given(first.generateCode(codeGenerator, value))
.willReturn(CodeBlock.of("test"));
CodeBlock code = codeGenerator.generateCode(value);
assertThat(code).hasToString("test");
verify(first).generateCode(codeGenerator, value);
verifyNoInteractions(second);
}
@Test
void scopedReturnsImmutableCopy() {
ValueCodeGenerator valueCodeGenerator = ValueCodeGenerator.withDefaults();
GeneratedMethods generatedMethods = new GeneratedMethods(
ClassName.get("com.example", "Test"), MethodName::toString);
ValueCodeGenerator scopedValueCodeGenerator = valueCodeGenerator.scoped(generatedMethods);
assertThat(scopedValueCodeGenerator).isNotSameAs(valueCodeGenerator);
assertThat(scopedValueCodeGenerator.getGeneratedMethods()).isSameAs(generatedMethods);
assertThat(valueCodeGenerator.getGeneratedMethods()).isNull();
}
}
@Nested
class NullTests {
@Test
void generateWhenNull() {
assertThat(generateCode(null)).hasToString("null");
}
}
@Nested
class PrimitiveTests {
@Test
void generateWhenBoolean() {
assertThat(generateCode(true)).hasToString("true");
}
@Test
void generateWhenByte() {
assertThat(generateCode((byte) 2)).hasToString("(byte) 2");
}
@Test
void generateWhenShort() {
assertThat(generateCode((short) 3)).hasToString("(short) 3");
}
@Test
void generateWhenInt() {
assertThat(generateCode(4)).hasToString("4");
}
@Test
void generateWhenLong() {
assertThat(generateCode(5L)).hasToString("5L");
}
@Test
void generateWhenFloat() {
assertThat(generateCode(0.1F)).hasToString("0.1F");
}
@Test
void generateWhenDouble() {
assertThat(generateCode(0.2)).hasToString("(double) 0.2");
}
@Test
void generateWhenChar() {
assertThat(generateCode('a')).hasToString("'a'");
}
@Test
void generateWhenSimpleEscapedCharReturnsEscaped() {
testEscaped('\b', "'\\b'");
testEscaped('\t', "'\\t'");
testEscaped('\n', "'\\n'");
testEscaped('\f', "'\\f'");
testEscaped('\r', "'\\r'");
testEscaped('\"', "'\"'");
testEscaped('\'', "'\\''");
testEscaped('\\', "'\\\\'");
}
@Test
void generatedWhenUnicodeEscapedCharReturnsEscaped() {
testEscaped('\u007f', "'\\u007f'");
}
private void testEscaped(char value, String expectedSourceContent) {
assertThat(generateCode(value)).hasToString(expectedSourceContent);
}
}
@Nested
class StringTests {
@Test
void generateWhenString() {
assertThat(generateCode("test")).hasToString("\"test\"");
}
@Test
void generateWhenStringWithCarriageReturn() {
assertThat(generateCode("test\n")).isEqualTo(CodeBlock.of("$S", "test\n"));
}
}
@Nested
class CharsetTests {
@Test
void generateWhenCharset() {
assertThat(resolve(generateCode(StandardCharsets.UTF_8))).hasImport(Charset.class)
.hasValueCode("Charset.forName(\"UTF-8\")");
}
}
@Nested
class EnumTests {
@Test
void generateWhenEnum() {
assertThat(resolve(generateCode(ChronoUnit.DAYS)))
.hasImport(ChronoUnit.class).hasValueCode("ChronoUnit.DAYS");
}
@Test
void generateWhenEnumWithClassBody() {
assertThat(resolve(generateCode(EnumWithClassBody.TWO)))
.hasImport(EnumWithClassBody.class).hasValueCode("EnumWithClassBody.TWO");
}
}
@Nested
class ClassTests {
@Test
void generateWhenClass() {
assertThat(resolve(generateCode(InputStream.class)))
.hasImport(InputStream.class).hasValueCode("InputStream.class");
}
@Test
void generateWhenCglibClass() {
assertThat(resolve(generateCode(ExampleClass$$GeneratedBy.class)))
.hasImport(ExampleClass.class).hasValueCode("ExampleClass.class");
}
}
@Nested
class ResolvableTypeTests {
@Test
void generateWhenSimpleResolvableType() {
ResolvableType resolvableType = ResolvableType.forClass(String.class);
assertThat(resolve(generateCode(resolvableType)))
.hasImport(ResolvableType.class)
.hasValueCode("ResolvableType.forClass(String.class)");
}
@Test
void generateWhenNoneResolvableType() {
ResolvableType resolvableType = ResolvableType.NONE;
assertThat(resolve(generateCode(resolvableType)))
.hasImport(ResolvableType.class).hasValueCode("ResolvableType.NONE");
}
@Test
void generateWhenGenericResolvableType() {
ResolvableType resolvableType = ResolvableType
.forClassWithGenerics(List.class, String.class);
assertThat(resolve(generateCode(resolvableType)))
.hasImport(ResolvableType.class, List.class)
.hasValueCode("ResolvableType.forClassWithGenerics(List.class, String.class)");
}
@Test
void generateWhenNestedGenericResolvableType() {
ResolvableType stringList = ResolvableType.forClassWithGenerics(List.class,
String.class);
ResolvableType resolvableType = ResolvableType.forClassWithGenerics(Map.class,
ResolvableType.forClass(Integer.class), stringList);
assertThat(resolve(generateCode(resolvableType)))
.hasImport(ResolvableType.class, List.class, Map.class).hasValueCode(
"ResolvableType.forClassWithGenerics(Map.class, ResolvableType.forClass(Integer.class), "
+ "ResolvableType.forClassWithGenerics(List.class, String.class))");
}
}
@Nested
class ArrayTests {
@Test
void generateWhenPrimitiveArray() {
int[] array = { 0, 1, 2 };
assertThat(generateCode(array)).hasToString("new int[] {0, 1, 2}");
}
@Test
void generateWhenWrapperArray() {
Integer[] array = { 0, 1, 2 };
assertThat(resolve(generateCode(array))).hasValueCode("new Integer[] {0, 1, 2}");
}
@Test
void generateWhenClassArray() {
Class<?>[] array = new Class<?>[] { InputStream.class, OutputStream.class };
assertThat(resolve(generateCode(array))).hasImport(InputStream.class, OutputStream.class)
.hasValueCode("new Class[] {InputStream.class, OutputStream.class}");
}
}
@Nested
class ListTests {
@Test
void generateWhenStringList() {
List<String> list = List.of("a", "b", "c");
assertThat(resolve(generateCode(list))).hasImport(List.class)
.hasValueCode("List.of(\"a\", \"b\", \"c\")");
}
@Test
void generateWhenEmptyList() {
List<String> list = List.of();
assertThat(resolve(generateCode(list))).hasImport(Collections.class)
.hasValueCode("Collections.emptyList()");
}
}
@Nested
class SetTests {
@Test
void generateWhenStringSet() {
Set<String> set = Set.of("a", "b", "c");
assertThat(resolve(generateCode(set))).hasImport(Set.class)
.hasValueCode("Set.of(\"a\", \"b\", \"c\")");
}
@Test
void generateWhenEmptySet() {
Set<String> set = Set.of();
assertThat(resolve(generateCode(set))).hasImport(Collections.class)
.hasValueCode("Collections.emptySet()");
}
@Test
void generateWhenLinkedHashSet() {
Set<String> set = new LinkedHashSet<>(List.of("a", "b", "c"));
assertThat(resolve(generateCode(set))).hasImport(List.class, LinkedHashSet.class)
.hasValueCode("new LinkedHashSet(List.of(\"a\", \"b\", \"c\"))");
}
@Test
void generateWhenSetOfClass() {
Set<Class<?>> set = Set.of(InputStream.class, OutputStream.class);
assertThat(resolve(generateCode(set))).hasImport(Set.class, InputStream.class, OutputStream.class)
.valueCode().contains("Set.of(", "InputStream.class", "OutputStream.class");
}
}
@Nested
class MapTests {
@Test
void generateWhenSmallMap() {
Map<String, String> map = Map.of("k1", "v1", "k2", "v2");
assertThat(resolve(generateCode(map))).hasImport(Map.class)
.hasValueCode("Map.of(\"k1\", \"v1\", \"k2\", \"v2\")");
}
@Test
void generateWhenMapWithOverTenElements() {
Map<String, String> map = new HashMap<>();
for (int i = 1; i <= 11; i++) {
map.put("k" + i, "v" + i);
}
assertThat(resolve(generateCode(map))).hasImport(Map.class)
.valueCode().startsWith("Map.ofEntries(");
}
}
@Nested
class ExceptionTests {
@Test
void generateWhenUnsupportedValue() {
StringWriter sw = new StringWriter();
assertThatExceptionOfType(ValueCodeGenerationException.class)
.isThrownBy(() -> generateCode(sw))
.withCauseInstanceOf(UnsupportedTypeValueCodeGenerationException.class)
.satisfies(ex -> assertThat(ex.getValue()).isEqualTo(sw));
}
@Test
void generateWhenUnsupportedDataTypeThrowsException() {
StringWriter sampleValue = new StringWriter();
assertThatExceptionOfType(ValueCodeGenerationException.class).isThrownBy(() -> generateCode(sampleValue))
.withMessageContaining("Failed to generate code for")
.withMessageContaining(sampleValue.toString())
.withMessageContaining(StringWriter.class.getName())
.havingCause()
.withMessageContaining("Code generation does not support")
.withMessageContaining(StringWriter.class.getName());
}
@Test
void generateWhenListOfUnsupportedElement() {
StringWriter one = new StringWriter();
StringWriter two = new StringWriter();
List<StringWriter> list = List.of(one, two);
assertThatExceptionOfType(ValueCodeGenerationException.class).isThrownBy(() -> generateCode(list))
.withMessageContaining("Failed to generate code for")
.withMessageContaining(list.toString())
.withMessageContaining(list.getClass().getName())
.havingCause()
.withMessageContaining("Failed to generate code for")
.withMessageContaining(one.toString())
.withMessageContaining(StringWriter.class.getName())
.havingCause()
.withMessageContaining("Code generation does not support " + StringWriter.class.getName());
}
}
private static CodeBlock generateCode(@Nullable Object value) {
return ValueCodeGenerator.withDefaults().generateCode(value);
}
private static ValueCode resolve(CodeBlock valueCode) {
String code = writeCode(valueCode);
List<String> imports = code.lines()
.filter(candidate -> candidate.startsWith("import") && candidate.endsWith(";"))
.map(line -> line.substring("import".length(), line.length() - 1))
.map(String::trim).toList();
int start = code.indexOf("value = ");
int end = code.indexOf(";", start);
return new ValueCode(code.substring(start + "value = ".length(), end), imports);
}
private static String writeCode(CodeBlock valueCode) {
FieldSpec field = FieldSpec.builder(Object.class, "value")
.initializer(valueCode)
.build();
TypeSpec helloWorld = TypeSpec.classBuilder("Test").addField(field).build();
JavaFile javaFile = JavaFile.builder("com.example", helloWorld).build();
StringWriter out = new StringWriter();
try {
javaFile.writeTo(out);
}
catch (IOException ex) {
throw new IllegalStateException(ex);
}
return out.toString();
}
static class ValueCodeAssert extends AbstractAssert<ValueCodeAssert, ValueCode> {
public ValueCodeAssert(ValueCode actual) {
super(actual, ValueCodeAssert.class);
}
ValueCodeAssert hasImport(Class<?>... imports) {
for (Class<?> anImport : imports) {
assertThat(this.actual.imports).contains(anImport.getName());
}
return this;
}
ValueCodeAssert hasValueCode(String code) {
assertThat(this.actual.code).isEqualTo(code);
return this;
}
StringAssert valueCode() {
return new StringAssert(this.actual.code);
}
}
record ValueCode(String code, List<String> imports) implements AssertProvider<ValueCodeAssert> {
@Override
public ValueCodeAssert assertThat() {
return new ValueCodeAssert(this);
}
}
}

View File

@@ -0,0 +1,42 @@
/*
* Copyright 2002-2023 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.core.testfixture.aot.generate.value;
/**
* Test enum that include a class body.
*
* @author Phillip Webb
*/
public enum EnumWithClassBody {
/**
* No class body.
*/
ONE,
/**
* With class body.
*/
TWO {
@Override
public String toString() {
return "2";
}
}
}

View File

@@ -0,0 +1,26 @@
/*
* Copyright 2002-2023 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.core.testfixture.aot.generate.value;
/**
* Fake CGLIB generated class.
*
* @author Phillip Webb
*/
public class ExampleClass$$GeneratedBy extends ExampleClass {
}

View File

@@ -0,0 +1,26 @@
/*
* Copyright 2002-2023 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.core.testfixture.aot.generate.value;
/**
* Public example class used for test.
*
* @author Phillip Webb
*/
public class ExampleClass {
}