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

@ -34,6 +34,9 @@ import java.util.function.Function;
import java.util.function.Predicate; import java.util.function.Predicate;
import org.springframework.aot.generate.GeneratedMethods; import org.springframework.aot.generate.GeneratedMethods;
import org.springframework.aot.generate.ValueCodeGenerator;
import org.springframework.aot.generate.ValueCodeGenerator.Delegate;
import org.springframework.aot.generate.ValueCodeGeneratorDelegates;
import org.springframework.aot.hint.ExecutableMode; import org.springframework.aot.hint.ExecutableMode;
import org.springframework.aot.hint.MemberCategory; import org.springframework.aot.hint.MemberCategory;
import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.RuntimeHints;
@ -89,7 +92,7 @@ class BeanDefinitionPropertiesCodeGenerator {
private final Predicate<String> attributeFilter; private final Predicate<String> attributeFilter;
private final BeanDefinitionPropertyValueCodeGenerator valueCodeGenerator; private final ValueCodeGenerator valueCodeGenerator;
BeanDefinitionPropertiesCodeGenerator(RuntimeHints hints, BeanDefinitionPropertiesCodeGenerator(RuntimeHints hints,
@ -98,8 +101,11 @@ class BeanDefinitionPropertiesCodeGenerator {
this.hints = hints; this.hints = hints;
this.attributeFilter = attributeFilter; this.attributeFilter = attributeFilter;
this.valueCodeGenerator = new BeanDefinitionPropertyValueCodeGenerator(generatedMethods, this.valueCodeGenerator = ValueCodeGenerator
(object, type) -> customValueCodeGenerator.apply(PropertyNamesStack.peek(), object)); .with(new ValueCodeGeneratorDelegateAdapter(customValueCodeGenerator))
.add(BeanDefinitionPropertyValueCodeGeneratorDelegates.INSTANCES)
.add(ValueCodeGeneratorDelegates.INSTANCES)
.scoped(generatedMethods);
} }
@ -366,6 +372,22 @@ class BeanDefinitionPropertiesCodeGenerator {
return (castNecessary ? CodeBlock.of("($T) $L", castType, valueCode) : valueCode); return (castNecessary ? CodeBlock.of("($T) $L", castType, valueCode) : valueCode);
} }
static class ValueCodeGeneratorDelegateAdapter implements Delegate {
private final BiFunction<String, Object, CodeBlock> customValueCodeGenerator;
ValueCodeGeneratorDelegateAdapter(BiFunction<String, Object, CodeBlock> customValueCodeGenerator) {
this.customValueCodeGenerator = customValueCodeGenerator;
}
@Override
public CodeBlock generateCode(ValueCodeGenerator valueCodeGenerator, Object value) {
return this.customValueCodeGenerator.apply(PropertyNamesStack.peek(), value);
}
}
static class PropertyNamesStack { static class PropertyNamesStack {
private static final ThreadLocal<ArrayDeque<String>> threadLocal = ThreadLocal.withInitial(ArrayDeque::new); private static final ThreadLocal<ArrayDeque<String>> threadLocal = ThreadLocal.withInitial(ArrayDeque::new);
@ -384,7 +406,6 @@ class BeanDefinitionPropertiesCodeGenerator {
String value = threadLocal.get().peek(); String value = threadLocal.get().peek();
return ("".equals(value) ? null : value); return ("".equals(value) ? null : value);
} }
} }
} }

View File

