diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanDefinitionPropertiesCodeGenerator.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanDefinitionPropertiesCodeGenerator.java index 2f474de6a54a..e12e4f3d2e27 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanDefinitionPropertiesCodeGenerator.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanDefinitionPropertiesCodeGenerator.java @@ -34,6 +34,9 @@ import java.util.function.Predicate; 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.MemberCategory; import org.springframework.aot.hint.RuntimeHints; @@ -89,7 +92,7 @@ class BeanDefinitionPropertiesCodeGenerator { private final Predicate attributeFilter; - private final BeanDefinitionPropertyValueCodeGenerator valueCodeGenerator; + private final ValueCodeGenerator valueCodeGenerator; BeanDefinitionPropertiesCodeGenerator(RuntimeHints hints, @@ -98,8 +101,11 @@ class BeanDefinitionPropertiesCodeGenerator { this.hints = hints; this.attributeFilter = attributeFilter; - this.valueCodeGenerator = new BeanDefinitionPropertyValueCodeGenerator(generatedMethods, - (object, type) -> customValueCodeGenerator.apply(PropertyNamesStack.peek(), object)); + this.valueCodeGenerator = ValueCodeGenerator + .with(new ValueCodeGeneratorDelegateAdapter(customValueCodeGenerator)) + .add(BeanDefinitionPropertyValueCodeGeneratorDelegates.INSTANCES) + .add(ValueCodeGeneratorDelegates.INSTANCES) + .scoped(generatedMethods); } @@ -366,6 +372,22 @@ private CodeBlock castIfNecessary(boolean castNecessary, Class castType, Code return (castNecessary ? CodeBlock.of("($T) $L", castType, valueCode) : valueCode); } + + static class ValueCodeGeneratorDelegateAdapter implements Delegate { + + private final BiFunction customValueCodeGenerator; + + ValueCodeGeneratorDelegateAdapter(BiFunction customValueCodeGenerator) { + this.customValueCodeGenerator = customValueCodeGenerator; + } + + @Override + public CodeBlock generateCode(ValueCodeGenerator valueCodeGenerator, Object value) { + return this.customValueCodeGenerator.apply(PropertyNamesStack.peek(), value); + } + } + + static class PropertyNamesStack { private static final ThreadLocal> threadLocal = ThreadLocal.withInitial(ArrayDeque::new); @@ -384,7 +406,6 @@ static String peek() { String value = threadLocal.get().peek(); return ("".equals(value) ? null : value); } - } } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanDefinitionPropertyValueCodeGenerator.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanDefinitionPropertyValueCodeGenerator.java deleted file mode 100644 index 066fc5d4b1e6..000000000000 --- a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanDefinitionPropertyValueCodeGenerator.java +++ /dev/null @@ -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 delegates; - - - BeanDefinitionPropertyValueCodeGenerator(GeneratedMethods generatedMethods, - @Nullable BiFunction 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 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 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> 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> { - - public ManagedListDelegate() { - super(ManagedList.class, CodeBlock.of("new $T()", ManagedList.class)); - } - } - - - /** - * {@link Delegate} for {@link ManagedSet} types. - */ - private class ManagedSetDelegate extends CollectionDelegate> { - - 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 CodeBlock generateManagedMapCode(ResolvableType type, ManagedMap 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> 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> { - - ListDelegate() { - super(List.class, CodeBlock.of("$T.emptyList()", Collections.class)); - } - } - - - /** - * {@link Delegate} for {@link Set} types. - */ - private class SetDelegate extends CollectionDelegate> { - - 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(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 CodeBlock generateMapCode(ResolvableType type, Map 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> iterator = map.entrySet().iterator(); - while (iterator.hasNext()) { - Entry 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 Map orderForCodeConsistency(Map map) { - try { - return new TreeMap<>(map); - } - catch (ClassCastException ex) { - // If elements are not comparable, just keep the original map - return map; - } - } - - private CodeBlock generateLinkedHashMapCode(Map 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); - } - } - -} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanDefinitionPropertyValueCodeGeneratorDelegates.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanDefinitionPropertyValueCodeGeneratorDelegates.java new file mode 100644 index 000000000000..f9cfc2ad00c5 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanDefinitionPropertyValueCodeGeneratorDelegates.java @@ -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: + *
    + *
  • {@link ManagedList},
  • + *
  • {@link ManagedSet},
  • + *
  • {@link ManagedMap},
  • + *
  • {@link LinkedHashMap},
  • + *
  • {@link BeanReference},
  • + *
  • {@link TypedStringValue}.
  • + *
+ * 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 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> { + + public ManagedListDelegate() { + super(ManagedList.class, CodeBlock.of("new $T()", ManagedList.class)); + } + } + + + /** + * {@link Delegate} for {@link ManagedSet} types. + */ + private static class ManagedSetDelegate extends CollectionDelegate> { + + 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 CodeBlock generateManagedMapCode(ValueCodeGenerator valueCodeGenerator, + ManagedMap managedMap) { + if (managedMap.isEmpty()) { + return EMPTY_RESULT; + } + CodeBlock.Builder code = CodeBlock.builder(); + code.add("$T.ofEntries(", ManagedMap.class); + Iterator> 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); + } + } +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/DefaultBeanRegistrationCodeFragments.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/DefaultBeanRegistrationCodeFragments.java index 90c4f8acf25a..797808f46391 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/aot/DefaultBeanRegistrationCodeFragments.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/DefaultBeanRegistrationCodeFragments.java @@ -27,6 +27,7 @@ import org.springframework.aot.generate.GenerationContext; import org.springframework.aot.generate.MethodReference; import org.springframework.aot.generate.MethodReference.ArgumentCodeGenerator; +import org.springframework.aot.generate.ValueCodeGenerator; import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanDefinitionHolder; @@ -51,6 +52,8 @@ */ class DefaultBeanRegistrationCodeFragments implements BeanRegistrationCodeFragments { + private static final ValueCodeGenerator valueCodeGenerator = ValueCodeGenerator.withDefaults(); + private final BeanRegistrationsCode beanRegistrationsCode; private final RegisteredBean registeredBean; @@ -147,9 +150,9 @@ private CodeBlock generateBeanClassCode(String targetPackage, Class beanClass private CodeBlock generateBeanTypeCode(ResolvableType beanType) { 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) { diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/ResolvableTypeCodeGenerator.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/ResolvableTypeCodeGenerator.java deleted file mode 100644 index e7b715dd006c..000000000000 --- a/spring-beans/src/main/java/org/springframework/beans/factory/aot/ResolvableTypeCodeGenerator.java +++ /dev/null @@ -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(); - } - -} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanDefinitionPropertyValueCodeGeneratorTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanDefinitionPropertyValueCodeGeneratorDelegatesTests.java similarity index 61% rename from spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanDefinitionPropertyValueCodeGeneratorTests.java rename to spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanDefinitionPropertyValueCodeGeneratorDelegatesTests.java index 47b8237359c5..0dafc56c1a23 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanDefinitionPropertyValueCodeGeneratorTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanDefinitionPropertyValueCodeGeneratorDelegatesTests.java @@ -36,6 +36,8 @@ import org.junit.jupiter.api.Test; 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.beans.factory.config.BeanReference; import org.springframework.beans.factory.config.RuntimeBeanNameReference; @@ -47,33 +49,38 @@ import org.springframework.core.ResolvableType; import org.springframework.core.test.tools.Compiled; 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.MethodSpec; import org.springframework.javapoet.ParameterizedTypeName; 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 Phillip Webb * @author Sebastien Deleuze * @since 6.0 - * @see BeanDefinitionPropertyValueCodeGeneratorTests */ -class BeanDefinitionPropertyValueCodeGeneratorTests { +class BeanDefinitionPropertyValueCodeGeneratorDelegatesTests { - private static BeanDefinitionPropertyValueCodeGenerator createPropertyValuesCodeGenerator(GeneratedClass generatedClass) { - return new BeanDefinitionPropertyValueCodeGenerator(generatedClass.getMethods(), null); + private static ValueCodeGenerator createValueCodeGenerator(GeneratedClass generatedClass) { + return ValueCodeGenerator.with(BeanDefinitionPropertyValueCodeGeneratorDelegates.INSTANCES) + .add(ValueCodeGeneratorDelegates.INSTANCES) + .scoped(generatedClass.getMethods()); } private void compile(Object value, BiConsumer result) { TestGenerationContext generationContext = new TestGenerationContext(); DeferredTypeBuilder typeBuilder = new DeferredTypeBuilder(); GeneratedClass generatedClass = generationContext.getGeneratedClasses().addForFeature("TestCode", typeBuilder); - CodeBlock generatedCode = createPropertyValuesCodeGenerator(generatedClass).generateCode(value); + CodeBlock generatedCode = createValueCodeGenerator(generatedClass).generateCode(value); typeBuilder.set(type -> { type.addModifiers(Modifier.PUBLIC); type.addSuperinterface( @@ -101,90 +108,72 @@ class PrimitiveTests { @Test void generateWhenBoolean() { - compile(true, (instance, compiled) -> { - assertThat(instance).isEqualTo(Boolean.TRUE); - assertThat(compiled.getSourceFile()).contains("true"); - }); + compile(true, (instance, compiled) -> + assertThat(instance).isEqualTo(Boolean.TRUE)); } @Test void generateWhenByte() { - compile((byte) 2, (instance, compiled) -> { - assertThat(instance).isEqualTo((byte) 2); - assertThat(compiled.getSourceFile()).contains("(byte) 2"); - }); + compile((byte) 2, (instance, compiled) -> + assertThat(instance).isEqualTo((byte) 2)); } @Test void generateWhenShort() { - compile((short) 3, (instance, compiled) -> { - assertThat(instance).isEqualTo((short) 3); - assertThat(compiled.getSourceFile()).contains("(short) 3"); - }); + compile((short) 3, (instance, compiled) -> + assertThat(instance).isEqualTo((short) 3)); } @Test void generateWhenInt() { - compile(4, (instance, compiled) -> { - assertThat(instance).isEqualTo(4); - assertThat(compiled.getSourceFile()).contains("return 4;"); - }); + compile(4, (instance, compiled) -> + assertThat(instance).isEqualTo(4)); } @Test void generateWhenLong() { - compile(5L, (instance, compiled) -> { - assertThat(instance).isEqualTo(5L); - assertThat(compiled.getSourceFile()).contains("5L"); - }); + compile(5L, (instance, compiled) -> + assertThat(instance).isEqualTo(5L)); } @Test void generateWhenFloat() { - compile(0.1F, (instance, compiled) -> { - assertThat(instance).isEqualTo(0.1F); - assertThat(compiled.getSourceFile()).contains("0.1F"); - }); + compile(0.1F, (instance, compiled) -> + assertThat(instance).isEqualTo(0.1F)); } @Test void generateWhenDouble() { - compile(0.2, (instance, compiled) -> { - assertThat(instance).isEqualTo(0.2); - assertThat(compiled.getSourceFile()).contains("(double) 0.2"); - }); + compile(0.2, (instance, compiled) -> + assertThat(instance).isEqualTo(0.2)); } @Test void generateWhenChar() { - compile('a', (instance, compiled) -> { - assertThat(instance).isEqualTo('a'); - assertThat(compiled.getSourceFile()).contains("'a'"); - }); + compile('a', (instance, compiled) -> + assertThat(instance).isEqualTo('a')); } @Test void generateWhenSimpleEscapedCharReturnsEscaped() { - testEscaped('\b', "'\\b'"); - testEscaped('\t', "'\\t'"); - testEscaped('\n', "'\\n'"); - testEscaped('\f', "'\\f'"); - testEscaped('\r', "'\\r'"); - testEscaped('\"', "'\"'"); - testEscaped('\'', "'\\''"); - testEscaped('\\', "'\\\\'"); + testEscaped('\b'); + testEscaped('\t'); + testEscaped('\n'); + testEscaped('\f'); + testEscaped('\r'); + testEscaped('\"'); + testEscaped('\''); + testEscaped('\\'); } @Test void generatedWhenUnicodeEscapedCharReturnsEscaped() { - testEscaped('\u007f', "'\\u007f'"); + testEscaped('\u007f'); } - private void testEscaped(char value, String expectedSourceContent) { - compile(value, (instance, compiled) -> { - assertThat(instance).isEqualTo(value); - assertThat(compiled.getSourceFile()).contains(expectedSourceContent); - }); + private void testEscaped(char value) { + compile(value, (instance, compiled) -> + assertThat(instance).isEqualTo(value)); } } @@ -194,10 +183,8 @@ class StringTests { @Test void generateWhenString() { - compile("test\n", (instance, compiled) -> { - assertThat(instance).isEqualTo("test\n"); - assertThat(compiled.getSourceFile()).contains("\n"); - }); + compile("test\n", (instance, compiled) -> + assertThat(instance).isEqualTo("test\n")); } } @@ -207,10 +194,8 @@ class CharsetTests { @Test void generateWhenCharset() { - compile(StandardCharsets.UTF_8, (instance, compiled) -> { - assertThat(instance).isEqualTo(Charset.forName("UTF-8")); - assertThat(compiled.getSourceFile()).contains("\"UTF-8\""); - }); + compile(StandardCharsets.UTF_8, (instance, compiled) -> + assertThat(instance).isEqualTo(Charset.forName("UTF-8"))); } } @@ -220,18 +205,14 @@ class EnumTests { @Test void generateWhenEnum() { - compile(ChronoUnit.DAYS, (instance, compiled) -> { - assertThat(instance).isEqualTo(ChronoUnit.DAYS); - assertThat(compiled.getSourceFile()).contains("ChronoUnit.DAYS"); - }); + compile(ChronoUnit.DAYS, (instance, compiled) -> + assertThat(instance).isEqualTo(ChronoUnit.DAYS)); } @Test void generateWhenEnumWithClassBody() { - compile(EnumWithClassBody.TWO, (instance, compiled) -> { - assertThat(instance).isEqualTo(EnumWithClassBody.TWO); - assertThat(compiled.getSourceFile()).contains("EnumWithClassBody.TWO"); - }); + compile(EnumWithClassBody.TWO, (instance, compiled) -> + assertThat(instance).isEqualTo(EnumWithClassBody.TWO)); } } @@ -266,18 +247,16 @@ void generateWhenSimpleResolvableType() { @Test void generateWhenNoneResolvableType() { ResolvableType resolvableType = ResolvableType.NONE; - compile(resolvableType, (instance, compiled) -> { - assertThat(instance).isEqualTo(resolvableType); - assertThat(compiled.getSourceFile()).contains("ResolvableType.NONE"); - }); + compile(resolvableType, (instance, compiled) -> + assertThat(instance).isEqualTo(resolvableType)); } @Test void generateWhenGenericResolvableType() { ResolvableType resolvableType = ResolvableType .forClassWithGenerics(List.class, String.class); - compile(resolvableType, (instance, compiled) -> assertThat(instance) - .isEqualTo(resolvableType)); + compile(resolvableType, (instance, compiled) -> + assertThat(instance).isEqualTo(resolvableType)); } @Test @@ -298,28 +277,22 @@ class ArrayTests { @Test void generateWhenPrimitiveArray() { byte[] bytes = { 0, 1, 2 }; - compile(bytes, (instance, compiler) -> { - assertThat(instance).isEqualTo(bytes); - assertThat(compiler.getSourceFile()).contains("new byte[]"); - }); + compile(bytes, (instance, compiler) -> + assertThat(instance).isEqualTo(bytes)); } @Test void generateWhenWrapperArray() { Byte[] bytes = { 0, 1, 2 }; - compile(bytes, (instance, compiler) -> { - assertThat(instance).isEqualTo(bytes); - assertThat(compiler.getSourceFile()).contains("new Byte[]"); - }); + compile(bytes, (instance, compiler) -> + assertThat(instance).isEqualTo(bytes)); } @Test void generateWhenClassArray() { Class[] classes = new Class[] { InputStream.class, OutputStream.class }; - compile(classes, (instance, compiler) -> { - assertThat(instance).isEqualTo(classes); - assertThat(compiler.getSourceFile()).contains("new Class[]"); - }); + compile(classes, (instance, compiler) -> + assertThat(instance).isEqualTo(classes)); } } @@ -402,10 +375,7 @@ void generateWhenStringList() { @Test void generateWhenEmptyList() { List list = List.of(); - compile(list, (instance, compiler) -> { - assertThat(instance).isEqualTo(list); - assertThat(compiler.getSourceFile()).contains("Collections.emptyList();"); - }); + compile(list, (instance, compiler) -> assertThat(instance).isEqualTo(list)); } } @@ -423,20 +393,14 @@ void generateWhenStringSet() { @Test void generateWhenEmptySet() { Set set = Set.of(); - compile(set, (instance, compiler) -> { - assertThat(instance).isEqualTo(set); - assertThat(compiler.getSourceFile()).contains("Collections.emptySet();"); - }); + compile(set, (instance, compiler) -> assertThat(instance).isEqualTo(set)); } @Test void generateWhenLinkedHashSet() { Set set = new LinkedHashSet<>(List.of("a", "b", "c")); - compile(set, (instance, compiler) -> { - assertThat(instance).isEqualTo(set).isInstanceOf(LinkedHashSet.class); - assertThat(compiler.getSourceFile()) - .contains("new LinkedHashSet(List.of("); - }); + compile(set, (instance, compiler) -> + assertThat(instance).isEqualTo(set).isInstanceOf(LinkedHashSet.class)); } @Test @@ -453,10 +417,8 @@ class MapTests { @Test void generateWhenSmallMap() { Map map = Map.of("k1", "v1", "k2", "v2"); - compile(map, (instance, compiler) -> { - assertThat(instance).isEqualTo(map); - assertThat(compiler.getSourceFile()).contains("Map.of("); - }); + compile(map, (instance, compiler) -> + assertThat(instance).isEqualTo(map)); } @Test @@ -465,10 +427,7 @@ void generateWhenMapWithOverTenElements() { for (int i = 1; i <= 11; i++) { map.put("k" + i, "v" + i); } - compile(map, (instance, compiler) -> { - assertThat(instance).isEqualTo(map); - assertThat(compiler.getSourceFile()).contains("Map.ofEntries("); - }); + compile(map, (instance, compiler) -> assertThat(instance).isEqualTo(map)); } @Test @@ -518,47 +477,4 @@ void generatedWhenBeanReferenceByType() { } - @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 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) {} - - } - } diff --git a/spring-core/src/main/java/org/springframework/aot/generate/UnsupportedTypeValueCodeGenerationException.java b/spring-core/src/main/java/org/springframework/aot/generate/UnsupportedTypeValueCodeGenerationException.java new file mode 100644 index 000000000000..e2fe95887feb --- /dev/null +++ b/spring-core/src/main/java/org/springframework/aot/generate/UnsupportedTypeValueCodeGenerationException.java @@ -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); + } + +} diff --git a/spring-core/src/main/java/org/springframework/aot/generate/ValueCodeGenerationException.java b/spring-core/src/main/java/org/springframework/aot/generate/ValueCodeGenerationException.java new file mode 100644 index 000000000000..5aaa9ba513a9 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/aot/generate/ValueCodeGenerationException.java @@ -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; + } + +} diff --git a/spring-core/src/main/java/org/springframework/aot/generate/ValueCodeGenerator.java b/spring-core/src/main/java/org/springframework/aot/generate/ValueCodeGenerator.java new file mode 100644 index 000000000000..61008f5aa259 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/aot/generate/ValueCodeGenerator.java @@ -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 delegates; + + @Nullable + private final GeneratedMethods generatedMethods; + + private ValueCodeGenerator(List 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 delegates) { + Assert.notEmpty(delegates, "Delegates must not be empty"); + return new ValueCodeGenerator(new ArrayList<>(delegates), null); + } + + public ValueCodeGenerator add(List additionalDelegates) { + Assert.notEmpty(additionalDelegates, "AdditionalDelegates must not be empty"); + List 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); + + } + +} diff --git a/spring-core/src/main/java/org/springframework/aot/generate/ValueCodeGeneratorDelegates.java b/spring-core/src/main/java/org/springframework/aot/generate/ValueCodeGeneratorDelegates.java new file mode 100644 index 000000000000..ce4ade2027b6 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/aot/generate/ValueCodeGeneratorDelegates.java @@ -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: + *
    + *
  • Primitive types,
  • + *
  • String,
  • + *
  • Charset,
  • + *
  • Enum,
  • + *
  • Class,
  • + *
  • {@link ResolvableType},
  • + *
  • Array,
  • + *
  • List via {@code List.of},
  • + *
  • Set via {@code Set.of} and support of {@link LinkedHashSet},
  • + *
  • Map via {@code Map.of} or {@code Map.ofEntries}.
  • + *
+ * Those implementations do not require the {@link ValueCodeGenerator} to be + * {@linkplain ValueCodeGenerator#scoped(GeneratedMethods) scoped}. + */ + public static final List 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 type the collection type + */ + public abstract static class CollectionDelegate> 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> 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 Map orderForCodeConsistency(Map 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 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 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> { + + ListDelegate() { + super(List.class, CodeBlock.of("$T.emptyList()", Collections.class)); + } + } + + + /** + * {@link Delegate} for {@link Set} types. + */ + private static class SetDelegate extends CollectionDelegate> { + + 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(set); + } + catch (ClassCastException ex) { + // If elements are not comparable, just keep the original set + return set; + } + } + } + +} diff --git a/spring-core/src/test/java/org/springframework/aot/generate/ValueCodeGeneratorTests.java b/spring-core/src/test/java/org/springframework/aot/generate/ValueCodeGeneratorTests.java new file mode 100644 index 000000000000..dced0ed7bbf1 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/aot/generate/ValueCodeGeneratorTests.java @@ -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 list = List.of("a", "b", "c"); + assertThat(resolve(generateCode(list))).hasImport(List.class) + .hasValueCode("List.of(\"a\", \"b\", \"c\")"); + } + + @Test + void generateWhenEmptyList() { + List list = List.of(); + assertThat(resolve(generateCode(list))).hasImport(Collections.class) + .hasValueCode("Collections.emptyList()"); + } + + } + + @Nested + class SetTests { + + @Test + void generateWhenStringSet() { + Set set = Set.of("a", "b", "c"); + assertThat(resolve(generateCode(set))).hasImport(Set.class) + .hasValueCode("Set.of(\"a\", \"b\", \"c\")"); + } + + @Test + void generateWhenEmptySet() { + Set set = Set.of(); + assertThat(resolve(generateCode(set))).hasImport(Collections.class) + .hasValueCode("Collections.emptySet()"); + } + + @Test + void generateWhenLinkedHashSet() { + Set 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> 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 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 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 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 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 { + + 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 imports) implements AssertProvider { + + @Override + public ValueCodeAssert assertThat() { + return new ValueCodeAssert(this); + } + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/aot/EnumWithClassBody.java b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/aot/generate/value/EnumWithClassBody.java similarity index 87% rename from spring-beans/src/test/java/org/springframework/beans/factory/aot/EnumWithClassBody.java rename to spring-core/src/testFixtures/java/org/springframework/core/testfixture/aot/generate/value/EnumWithClassBody.java index a8064b97ace8..5797adb0a5bf 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/aot/EnumWithClassBody.java +++ b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/aot/generate/value/EnumWithClassBody.java @@ -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"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * 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. diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/aot/ExampleClass$$GeneratedBy.java b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/aot/generate/value/ExampleClass$$GeneratedBy.java similarity index 77% rename from spring-beans/src/test/java/org/springframework/beans/factory/aot/ExampleClass$$GeneratedBy.java rename to spring-core/src/testFixtures/java/org/springframework/core/testfixture/aot/generate/value/ExampleClass$$GeneratedBy.java index 8b40ef58defd..36d070119402 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/aot/ExampleClass$$GeneratedBy.java +++ b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/aot/generate/value/ExampleClass$$GeneratedBy.java @@ -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"); * you may not use this file except in compliance with the License. @@ -14,13 +14,13 @@ * limitations under the License. */ -package org.springframework.beans.factory.aot; +package org.springframework.core.testfixture.aot.generate.value; /** * Fake CGLIB generated class. * * @author Phillip Webb */ -class ExampleClass$$GeneratedBy extends ExampleClass { +public class ExampleClass$$GeneratedBy extends ExampleClass { } diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/aot/ExampleClass.java b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/aot/generate/value/ExampleClass.java similarity index 84% rename from spring-beans/src/test/java/org/springframework/beans/factory/aot/ExampleClass.java rename to spring-core/src/testFixtures/java/org/springframework/core/testfixture/aot/generate/value/ExampleClass.java index c549b9befabb..b4a5681500c5 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/aot/ExampleClass.java +++ b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/aot/generate/value/ExampleClass.java @@ -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"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.beans.factory.aot; +package org.springframework.core.testfixture.aot.generate.value; /** * Public example class used for test.