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:
parent
75da9c3c47
commit
3c2c9ca186
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -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) {}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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.
|
||||||
|
|
@ -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 {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -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.
|
||||||
Loading…
Reference in New Issue