@ -1,600 +0,0 @@
/*
* 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.beans.factory.aot;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
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.function.BiFunction;
import java.util.stream.Stream;
import org.springframework.aot.generate.GeneratedMethod;
import org.springframework.aot.generate.GeneratedMethods;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanReference;
import org.springframework.beans.factory.config.RuntimeBeanReference;
import org.springframework.beans.factory.config.TypedStringValue;
import org.springframework.beans.factory.support.ManagedList;
import org.springframework.beans.factory.support.ManagedMap;
import org.springframework.beans.factory.support.ManagedSet;
import org.springframework.core.ResolvableType;
import org.springframework.javapoet.AnnotationSpec;
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;
/**
* Internal code generator used to generate code for a single value contained in
* a {@link BeanDefinition} property.
*
* @author Stephane Nicoll
* @author Phillip Webb
* @author Sebastien Deleuze
* @since 6.0
*/
class BeanDefinitionPropertyValueCodeGenerator {
static final CodeBlock NULL_VALUE_CODE_BLOCK = CodeBlock.of("null");
private final GeneratedMethods generatedMethods;
private final List<Delegate> delegates;
BeanDefinitionPropertyValueCodeGenerator(GeneratedMethods generatedMethods,
@Nullable BiFunction<Object, ResolvableType, CodeBlock> customValueGenerator) {
this.generatedMethods = generatedMethods;
this.delegates = new ArrayList<>();
if (customValueGenerator != null) {
this.delegates.add(customValueGenerator::apply);
}
this.delegates.addAll(List.of(
new PrimitiveDelegate(),
new StringDelegate(),
new CharsetDelegate(),
new EnumDelegate(),
new ClassDelegate(),
new ResolvableTypeDelegate(),
new ArrayDelegate(),
new ManagedListDelegate(),
new ManagedSetDelegate(),
new ManagedMapDelegate(),
new ListDelegate(),
new SetDelegate(),
new MapDelegate(),
new BeanReferenceDelegate(),
new TypedStringValueDelegate()
));
}
CodeBlock generateCode(@Nullable Object value) {
ResolvableType type = ResolvableType.forInstance(value);
try {
return generateCode(value, type);
}
catch (Exception ex) {
throw new IllegalArgumentException(buildErrorMessage(value, type), ex);
}
}
private CodeBlock generateCodeForElement(@Nullable Object value, ResolvableType type) {
try {
return generateCode(value, type);
}
catch (Exception ex) {
throw new IllegalArgumentException(buildErrorMessage(value, type), ex);
}
}
private static String buildErrorMessage(@Nullable Object value, ResolvableType type) {
StringBuilder message = new StringBuilder("Failed to generate code for '");
message.append(value).append("'");
if (type != ResolvableType.NONE) {
message.append(" with type ").append(type);
}
return message.toString();
}
private CodeBlock generateCode(@Nullable Object value, ResolvableType type) {
if (value == null) {
return NULL_VALUE_CODE_BLOCK;
}
for (Delegate delegate : this.delegates) {
CodeBlock code = delegate.generateCode(value, type);
if (code != null) {
return code;
}
}
throw new IllegalArgumentException("Code generation does not support " + type);
}
/**
* Internal delegate used to support generation for a specific type.
*/
@FunctionalInterface
private interface Delegate {
@Nullable
CodeBlock generateCode(Object value, ResolvableType type);
}
/**
* {@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(Object value, ResolvableType type) {
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(Object value, ResolvableType type) {
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(Object value, ResolvableType type) {
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(Object value, ResolvableType type) {
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(Object value, ResolvableType type) {
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(Object value, ResolvableType type) {
if (value instanceof ResolvableType resolvableType) {
return ResolvableTypeCodeGenerator.generateCode(resolvableType);
}
return null;
}
}
/**
* {@link Delegate} for {@code array} types.
*/
private class ArrayDelegate implements Delegate {
@Override
@Nullable
public CodeBlock generateCode(@Nullable Object value, ResolvableType type) {
if (type.isArray()) {
ResolvableType componentType = type.getComponentType();
Stream<CodeBlock> elements = Arrays.stream(ObjectUtils.toObjectArray(value)).map(component ->
BeanDefinitionPropertyValueCodeGenerator.this.generateCode(component, componentType));
CodeBlock.Builder code = CodeBlock.builder();
code.add("new $T {", type.toClass());
code.add(elements.collect(CodeBlock.joining(", ")));
code.add("}");
return code.build();
}
return null;
}
}
/**
* Abstract {@link Delegate} for {@code Collection} types.
*/
private abstract class CollectionDelegate<T extends Collection<?>> implements Delegate {
private final Class<?> collectionType;
private final CodeBlock emptyResult;
public CollectionDelegate(Class<?> collectionType, CodeBlock emptyResult) {
this.collectionType = collectionType;
this.emptyResult = emptyResult;
}
@SuppressWarnings("unchecked")
@Override
@Nullable
public CodeBlock generateCode(Object value, ResolvableType type) {
if (this.collectionType.isInstance(value)) {
T collection = (T) value;
if (collection.isEmpty()) {
return this.emptyResult;
}
ResolvableType elementType = type.as(this.collectionType).getGeneric();
return generateCollectionCode(elementType, collection);
}
return null;
}
protected CodeBlock generateCollectionCode(ResolvableType elementType, T collection) {
return generateCollectionOf(collection, this.collectionType, elementType);
}
protected final CodeBlock generateCollectionOf(Collection<?> collection,
Class<?> collectionType, ResolvableType elementType) {
Builder code = CodeBlock.builder();
code.add("$T.of(", collectionType);
Iterator<?> iterator = collection.iterator();
while (iterator.hasNext()) {
Object element = iterator.next();
code.add("$L", BeanDefinitionPropertyValueCodeGenerator.this
.generateCodeForElement(element, elementType));
if (iterator.hasNext()) {
code.add(", ");
}
}
code.add(")");
return code.build();
}
}
/**
* {@link Delegate} for {@link ManagedList} types.
*/
private class ManagedListDelegate extends CollectionDelegate<ManagedList<?>> {
public ManagedListDelegate() {
super(ManagedList.class, CodeBlock.of("new $T()", ManagedList.class));
}
}
/**
* {@link Delegate} for {@link ManagedSet} types.
*/
private class ManagedSetDelegate extends CollectionDelegate<ManagedSet<?>> {
public ManagedSetDelegate() {
super(ManagedSet.class, CodeBlock.of("new $T()", ManagedSet.class));
}
}
/**
* {@link Delegate} for {@link ManagedMap} types.
*/
private class ManagedMapDelegate implements Delegate {
private static final CodeBlock EMPTY_RESULT = CodeBlock.of("$T.ofEntries()", ManagedMap.class);
@Override
@Nullable
public CodeBlock generateCode(Object value, ResolvableType type) {
if (value instanceof ManagedMap<?, ?> managedMap) {
return generateManagedMapCode(type, managedMap);
}
return null;
}
private <K, V> CodeBlock generateManagedMapCode(ResolvableType type, ManagedMap<K, V> managedMap) {
if (managedMap.isEmpty()) {
return EMPTY_RESULT;
}
ResolvableType keyType = type.as(Map.class).getGeneric(0);
ResolvableType valueType = type.as(Map.class).getGeneric(1);
CodeBlock.Builder code = CodeBlock.builder();
code.add("$T.ofEntries(", ManagedMap.class);
Iterator<Map.Entry<K, V>> iterator = managedMap.entrySet().iterator();
while (iterator.hasNext()) {
Entry<?, ?> entry = iterator.next();
code.add("$T.entry($L,$L)", Map.class,
BeanDefinitionPropertyValueCodeGenerator.this
.generateCodeForElement(entry.getKey(), keyType),
BeanDefinitionPropertyValueCodeGenerator.this
.generateCodeForElement(entry.getValue(), valueType));
if (iterator.hasNext()) {
code.add(", ");
}
}
code.add(")");
return code.build();
}
}
/**
* {@link Delegate} for {@link List} types.
*/
private class ListDelegate extends CollectionDelegate<List<?>> {
ListDelegate() {
super(List.class, CodeBlock.of("$T.emptyList()", Collections.class));
}
}
/**
* {@link Delegate} for {@link Set} types.
*/
private class SetDelegate extends CollectionDelegate<Set<?>> {
SetDelegate() {
super(Set.class, CodeBlock.of("$T.emptySet()", Collections.class));
}
@Override
protected CodeBlock generateCollectionCode(ResolvableType elementType, Set<?> set) {
if (set instanceof LinkedHashSet) {
return CodeBlock.of("new $T($L)", LinkedHashSet.class,
generateCollectionOf(set, List.class, elementType));
}
return super.generateCollectionCode(elementType, orderForCodeConsistency(set));
}
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;
}
}
}
/**
* {@link Delegate} for {@link Map} types.
*/
private class MapDelegate implements Delegate {
private static final CodeBlock EMPTY_RESULT = CodeBlock.of("$T.emptyMap()", Collections.class);
@Override
@Nullable
public CodeBlock generateCode(Object value, ResolvableType type) {
if (value instanceof Map<?, ?> map) {
return generateMapCode(type, map);
}
return null;
}
private <K, V> CodeBlock generateMapCode(ResolvableType type, Map<K, V> map) {
if (map.isEmpty()) {
return EMPTY_RESULT;
}
ResolvableType keyType = type.as(Map.class).getGeneric(0);
ResolvableType valueType = type.as(Map.class).getGeneric(1);
if (map instanceof LinkedHashMap<?, ?>) {
return generateLinkedHashMapCode(map, keyType, valueType);
}
map = orderForCodeConsistency(map);
boolean useOfEntries = map.size() > 10;
CodeBlock.Builder code = CodeBlock.builder();
code.add("$T" + ((!useOfEntries) ? ".of(" : ".ofEntries("), Map.class);
Iterator<Map.Entry<K, V>> iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
Entry<K, V> entry = iterator.next();
CodeBlock keyCode = BeanDefinitionPropertyValueCodeGenerator.this
.generateCodeForElement(entry.getKey(), keyType);
CodeBlock valueCode = BeanDefinitionPropertyValueCodeGenerator.this
.generateCodeForElement(entry.getValue(), valueType);
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;
}
}
private <K, V> CodeBlock generateLinkedHashMapCode(Map<K, V> map,
ResolvableType keyType, ResolvableType valueType) {
GeneratedMethods generatedMethods = BeanDefinitionPropertyValueCodeGenerator.this.generatedMethods;
GeneratedMethod generatedMethod = generatedMethods.add("getMap", method -> {
method.addAnnotation(AnnotationSpec
.builder(SuppressWarnings.class)
.addMember("value", "{\"rawtypes\", \"unchecked\"}")
.build());
method.returns(Map.class);
method.addStatement("$T map = new $T($L)", Map.class,
LinkedHashMap.class, map.size());
map.forEach((key, value) -> method.addStatement("map.put($L, $L)",
BeanDefinitionPropertyValueCodeGenerator.this
.generateCodeForElement(key, keyType),
BeanDefinitionPropertyValueCodeGenerator.this
.generateCodeForElement(value, valueType)));
method.addStatement("return map");
});
return CodeBlock.of("$L()", generatedMethod.getName());
}
}
/**
* {@link Delegate} for {@link BeanReference} types.
*/
private static class BeanReferenceDelegate implements Delegate {
@Override
@Nullable
public CodeBlock generateCode(Object value, ResolvableType type) {
if (value instanceof RuntimeBeanReference runtimeBeanReference &&
runtimeBeanReference.getBeanType() != null) {
return CodeBlock.of("new $T($T.class)", RuntimeBeanReference.class,
runtimeBeanReference.getBeanType());
}
else if (value instanceof BeanReference beanReference) {
return CodeBlock.of("new $T($S)", RuntimeBeanReference.class,
beanReference.getBeanName());
}
return null;
}
}
/**
* {@link Delegate} for {@link TypedStringValue} types.
*/
private class TypedStringValueDelegate implements Delegate {
@Override
public CodeBlock generateCode(Object value, ResolvableType type) {
if (value instanceof TypedStringValue typedStringValue) {
return generateTypeStringValueCode(typedStringValue);
}
return null;
}
private CodeBlock generateTypeStringValueCode(TypedStringValue typedStringValue) {
String value = typedStringValue.getValue();
if (typedStringValue.hasTargetType()) {
return CodeBlock.of("new $T($S, $L)", TypedStringValue.class, value,
generateCode(typedStringValue.getTargetType()));
}
return generateCode(value);
}
private CodeBlock generateCode(@Nullable Object value) {
return BeanDefinitionPropertyValueCodeGenerator.this.generateCode(value);
}
}
}

View File

@ -0,0 +1,212 @@
/*
* 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.beans.factory.aot;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.springframework.aot.generate.GeneratedMethod;
import org.springframework.aot.generate.GeneratedMethods;
import org.springframework.aot.generate.ValueCodeGenerator;
import org.springframework.aot.generate.ValueCodeGenerator.Delegate;
import org.springframework.aot.generate.ValueCodeGeneratorDelegates;
import org.springframework.aot.generate.ValueCodeGeneratorDelegates.CollectionDelegate;
import org.springframework.aot.generate.ValueCodeGeneratorDelegates.MapDelegate;
import org.springframework.beans.factory.config.BeanReference;
import org.springframework.beans.factory.config.RuntimeBeanReference;
import org.springframework.beans.factory.config.TypedStringValue;
import org.springframework.beans.factory.support.ManagedList;
import org.springframework.beans.factory.support.ManagedMap;
import org.springframework.beans.factory.support.ManagedSet;
import org.springframework.javapoet.AnnotationSpec;
import org.springframework.javapoet.CodeBlock;
/**
* Code generator {@link Delegate} for common bean definition property values.
*
* @author Stephane Nicoll
* @since 6.1.2
*/
abstract class BeanDefinitionPropertyValueCodeGeneratorDelegates {
/**
* Return the {@link Delegate} implementations for common bean definition
* property value types. These are:
* <ul>
* <li>{@link ManagedList},</li>
* <li>{@link ManagedSet},</li>
* <li>{@link ManagedMap},</li>
* <li>{@link LinkedHashMap},</li>
* <li>{@link BeanReference},</li>
* <li>{@link TypedStringValue}.</li>
* </ul>
* When combined with {@linkplain ValueCodeGeneratorDelegates#INSTANCES the
* delegates for common value types}, this should be added first as they have
* special handling for list, set, and map.
*/
public static final List<Delegate> INSTANCES = List.of(
new ManagedListDelegate(),
new ManagedSetDelegate(),
new ManagedMapDelegate(),
new LinkedHashMapDelegate(),
new BeanReferenceDelegate(),
new TypedStringValueDelegate()
);
/**
* {@link Delegate} for {@link ManagedList} types.
*/
private static class ManagedListDelegate extends CollectionDelegate<ManagedList<?>> {
public ManagedListDelegate() {
super(ManagedList.class, CodeBlock.of("new $T()", ManagedList.class));
}
}
/**
* {@link Delegate} for {@link ManagedSet} types.
*/
private static class ManagedSetDelegate extends CollectionDelegate<ManagedSet<?>> {
public ManagedSetDelegate() {
super(ManagedSet.class, CodeBlock.of("new $T()", ManagedSet.class));
}
}
/**
* {@link Delegate} for {@link ManagedMap} types.
*/
private static class ManagedMapDelegate implements Delegate {
private static final CodeBlock EMPTY_RESULT = CodeBlock.of("$T.ofEntries()", ManagedMap.class);
@Override
public CodeBlock generateCode(ValueCodeGenerator valueCodeGenerator, Object value) {
if (value instanceof ManagedMap<?, ?> managedMap) {
return generateManagedMapCode(valueCodeGenerator, managedMap);
}
return null;
}
private <K, V> CodeBlock generateManagedMapCode(ValueCodeGenerator valueCodeGenerator,
ManagedMap<K, V> managedMap) {
if (managedMap.isEmpty()) {
return EMPTY_RESULT;
}
CodeBlock.Builder code = CodeBlock.builder();
code.add("$T.ofEntries(", ManagedMap.class);
Iterator<Entry<K, V>> iterator = managedMap.entrySet().iterator();
while (iterator.hasNext()) {
Entry<?, ?> entry = iterator.next();
code.add("$T.entry($L,$L)", Map.class,
valueCodeGenerator.generateCode(entry.getKey()),
valueCodeGenerator.generateCode(entry.getValue()));
if (iterator.hasNext()) {
code.add(", ");
}
}
code.add(")");
return code.build();
}
}
/**
* {@link Delegate} for {@link Map} types.
*/
private static class LinkedHashMapDelegate extends MapDelegate {
@Override
protected CodeBlock generateMapCode(ValueCodeGenerator valueCodeGenerator, Map<?, ?> map) {
GeneratedMethods generatedMethods = valueCodeGenerator.getGeneratedMethods();
if (map instanceof LinkedHashMap<?, ?> && generatedMethods != null) {
return generateLinkedHashMapCode(valueCodeGenerator, generatedMethods, map);
}
return super.generateMapCode(valueCodeGenerator, map);
}
private CodeBlock generateLinkedHashMapCode(ValueCodeGenerator valueCodeGenerator,
GeneratedMethods generatedMethods, Map<?, ?> map) {
GeneratedMethod generatedMethod = generatedMethods.add("getMap", method -> {
method.addAnnotation(AnnotationSpec
.builder(SuppressWarnings.class)
.addMember("value", "{\"rawtypes\", \"unchecked\"}")
.build());
method.returns(Map.class);
method.addStatement("$T map = new $T($L)", Map.class,
LinkedHashMap.class, map.size());
map.forEach((key, value) -> method.addStatement("map.put($L, $L)",
valueCodeGenerator.generateCode(key),
valueCodeGenerator.generateCode(value)));
method.addStatement("return map");
});
return CodeBlock.of("$L()", generatedMethod.getName());
}
}
/**
* {@link Delegate} for {@link BeanReference} types.
*/
private static class BeanReferenceDelegate implements Delegate {
@Override
public CodeBlock generateCode(ValueCodeGenerator valueCodeGenerator, Object value) {
if (value instanceof RuntimeBeanReference runtimeBeanReference &&
runtimeBeanReference.getBeanType() != null) {
return CodeBlock.of("new $T($T.class)", RuntimeBeanReference.class,
runtimeBeanReference.getBeanType());
}
else if (value instanceof BeanReference beanReference) {
return CodeBlock.of("new $T($S)", RuntimeBeanReference.class,
beanReference.getBeanName());
}
return null;
}
}
/**
* {@link Delegate} for {@link TypedStringValue} types.
*/
private static class TypedStringValueDelegate implements Delegate {
@Override
public CodeBlock generateCode(ValueCodeGenerator valueCodeGenerator, Object value) {
if (value instanceof TypedStringValue typedStringValue) {
return generateTypeStringValueCode(valueCodeGenerator, typedStringValue);
}
return null;
}
private CodeBlock generateTypeStringValueCode(ValueCodeGenerator valueCodeGenerator, TypedStringValue typedStringValue) {
String value = typedStringValue.getValue();
if (typedStringValue.hasTargetType()) {
return CodeBlock.of("new $T($S, $L)", TypedStringValue.class, value,
valueCodeGenerator.generateCode(typedStringValue.getTargetType()));
}
return valueCodeGenerator.generateCode(value);
}
}
}

View File

@ -27,6 +27,7 @@ import org.springframework.aot.generate.AccessControl;
import org.springframework.aot.generate.GenerationContext; import org.springframework.aot.generate.GenerationContext;
import org.springframework.aot.generate.MethodReference; import org.springframework.aot.generate.MethodReference;
import org.springframework.aot.generate.MethodReference.ArgumentCodeGenerator; import org.springframework.aot.generate.MethodReference.ArgumentCodeGenerator;
import org.springframework.aot.generate.ValueCodeGenerator;
import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanDefinitionHolder; import org.springframework.beans.factory.config.BeanDefinitionHolder;
@ -51,6 +52,8 @@ import org.springframework.util.function.SingletonSupplier;
*/ */
class DefaultBeanRegistrationCodeFragments implements BeanRegistrationCodeFragments { class DefaultBeanRegistrationCodeFragments implements BeanRegistrationCodeFragments {
private static final ValueCodeGenerator valueCodeGenerator = ValueCodeGenerator.withDefaults();
private final BeanRegistrationsCode beanRegistrationsCode; private final BeanRegistrationsCode beanRegistrationsCode;
private final RegisteredBean registeredBean; private final RegisteredBean registeredBean;
@ -147,9 +150,9 @@ class DefaultBeanRegistrationCodeFragments implements BeanRegistrationCodeFragme
private CodeBlock generateBeanTypeCode(ResolvableType beanType) { private CodeBlock generateBeanTypeCode(ResolvableType beanType) {
if (!beanType.hasGenerics()) { if (!beanType.hasGenerics()) {
return CodeBlock.of("$T.class", ClassUtils.getUserClass(beanType.toClass())); return valueCodeGenerator.generateCode(ClassUtils.getUserClass(beanType.toClass()));
} }
return ResolvableTypeCodeGenerator.generateCode(beanType); return valueCodeGenerator.generateCode(beanType);
} }
private boolean targetTypeNecessary(ResolvableType beanType, @Nullable Class<?> beanClass) { private boolean targetTypeNecessary(ResolvableType beanType, @Nullable Class<?> beanClass) {

View File

@ -1,69 +0,0 @@
/*
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.beans.factory.aot;
import java.util.Arrays;
import org.springframework.core.ResolvableType;
import org.springframework.javapoet.CodeBlock;
import org.springframework.util.ClassUtils;
/**
* Internal code generator used to support {@link ResolvableType}.
*
* @author Stephane Nicoll
* @author Phillip Webb
* @since 6.0
*/
final class ResolvableTypeCodeGenerator {
private ResolvableTypeCodeGenerator() {
}
public static CodeBlock generateCode(ResolvableType resolvableType) {
return generateCode(resolvableType, false);
}
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();
}
}

View File

@ -36,6 +36,8 @@ import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.aot.generate.GeneratedClass; import org.springframework.aot.generate.GeneratedClass;
import org.springframework.aot.generate.ValueCodeGenerator;
import org.springframework.aot.generate.ValueCodeGeneratorDelegates;
import org.springframework.aot.test.generate.TestGenerationContext; import org.springframework.aot.test.generate.TestGenerationContext;
import org.springframework.beans.factory.config.BeanReference; import org.springframework.beans.factory.config.BeanReference;
import org.springframework.beans.factory.config.RuntimeBeanNameReference; import org.springframework.beans.factory.config.RuntimeBeanNameReference;
@ -47,33 +49,38 @@ import org.springframework.beans.testfixture.beans.factory.aot.DeferredTypeBuild
import org.springframework.core.ResolvableType; import org.springframework.core.ResolvableType;
import org.springframework.core.test.tools.Compiled; import org.springframework.core.test.tools.Compiled;
import org.springframework.core.test.tools.TestCompiler; import org.springframework.core.test.tools.TestCompiler;
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.CodeBlock; import org.springframework.javapoet.CodeBlock;
import org.springframework.javapoet.MethodSpec; import org.springframework.javapoet.MethodSpec;
import org.springframework.javapoet.ParameterizedTypeName; import org.springframework.javapoet.ParameterizedTypeName;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
/** /**
* Tests for {@link BeanDefinitionPropertyValueCodeGenerator}. * Tests for {@link BeanDefinitionPropertyValueCodeGeneratorDelegates}. This
* also tests that code generated by {@link ValueCodeGeneratorDelegates}
* compiles.
* *
* @author Stephane Nicoll * @author Stephane Nicoll
* @author Phillip Webb * @author Phillip Webb
* @author Sebastien Deleuze * @author Sebastien Deleuze
* @since 6.0 * @since 6.0
* @see BeanDefinitionPropertyValueCodeGeneratorTests
*/ */
class BeanDefinitionPropertyValueCodeGeneratorTests { class BeanDefinitionPropertyValueCodeGeneratorDelegatesTests {
private static BeanDefinitionPropertyValueCodeGenerator createPropertyValuesCodeGenerator(GeneratedClass generatedClass) { private static ValueCodeGenerator createValueCodeGenerator(GeneratedClass generatedClass) {
return new BeanDefinitionPropertyValueCodeGenerator(generatedClass.getMethods(), null); return ValueCodeGenerator.with(BeanDefinitionPropertyValueCodeGeneratorDelegates.INSTANCES)
.add(ValueCodeGeneratorDelegates.INSTANCES)
.scoped(generatedClass.getMethods());
} }
private void compile(Object value, BiConsumer<Object, Compiled> result) { private void compile(Object value, BiConsumer<Object, Compiled> result) {
TestGenerationContext generationContext = new TestGenerationContext(); TestGenerationContext generationContext = new TestGenerationContext();
DeferredTypeBuilder typeBuilder = new DeferredTypeBuilder(); DeferredTypeBuilder typeBuilder = new DeferredTypeBuilder();
GeneratedClass generatedClass = generationContext.getGeneratedClasses().addForFeature("TestCode", typeBuilder); GeneratedClass generatedClass = generationContext.getGeneratedClasses().addForFeature("TestCode", typeBuilder);
CodeBlock generatedCode = createPropertyValuesCodeGenerator(generatedClass).generateCode(value); CodeBlock generatedCode = createValueCodeGenerator(generatedClass).generateCode(value);
typeBuilder.set(type -> { typeBuilder.set(type -> {
type.addModifiers(Modifier.PUBLIC); type.addModifiers(Modifier.PUBLIC);
type.addSuperinterface( type.addSuperinterface(
@ -101,90 +108,72 @@ class BeanDefinitionPropertyValueCodeGeneratorTests {
@Test @Test
void generateWhenBoolean() { void generateWhenBoolean() {
compile(true, (instance, compiled) -> { compile(true, (instance, compiled) ->
assertThat(instance).isEqualTo(Boolean.TRUE); assertThat(instance).isEqualTo(Boolean.TRUE));
assertThat(compiled.getSourceFile()).contains("true");
});
} }
@Test @Test
void generateWhenByte() { void generateWhenByte() {
compile((byte) 2, (instance, compiled) -> { compile((byte) 2, (instance, compiled) ->
assertThat(instance).isEqualTo((byte) 2); assertThat(instance).isEqualTo((byte) 2));
assertThat(compiled.getSourceFile()).contains("(byte) 2");
});
} }
@Test @Test
void generateWhenShort() { void generateWhenShort() {
compile((short) 3, (instance, compiled) -> { compile((short) 3, (instance, compiled) ->
assertThat(instance).isEqualTo((short) 3); assertThat(instance).isEqualTo((short) 3));
assertThat(compiled.getSourceFile()).contains("(short) 3");
});
} }
@Test @Test
void generateWhenInt() { void generateWhenInt() {
compile(4, (instance, compiled) -> { compile(4, (instance, compiled) ->
assertThat(instance).isEqualTo(4); assertThat(instance).isEqualTo(4));
assertThat(compiled.getSourceFile()).contains("return 4;");
});
} }
@Test @Test
void generateWhenLong() { void generateWhenLong() {
compile(5L, (instance, compiled) -> { compile(5L, (instance, compiled) ->
assertThat(instance).isEqualTo(5L); assertThat(instance).isEqualTo(5L));
assertThat(compiled.getSourceFile()).contains("5L");
});
} }
@Test @Test
void generateWhenFloat() { void generateWhenFloat() {
compile(0.1F, (instance, compiled) -> { compile(0.1F, (instance, compiled) ->
assertThat(instance).isEqualTo(0.1F); assertThat(instance).isEqualTo(0.1F));
assertThat(compiled.getSourceFile()).contains("0.1F");
});
} }
@Test @Test
void generateWhenDouble() { void generateWhenDouble() {
compile(0.2, (instance, compiled) -> { compile(0.2, (instance, compiled) ->
assertThat(instance).isEqualTo(0.2); assertThat(instance).isEqualTo(0.2));
assertThat(compiled.getSourceFile()).contains("(double) 0.2");
});
} }
@Test @Test
void generateWhenChar() { void generateWhenChar() {
compile('a', (instance, compiled) -> { compile('a', (instance, compiled) ->
assertThat(instance).isEqualTo('a'); assertThat(instance).isEqualTo('a'));
assertThat(compiled.getSourceFile()).contains("'a'");
});
} }
@Test @Test
void generateWhenSimpleEscapedCharReturnsEscaped() { void generateWhenSimpleEscapedCharReturnsEscaped() {
testEscaped('\b', "'\\b'"); testEscaped('\b');
testEscaped('\t', "'\\t'"); testEscaped('\t');
testEscaped('\n', "'\\n'"); testEscaped('\n');
testEscaped('\f', "'\\f'"); testEscaped('\f');
testEscaped('\r', "'\\r'"); testEscaped('\r');
testEscaped('\"', "'\"'"); testEscaped('\"');
testEscaped('\'', "'\\''"); testEscaped('\'');
testEscaped('\\', "'\\\\'"); testEscaped('\\');
} }
@Test @Test
void generatedWhenUnicodeEscapedCharReturnsEscaped() { void generatedWhenUnicodeEscapedCharReturnsEscaped() {
testEscaped('\u007f', "'\\u007f'"); testEscaped('\u007f');
} }
private void testEscaped(char value, String expectedSourceContent) { private void testEscaped(char value) {
compile(value, (instance, compiled) -> { compile(value, (instance, compiled) ->
assertThat(instance).isEqualTo(value); assertThat(instance).isEqualTo(value));
assertThat(compiled.getSourceFile()).contains(expectedSourceContent);
});
} }
} }
@ -194,10 +183,8 @@ class BeanDefinitionPropertyValueCodeGeneratorTests {
@Test @Test
void generateWhenString() { void generateWhenString() {
compile("test\n", (instance, compiled) -> { compile("test\n", (instance, compiled) ->
assertThat(instance).isEqualTo("test\n"); assertThat(instance).isEqualTo("test\n"));
assertThat(compiled.getSourceFile()).contains("\n");
});
} }
} }
@ -207,10 +194,8 @@ class BeanDefinitionPropertyValueCodeGeneratorTests {
@Test @Test
void generateWhenCharset() { void generateWhenCharset() {
compile(StandardCharsets.UTF_8, (instance, compiled) -> { compile(StandardCharsets.UTF_8, (instance, compiled) ->
assertThat(instance).isEqualTo(Charset.forName("UTF-8")); assertThat(instance).isEqualTo(Charset.forName("UTF-8")));
assertThat(compiled.getSourceFile()).contains("\"UTF-8\"");
});
} }
} }
@ -220,18 +205,14 @@ class BeanDefinitionPropertyValueCodeGeneratorTests {
@Test @Test
void generateWhenEnum() { void generateWhenEnum() {
compile(ChronoUnit.DAYS, (instance, compiled) -> { compile(ChronoUnit.DAYS, (instance, compiled) ->
assertThat(instance).isEqualTo(ChronoUnit.DAYS); assertThat(instance).isEqualTo(ChronoUnit.DAYS));
assertThat(compiled.getSourceFile()).contains("ChronoUnit.DAYS");
});
} }
@Test @Test
void generateWhenEnumWithClassBody() { void generateWhenEnumWithClassBody() {
compile(EnumWithClassBody.TWO, (instance, compiled) -> { compile(EnumWithClassBody.TWO, (instance, compiled) ->
assertThat(instance).isEqualTo(EnumWithClassBody.TWO); assertThat(instance).isEqualTo(EnumWithClassBody.TWO));
assertThat(compiled.getSourceFile()).contains("EnumWithClassBody.TWO");
});
} }
} }
@ -266,18 +247,16 @@ class BeanDefinitionPropertyValueCodeGeneratorTests {
@Test @Test
void generateWhenNoneResolvableType() { void generateWhenNoneResolvableType() {
ResolvableType resolvableType = ResolvableType.NONE; ResolvableType resolvableType = ResolvableType.NONE;
compile(resolvableType, (instance, compiled) -> { compile(resolvableType, (instance, compiled) ->
assertThat(instance).isEqualTo(resolvableType); assertThat(instance).isEqualTo(resolvableType));
assertThat(compiled.getSourceFile()).contains("ResolvableType.NONE");
});
} }
@Test @Test
void generateWhenGenericResolvableType() { void generateWhenGenericResolvableType() {
ResolvableType resolvableType = ResolvableType ResolvableType resolvableType = ResolvableType
.forClassWithGenerics(List.class, String.class); .forClassWithGenerics(List.class, String.class);
compile(resolvableType, (instance, compiled) -> assertThat(instance) compile(resolvableType, (instance, compiled) ->
.isEqualTo(resolvableType)); assertThat(instance).isEqualTo(resolvableType));
} }
@Test @Test
@ -298,28 +277,22 @@ class BeanDefinitionPropertyValueCodeGeneratorTests {
@Test @Test
void generateWhenPrimitiveArray() { void generateWhenPrimitiveArray() {
byte[] bytes = { 0, 1, 2 }; byte[] bytes = { 0, 1, 2 };
compile(bytes, (instance, compiler) -> { compile(bytes, (instance, compiler) ->
assertThat(instance).isEqualTo(bytes); assertThat(instance).isEqualTo(bytes));
assertThat(compiler.getSourceFile()).contains("new byte[]");
});
} }
@Test @Test
void generateWhenWrapperArray() { void generateWhenWrapperArray() {
Byte[] bytes = { 0, 1, 2 }; Byte[] bytes = { 0, 1, 2 };
compile(bytes, (instance, compiler) -> { compile(bytes, (instance, compiler) ->
assertThat(instance).isEqualTo(bytes); assertThat(instance).isEqualTo(bytes));
assertThat(compiler.getSourceFile()).contains("new Byte[]");
});
} }
@Test @Test
void generateWhenClassArray() { void generateWhenClassArray() {
Class<?>[] classes = new Class<?>[] { InputStream.class, OutputStream.class }; Class<?>[] classes = new Class<?>[] { InputStream.class, OutputStream.class };
compile(classes, (instance, compiler) -> { compile(classes, (instance, compiler) ->
assertThat(instance).isEqualTo(classes); assertThat(instance).isEqualTo(classes));
assertThat(compiler.getSourceFile()).contains("new Class[]");
});
} }
} }
@ -402,10 +375,7 @@ class BeanDefinitionPropertyValueCodeGeneratorTests {
@Test @Test
void generateWhenEmptyList() { void generateWhenEmptyList() {
List<String> list = List.of(); List<String> list = List.of();
compile(list, (instance, compiler) -> { compile(list, (instance, compiler) -> assertThat(instance).isEqualTo(list));
assertThat(instance).isEqualTo(list);
assertThat(compiler.getSourceFile()).contains("Collections.emptyList();");
});
} }
} }
@ -423,20 +393,14 @@ class BeanDefinitionPropertyValueCodeGeneratorTests {
@Test @Test
void generateWhenEmptySet() { void generateWhenEmptySet() {
Set<String> set = Set.of(); Set<String> set = Set.of();
compile(set, (instance, compiler) -> { compile(set, (instance, compiler) -> assertThat(instance).isEqualTo(set));
assertThat(instance).isEqualTo(set);
assertThat(compiler.getSourceFile()).contains("Collections.emptySet();");
});
} }
@Test @Test
void generateWhenLinkedHashSet() { void generateWhenLinkedHashSet() {
Set<String> set = new LinkedHashSet<>(List.of("a", "b", "c")); Set<String> set = new LinkedHashSet<>(List.of("a", "b", "c"));
compile(set, (instance, compiler) -> { compile(set, (instance, compiler) ->
assertThat(instance).isEqualTo(set).isInstanceOf(LinkedHashSet.class); assertThat(instance).isEqualTo(set).isInstanceOf(LinkedHashSet.class));
assertThat(compiler.getSourceFile())
.contains("new LinkedHashSet(List.of(");
});
} }
@Test @Test
@ -453,10 +417,8 @@ class BeanDefinitionPropertyValueCodeGeneratorTests {
@Test @Test
void generateWhenSmallMap() { void generateWhenSmallMap() {
Map<String, String> map = Map.of("k1", "v1", "k2", "v2"); Map<String, String> map = Map.of("k1", "v1", "k2", "v2");
compile(map, (instance, compiler) -> { compile(map, (instance, compiler) ->
assertThat(instance).isEqualTo(map); assertThat(instance).isEqualTo(map));
assertThat(compiler.getSourceFile()).contains("Map.of(");
});
} }
@Test @Test
@ -465,10 +427,7 @@ class BeanDefinitionPropertyValueCodeGeneratorTests {
for (int i = 1; i <= 11; i++) { for (int i = 1; i <= 11; i++) {
map.put("k" + i, "v" + i); map.put("k" + i, "v" + i);
} }
compile(map, (instance, compiler) -> { compile(map, (instance, compiler) -> assertThat(instance).isEqualTo(map));
assertThat(instance).isEqualTo(map);
assertThat(compiler.getSourceFile()).contains("Map.ofEntries(");
});
} }
@Test @Test
@ -518,47 +477,4 @@ class BeanDefinitionPropertyValueCodeGeneratorTests {
} }
@Nested
static class ExceptionTests {
@Test
void generateWhenUnsupportedDataTypeThrowsException() {
SampleValue sampleValue = new SampleValue("one");
assertThatIllegalArgumentException().isThrownBy(() -> generateCode(sampleValue))
.withMessageContaining("Failed to generate code for")
.withMessageContaining(sampleValue.toString())
.withMessageContaining(SampleValue.class.getName())
.havingCause()
.withMessageContaining("Code generation does not support")
.withMessageContaining(SampleValue.class.getName());
}
@Test
void generateWhenListOfUnsupportedElement() {
SampleValue one = new SampleValue("one");
SampleValue two = new SampleValue("two");
List<SampleValue> list = List.of(one, two);
assertThatIllegalArgumentException().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("?")
.havingCause()
.withMessageContaining("Code generation does not support ?");
}
private void generateCode(Object value) {
TestGenerationContext context = new TestGenerationContext();
GeneratedClass generatedClass = context.getGeneratedClasses()
.addForFeature("Test", type -> {});
createPropertyValuesCodeGenerator(generatedClass).generateCode(value);
}
record SampleValue(String name) {}
}
} }

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

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2022 the original author or authors. * Copyright 2002-2023 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.beans.factory.aot; package org.springframework.core.testfixture.aot.generate.value;
/** /**
* Test enum that include a class body. * Test enum that include a class body.

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2022 the original author or authors. * Copyright 2002-2023 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -14,13 +14,13 @@
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.beans.factory.aot; package org.springframework.core.testfixture.aot.generate.value;
/** /**
* Fake CGLIB generated class. * Fake CGLIB generated class.
* *
* @author Phillip Webb * @author Phillip Webb
*/ */
class ExampleClass$$GeneratedBy extends ExampleClass { public class ExampleClass$$GeneratedBy extends ExampleClass {
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2022 the original author or authors. * Copyright 2002-2023 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.beans.factory.aot; package org.springframework.core.testfixture.aot.generate.value;
/** /**
* Public example class used for test. * Public example class used for test.