diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanDefinitionMethodGeneratorTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanDefinitionMethodGeneratorTests.java index 991578a2fca4..d7cb6da2aa4b 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanDefinitionMethodGeneratorTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanDefinitionMethodGeneratorTests.java @@ -34,6 +34,9 @@ import org.springframework.aot.generate.MethodReference; import org.springframework.aot.generate.MethodReference.ArgumentCodeGenerator; import org.springframework.aot.test.generate.TestGenerationContext; +import org.springframework.beans.factory.aot.support.ObjectPair; +import org.springframework.beans.factory.aot.support.RecordPair; +import org.springframework.beans.factory.aot.support.RecordWithObject; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.ConstructorArgumentValues.ValueHolder; import org.springframework.beans.factory.support.BeanDefinitionBuilder; @@ -683,6 +686,21 @@ void generateBeanDefinitionMethodWhenBeanIsOfPrimitiveType() { testBeanDefinitionMethodInCurrentFile(Boolean.class, beanDefinition); } + @Test + void generateBeanDefinitionMethodWhenBeanIsOfCommonValueType() { + RootBeanDefinition beanDefinition = (RootBeanDefinition) BeanDefinitionBuilder + .rootBeanDefinition(RecordPair.class) + .setFactoryMethod("of") + .addConstructorArgValue(new RecordWithObject(0, null)) + .addConstructorArgValue(new ObjectPair<>("Test", 0)) + .getBeanDefinition(); + BeanDefinitionMethodGenerator generator = new BeanDefinitionMethodGenerator( + this.methodGeneratorFactory, registerBean(beanDefinition), null, Collections.emptyList()); + MethodReference method = generator.generateBeanDefinitionMethod( + this.generationContext, this.beanRegistrationsCode); + compile(method, (actual, compiled) -> assertThat(actual).isEqualTo(beanDefinition)); + } + @Test void generateBeanDefinitionMethodWhenInstanceSupplierWithNoCustomization() { RegisteredBean registeredBean = registerBean(new RootBeanDefinition(TestBean.class, TestBean::new)); diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanDefinitionPropertyValueCodeGeneratorDelegatesTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanDefinitionPropertyValueCodeGeneratorDelegatesTests.java index 0dafc56c1a23..2f01ccaf6da5 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanDefinitionPropertyValueCodeGeneratorDelegatesTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanDefinitionPropertyValueCodeGeneratorDelegatesTests.java @@ -18,10 +18,13 @@ import java.io.InputStream; import java.io.OutputStream; +import java.lang.reflect.InvocationTargetException; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.time.temporal.ChronoUnit; +import java.util.ArrayList; import java.util.HashMap; +import java.util.IdentityHashMap; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; @@ -36,9 +39,21 @@ import org.junit.jupiter.api.Test; import org.springframework.aot.generate.GeneratedClass; +import org.springframework.aot.generate.ValueCodeGenerationException; import org.springframework.aot.generate.ValueCodeGenerator; import org.springframework.aot.generate.ValueCodeGeneratorDelegates; import org.springframework.aot.test.generate.TestGenerationContext; +import org.springframework.beans.factory.aot.support.NestableObject; +import org.springframework.beans.factory.aot.support.NestableRecord; +import org.springframework.beans.factory.aot.support.ObjectPair; +import org.springframework.beans.factory.aot.support.ObjectWithMissingConstructor; +import org.springframework.beans.factory.aot.support.ObjectWithMissingSetter; +import org.springframework.beans.factory.aot.support.ObjectWithPrivateConstructor; +import org.springframework.beans.factory.aot.support.ObjectWithPrivateSetter; +import org.springframework.beans.factory.aot.support.ObjectWithRecord; +import org.springframework.beans.factory.aot.support.ProtectedObjectCreator; +import org.springframework.beans.factory.aot.support.RecordPair; +import org.springframework.beans.factory.aot.support.RecordWithObject; import org.springframework.beans.factory.config.BeanReference; import org.springframework.beans.factory.config.RuntimeBeanNameReference; import org.springframework.beans.factory.config.RuntimeBeanReference; @@ -55,8 +70,10 @@ import org.springframework.javapoet.CodeBlock; import org.springframework.javapoet.MethodSpec; import org.springframework.javapoet.ParameterizedTypeName; +import org.springframework.util.ReflectionUtils; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; /** * Tests for {@link BeanDefinitionPropertyValueCodeGeneratorDelegates}. This @@ -295,6 +312,15 @@ void generateWhenClassArray() { assertThat(instance).isEqualTo(classes)); } + @Test + void noGenerateWhenSelfReference() { + Object[] items = new Object[1]; + items[0] = items; + ValueCodeGenerator valueCodeGenerator = ValueCodeGenerator.withDefaults(); + assertThatCode(() -> valueCodeGenerator.generateCode(items)) + .isInstanceOf(ValueCodeGenerationException.class); + } + } @Nested @@ -317,6 +343,15 @@ void generateWhenEmptyManagedList() { .isInstanceOf(ManagedList.class)); } + @Test + void noGenerateWhenSelfReference() { + ManagedList list = new ManagedList<>(); + list.add(list); + ValueCodeGenerator valueCodeGenerator = ValueCodeGenerator.withDefaults(); + assertThatCode(() -> valueCodeGenerator.generateCode(list)) + .isInstanceOf(ValueCodeGenerationException.class); + } + } @Nested @@ -338,7 +373,6 @@ void generateWhenEmptyManagedSet() { compile(set, (instance, compiler) -> assertThat(instance).isEqualTo(set) .isInstanceOf(ManagedSet.class)); } - } @Nested @@ -359,7 +393,6 @@ void generateWhenEmptyManagedMap() { compile(map, (instance, compiler) -> assertThat(instance).isEqualTo(map) .isInstanceOf(ManagedMap.class)); } - } @Nested @@ -378,6 +411,15 @@ void generateWhenEmptyList() { compile(list, (instance, compiler) -> assertThat(instance).isEqualTo(list)); } + @Test + void noGenerateWhenSelfReference() { + List list = new ArrayList<>(); + list.add(list); + ValueCodeGenerator valueCodeGenerator = ValueCodeGenerator.withDefaults(); + assertThatCode(() -> valueCodeGenerator.generateCode(list)) + .isInstanceOf(ValueCodeGenerationException.class); + } + } @Nested @@ -408,7 +450,6 @@ void generateWhenSetOfClass() { Set> set = Set.of(String.class, Integer.class, Long.class); compile(set, (instance, compiler) -> assertThat(instance).isEqualTo(set)); } - } @Nested @@ -442,6 +483,237 @@ void generateWhenLinkedHashMap() { }); } + @Test + void noGenerateWhenSelfReference() { + Map map = new IdentityHashMap<>(); + map.put(map, map); + ValueCodeGenerator valueCodeGenerator = ValueCodeGenerator.withDefaults(); + assertThatCode(() -> valueCodeGenerator.generateCode(map)) + .isInstanceOf(ValueCodeGenerationException.class); + } + + } + + @Nested + class RecordTests { + @Test + void generatedPublicRecord() { + NestableRecord record = new NestableRecord(0, "record", null); + compile(record, (instance, compiler) -> { + NestableRecord actual = (NestableRecord) instance; + assertThat(actual).isEqualTo(record); + }); + } + + @Test + void generatedNestedPublicRecord() { + NestableRecord record = new NestableRecord(0, "parent", new NestableRecord(1, "child", null)); + compile(record, (instance, compiler) -> { + NestableRecord actual = (NestableRecord) instance; + assertThat(actual).isEqualTo(record); + }); + } + + @Test + void generatedNestedWithObjects() { + RecordWithObject record = new RecordWithObject(0, new ObjectWithRecord(1, new RecordWithObject(2, new ObjectWithRecord(3, null)))); + compile(record, (instance, compiler) -> { + RecordWithObject actual = (RecordWithObject) instance; + assertThat(actual).isEqualTo(record); + }); + } + + @Test + void generatedGenericRecord() { + RecordPair pair = new RecordPair<>("Left", 1); + compile(pair, (instance, compiler) -> { + RecordPair actual = (RecordPair) instance; + assertThat(actual).isEqualTo(pair); + }); + } + + @Test + void notGeneratedCyclicRecord() { + ValueCodeGenerator valueCodeGenerator = ValueCodeGenerator.withDefaults(); + ObjectWithRecord cycleEnd = new ObjectWithRecord(1); + RecordWithObject cycleStart = new RecordWithObject(0, cycleEnd); + cycleEnd.setNested(cycleStart); + assertThatCode(() -> valueCodeGenerator.generateCode(cycleStart)) + .isInstanceOf(ValueCodeGenerationException.class); + } + + @Test + void notGeneratedProtectedRecord() { + ValueCodeGenerator valueCodeGenerator = ValueCodeGenerator.withDefaults(); + assertThatCode(() -> valueCodeGenerator.generateCode(ProtectedObjectCreator.getProtectedRecord())) + .isInstanceOf(ValueCodeGenerationException.class); + } + + public record InaccessibleRecord() {} + + @Test + void notGeneratedPublicRecordInProtectedClass() { + ValueCodeGenerator valueCodeGenerator = ValueCodeGenerator.withDefaults(); + assertThatCode(() -> valueCodeGenerator.generateCode(new InaccessibleRecord())) + .isInstanceOf(ValueCodeGenerationException.class); + } + } + + @Nested + class ObjectTests { + @Test + void generatedPublicObject() { + NestableObject object = new NestableObject(); + object.name = "Object"; + object.setId(1); + object.setNested(null); + compile(object, (instance, compiler) -> { + NestableObject actual = (NestableObject) instance; + assertThat(actual).isEqualTo(object); + }); + } + + @Test + void generatedNestedPublicObject() { + NestableObject parent = new NestableObject(); + parent.name = "Parent"; + parent.setId(1); + NestableObject child = new NestableObject(); + child.name = "Child"; + child.setId(2); + child.setNested(null); + parent.setNested(child); + compile(parent, (instance, compiler) -> { + NestableObject actual = (NestableObject) instance; + assertThat(actual).isEqualTo(parent); + }); + } + + @Test + void generatedGeneric() { + ObjectPair pair = new ObjectPair<>("Test", 1); + compile(pair, (instance, compiler) -> { + ObjectPair actual = (ObjectPair) instance; + assertThat(actual).isEqualTo(pair); + }); + } + + @Test + void generatedPairOfObjects() { + ObjectPair, ObjectPair> pair = + new ObjectPair<>(new ObjectPair<>("Left", 0), + new ObjectPair<>(1, "Right")); + compile(pair, (instance, compiler) -> { + ObjectPair actual = (ObjectPair) instance; + assertThat(actual).isEqualTo(pair); + }); + } + + @Test + void generatedNestedWithRecords() { + ObjectWithRecord object = new ObjectWithRecord(0, new RecordWithObject(1, new ObjectWithRecord(2, new RecordWithObject(3, null)))); + compile(object, (instance, compiler) -> { + ObjectWithRecord actual = (ObjectWithRecord) instance; + assertThat(actual).isEqualTo(object); + }); + } + + @Test + void generatedPairOfObjectsWithinList() { + var pair = new ObjectPair<>(List.of(new ObjectPair<>("Left", 0)), + List.of(new ObjectPair<>(1, "Right"))); + compile(pair, (instance, compiler) -> { + ObjectPair actual = (ObjectPair) instance; + assertThat(actual).isEqualTo(pair); + }); + } + + @Test + void generatedPairOfObjectsWithinSet() { + var pair = new ObjectPair<>(Set.of(new ObjectPair<>("Left", 0)), + Set.of(new ObjectPair<>(1, "Right"))); + compile(pair, (instance, compiler) -> { + ObjectPair actual = (ObjectPair) instance; + assertThat(actual).isEqualTo(pair); + }); + } + + @Test + void generatedPairOfObjectsWithinMap() { + var pair = new ObjectPair<>(Map.of("a", new ObjectPair<>("Left", 0)), + Map.of("b", new ObjectPair<>(1, "Right"))); + compile(pair, (instance, compiler) -> { + ObjectPair actual = (ObjectPair) instance; + assertThat(actual).isEqualTo(pair); + }); + } + + @Test + void notGeneratedProtectedClass() { + ValueCodeGenerator valueCodeGenerator = ValueCodeGenerator.withDefaults(); + assertThatCode(() -> valueCodeGenerator.generateCode(ProtectedObjectCreator.getProtectedObject())) + .isInstanceOf(ValueCodeGenerationException.class); + } + + public static class InaccessibleObject {} + + @Test + void notGeneratedInaccessibleClass() { + ValueCodeGenerator valueCodeGenerator = ValueCodeGenerator.withDefaults(); + assertThatCode(() -> valueCodeGenerator.generateCode(new InaccessibleObject())) + .isInstanceOf(ValueCodeGenerationException.class); + } + + @Test + void notGeneratedMissingConstructor() { + ValueCodeGenerator valueCodeGenerator = ValueCodeGenerator.withDefaults(); + assertThatCode(() -> valueCodeGenerator.generateCode(new ObjectWithMissingConstructor("value"))) + .isInstanceOf(ValueCodeGenerationException.class); + } + + @Test + void notGeneratedPrivateConstructor() + throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException { + ValueCodeGenerator valueCodeGenerator = ValueCodeGenerator.withDefaults(); + ObjectWithPrivateConstructor instance = ReflectionUtils.accessibleConstructor(ObjectWithPrivateConstructor.class).newInstance(); + assertThatCode(() -> valueCodeGenerator.generateCode(instance)) + .isInstanceOf(ValueCodeGenerationException.class); + } + + @Test + void notGeneratedMissingSetter() { + ValueCodeGenerator valueCodeGenerator = ValueCodeGenerator.withDefaults(); + assertThatCode(() -> valueCodeGenerator.generateCode(new ObjectWithMissingSetter())) + .isInstanceOf(ValueCodeGenerationException.class); + } + + @Test + void notGeneratedPrivateSetter() { + ValueCodeGenerator valueCodeGenerator = ValueCodeGenerator.withDefaults(); + assertThatCode(() -> valueCodeGenerator.generateCode(new ObjectWithPrivateSetter())) + .isInstanceOf(ValueCodeGenerationException.class); + } + + @Test + void notGeneratedCyclicPublicObject() { + NestableObject cyclicObject = new NestableObject(); + cyclicObject.name = "Cyclic"; + cyclicObject.setId(0); + cyclicObject.setNested(cyclicObject); + ValueCodeGenerator valueCodeGenerator = ValueCodeGenerator.withDefaults(); + assertThatCode(() -> valueCodeGenerator.generateCode(cyclicObject)) + .isInstanceOf(ValueCodeGenerationException.class); + } + + @Test + void notGeneratedCyclicObjectFromRecord() { + ValueCodeGenerator valueCodeGenerator = ValueCodeGenerator.withDefaults(); + ObjectWithRecord cycleEnd = new ObjectWithRecord(1); + RecordWithObject cycleStart = new RecordWithObject(0, cycleEnd); + cycleEnd.setNested(cycleStart); + assertThatCode(() -> valueCodeGenerator.generateCode(cycleEnd)) + .isInstanceOf(ValueCodeGenerationException.class); + } } @Nested @@ -474,7 +746,5 @@ void generatedWhenBeanReferenceByType() { assertThat(actual.getBeanType()).isEqualTo(String.class); }); } - } - } diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/aot/support/NestableObject.java b/spring-beans/src/test/java/org/springframework/beans/factory/aot/support/NestableObject.java new file mode 100644 index 000000000000..16bac9a5b5b5 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/aot/support/NestableObject.java @@ -0,0 +1,66 @@ +/* + * Copyright 2024-2024 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.support; + +import java.util.Objects; + +/** + * Test class for testing {@link org.springframework.aot.generate.ValueCodeGenerator} + * with nestable objects. + * + * @author Christopher Chianelli + */ +public class NestableObject { + private static final String A_STATIC_FIELD = "STATIC_FIELD"; + + public String name; + private int id; + private NestableObject nested; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public NestableObject getNested() { + return nested; + } + + public void setNested(NestableObject nested) { + this.nested = nested; + } + + @Override + public boolean equals(Object object) { + if (this == object) + return true; + if (object == null || getClass() != object.getClass()) + return false; + NestableObject that = (NestableObject) object; + return id == that.id + && Objects.equals(name, that.name) + && Objects.equals(nested, that.nested); + } + + @Override + public int hashCode() { + return Objects.hash(name, id); + } +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/aot/support/NestableRecord.java b/spring-beans/src/test/java/org/springframework/beans/factory/aot/support/NestableRecord.java new file mode 100644 index 000000000000..5cfea3253c29 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/aot/support/NestableRecord.java @@ -0,0 +1,28 @@ +/* + * Copyright 2024-2024 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.support; + +import org.springframework.lang.Nullable; + +/** + * Test class for testing {@link org.springframework.aot.generate.ValueCodeGenerator} + * with nestable records. + * + * @author Christopher Chianelli + */ +public record NestableRecord(int id, String name, @Nullable NestableRecord nested) { +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/aot/support/ObjectPair.java b/spring-beans/src/test/java/org/springframework/beans/factory/aot/support/ObjectPair.java new file mode 100644 index 000000000000..c0cdba47aad3 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/aot/support/ObjectPair.java @@ -0,0 +1,69 @@ +/* + * Copyright 2024-2024 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.support; + +import java.util.Objects; + +/** + * Test class for testing {@link org.springframework.aot.generate.ValueCodeGenerator} + * with generic object pairs. + * + * @author Christopher Chianelli + */ +public class ObjectPair { + L left; + R right; + + public ObjectPair() { + } + + public ObjectPair(L left, R right) { + this.left = left; + this.right = right; + } + + public L getLeft() { + return left; + } + + public void setLeft(L left) { + this.left = left; + } + + public R getRight() { + return right; + } + + public void setRight(R right) { + this.right = right; + } + + @Override + public boolean equals(Object object) { + if (this == object) + return true; + if (object == null || getClass() != object.getClass()) + return false; + ObjectPair that = (ObjectPair) object; + return Objects.equals(left, that.left) && Objects.equals(right, that.right); + } + + @Override + public int hashCode() { + return Objects.hash(left, right); + } +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/aot/support/ObjectWithMissingConstructor.java b/spring-beans/src/test/java/org/springframework/beans/factory/aot/support/ObjectWithMissingConstructor.java new file mode 100644 index 000000000000..1e1dc0ad7ef6 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/aot/support/ObjectWithMissingConstructor.java @@ -0,0 +1,39 @@ +/* + * Copyright 2024-2024 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.support; + +/** + * Test class for testing {@link org.springframework.aot.generate.ValueCodeGenerator} + * with objects without a no-args constructor. + * + * @author Christopher Chianelli + */ +public class ObjectWithMissingConstructor { + private String field; + + public ObjectWithMissingConstructor(String field) { + this.field = field; + } + + public String getField() { + return field; + } + + public void setField(String field) { + this.field = field; + } +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/aot/support/ObjectWithMissingSetter.java b/spring-beans/src/test/java/org/springframework/beans/factory/aot/support/ObjectWithMissingSetter.java new file mode 100644 index 000000000000..22bf7bee7f6a --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/aot/support/ObjectWithMissingSetter.java @@ -0,0 +1,28 @@ +/* + * Copyright 2024-2024 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.support; + +/** + * Test class for testing {@link org.springframework.aot.generate.ValueCodeGenerator} + * with objects missing a setter. + * + * @author Christopher Chianelli + */ +public class ObjectWithMissingSetter { + private static final String A_STATIC_FIELD = "STATIC_FIELD"; + private String field; +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/aot/support/ObjectWithPrivateConstructor.java b/spring-beans/src/test/java/org/springframework/beans/factory/aot/support/ObjectWithPrivateConstructor.java new file mode 100644 index 000000000000..3649124aeb9b --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/aot/support/ObjectWithPrivateConstructor.java @@ -0,0 +1,38 @@ +/* + * Copyright 2024-2024 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.support; + +/** + * Test class for testing {@link org.springframework.aot.generate.ValueCodeGenerator} + * with objects that have a private constructor. + * + * @author Christopher Chianelli + */ +public class ObjectWithPrivateConstructor { + private String field; + + private ObjectWithPrivateConstructor() { + } + + public String getField() { + return field; + } + + public void setField(String field) { + this.field = field; + } +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/aot/support/ObjectWithPrivateSetter.java b/spring-beans/src/test/java/org/springframework/beans/factory/aot/support/ObjectWithPrivateSetter.java new file mode 100644 index 000000000000..731ebb3a5fe5 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/aot/support/ObjectWithPrivateSetter.java @@ -0,0 +1,35 @@ +/* + * Copyright 2024-2024 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.support; + +/** + * Test class for testing {@link org.springframework.aot.generate.ValueCodeGenerator} + * with objects that have a private setter. + * + * @author Christopher Chianelli + */ +public class ObjectWithPrivateSetter { + private String field; + + private String getField() { + return field; + } + + private void setField(String field) { + this.field = field; + } +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/aot/support/ObjectWithRecord.java b/spring-beans/src/test/java/org/springframework/beans/factory/aot/support/ObjectWithRecord.java new file mode 100644 index 000000000000..e1e45f8d3c13 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/aot/support/ObjectWithRecord.java @@ -0,0 +1,77 @@ +/* + * Copyright 2024-2024 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.support; + +import java.util.Objects; + +import org.springframework.lang.Nullable; + +/** + * Test class for testing {@link org.springframework.aot.generate.ValueCodeGenerator} + * with objects containing a record. + * + * @author Christopher Chianelli + */ +public class ObjectWithRecord { + private int id; + @Nullable + private RecordWithObject nested; + + public ObjectWithRecord() { + } + + public ObjectWithRecord(int id) { + this(id, null); + } + + public ObjectWithRecord(int id, @Nullable RecordWithObject nested) { + this.id = id; + this.nested = nested; + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + @Nullable + public RecordWithObject getNested() { + return nested; + } + + public void setNested(@Nullable RecordWithObject nested) { + this.nested = nested; + } + + @Override + public boolean equals(Object object) { + if (this == object) + return true; + if (object == null || getClass() != object.getClass()) + return false; + ObjectWithRecord that = (ObjectWithRecord) object; + return id == that.id && Objects.equals(nested, that.nested); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/aot/support/ProtectedObject.java b/spring-beans/src/test/java/org/springframework/beans/factory/aot/support/ProtectedObject.java new file mode 100644 index 000000000000..e8d7b930688b --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/aot/support/ProtectedObject.java @@ -0,0 +1,27 @@ +/* + * Copyright 2024-2024 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.support; + +/** + * Test class for testing {@link org.springframework.aot.generate.ValueCodeGenerator} + * with objects whose class is protected + * + * @author Christopher Chianelli + */ +class ProtectedObject { + public ProtectedObject() {} +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/aot/support/ProtectedObjectCreator.java b/spring-beans/src/test/java/org/springframework/beans/factory/aot/support/ProtectedObjectCreator.java new file mode 100644 index 000000000000..875e727be93b --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/aot/support/ProtectedObjectCreator.java @@ -0,0 +1,34 @@ +/* + * Copyright 2024-2024 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.support; + +/** + * Class for generating test objects and records that are protected. + * + * @author Christopher Chianelli + */ +public class ProtectedObjectCreator { + private ProtectedObjectCreator() {} + + public static Object getProtectedRecord() { + return new ProtectedRecord(); + } + + public static Object getProtectedObject() { + return new ProtectedObject(); + } +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/aot/support/ProtectedRecord.java b/spring-beans/src/test/java/org/springframework/beans/factory/aot/support/ProtectedRecord.java new file mode 100644 index 000000000000..40591f656782 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/aot/support/ProtectedRecord.java @@ -0,0 +1,27 @@ +/* + * Copyright 2024-2024 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.support; + +/** + * Test class for testing {@link org.springframework.aot.generate.ValueCodeGenerator} + * with protected records. + * + * @author Christopher Chianelli + */ +record ProtectedRecord() { + public ProtectedRecord() {} +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/aot/support/RecordPair.java b/spring-beans/src/test/java/org/springframework/beans/factory/aot/support/RecordPair.java new file mode 100644 index 000000000000..3fb65685bfa9 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/aot/support/RecordPair.java @@ -0,0 +1,29 @@ +/* + * Copyright 2024-2024 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.support; + +/** + * Test class for testing {@link org.springframework.aot.generate.ValueCodeGenerator} + * with generic records. + * + * @author Christopher Chianelli + */ +public record RecordPair(Left_ left, Right_ right) { + public static RecordPair of(Left_ left, Right_ right) { + return new RecordPair<>(left, right); + } +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/aot/support/RecordWithObject.java b/spring-beans/src/test/java/org/springframework/beans/factory/aot/support/RecordWithObject.java new file mode 100644 index 000000000000..2ba195345832 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/aot/support/RecordWithObject.java @@ -0,0 +1,27 @@ +/* + * Copyright 2024-2024 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.support; + +import org.springframework.lang.Nullable; + +/** + * Test class for testing {@link org.springframework.aot.generate.ValueCodeGenerator} + * with nested records. + * + * @author Christopher Chianelli + */ +public record RecordWithObject(int id, @Nullable ObjectWithRecord record) {} 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 index 61008f5aa259..d1792121a43c 100644 --- a/spring-core/src/main/java/org/springframework/aot/generate/ValueCodeGenerator.java +++ b/spring-core/src/main/java/org/springframework/aot/generate/ValueCodeGenerator.java @@ -18,7 +18,10 @@ import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; +import java.util.IdentityHashMap; import java.util.List; +import java.util.Set; import org.springframework.javapoet.CodeBlock; import org.springframework.lang.Nullable; @@ -32,8 +35,9 @@ * @since 6.1.2 */ public final class ValueCodeGenerator { + static final String DEFAULT_IDENTIFIER_PREFIX = "$valueCodeGeneratorObject"; - private static final ValueCodeGenerator INSTANCE = new ValueCodeGenerator(ValueCodeGeneratorDelegates.INSTANCES, null); + private static final ValueCodeGenerator INSTANCE = new ValueCodeGenerator(DEFAULT_IDENTIFIER_PREFIX, ValueCodeGeneratorDelegates.INSTANCES, null); private static final CodeBlock NULL_VALUE_CODE_BLOCK = CodeBlock.of("null"); @@ -42,7 +46,13 @@ public final class ValueCodeGenerator { @Nullable private final GeneratedMethods generatedMethods; - private ValueCodeGenerator(List delegates, @Nullable GeneratedMethods generatedMethods) { + private final ThreadLocal generatedCodeDepth = ThreadLocal.withInitial(() -> 0); + private final ThreadLocal> toGenerateObjectSet = ThreadLocal.withInitial(() -> Collections.newSetFromMap(new IdentityHashMap<>())); + + private final String identifierPrefix; + + private ValueCodeGenerator(String identifierPrefix, List delegates, @Nullable GeneratedMethods generatedMethods) { + this.identifierPrefix = identifierPrefix; this.delegates = delegates; this.generatedMethods = generatedMethods; } @@ -72,14 +82,27 @@ public static ValueCodeGenerator with(Delegate... delegates) { */ public static ValueCodeGenerator with(List delegates) { Assert.notEmpty(delegates, "Delegates must not be empty"); - return new ValueCodeGenerator(new ArrayList<>(delegates), null); + return new ValueCodeGenerator(DEFAULT_IDENTIFIER_PREFIX, 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 new ValueCodeGenerator(DEFAULT_IDENTIFIER_PREFIX, allDelegates, this.generatedMethods); + } + + /** + * Return a new {@link ValueCodeGenerator} that uses the given {@link String} as the + * prefix for all of its identifiers. + * @param identifierPrefix the prefix to use for each identifier. Cannot be null or empty. + * @return a new {@link ValueCodeGenerator} with the same delegates and generated methods + * as this {@link ValueCodeGenerator}. + */ + public ValueCodeGenerator withIdentifierPrefix(String identifierPrefix) { + Assert.notNull(identifierPrefix, "Identifier prefix must not be null"); + Assert.state(!identifierPrefix.isEmpty(), "Identifier prefix must not be empty"); + return new ValueCodeGenerator(identifierPrefix, this.delegates, this.generatedMethods); } /** @@ -91,7 +114,7 @@ public ValueCodeGenerator add(List additionalDelegates) { * @return an instance scoped to the specified generated methods */ public ValueCodeGenerator scoped(GeneratedMethods generatedMethods) { - return new ValueCodeGenerator(this.delegates, generatedMethods); + return new ValueCodeGenerator(DEFAULT_IDENTIFIER_PREFIX, this.delegates, generatedMethods); } /** @@ -103,7 +126,13 @@ public CodeBlock generateCode(@Nullable Object value) { if (value == null) { return NULL_VALUE_CODE_BLOCK; } + if (this.toGenerateObjectSet.get().contains(value)) { + throw new ValueCodeGenerationException("Unable to generate code for (" + value + ") because it contains a cyclic reference.", + value, null); + } try { + this.toGenerateObjectSet.get().add(value); + this.generatedCodeDepth.set(this.generatedCodeDepth.get() + 1); for (Delegate delegate : this.delegates) { CodeBlock code = delegate.generateCode(this, value); if (code != null) { @@ -115,8 +144,23 @@ public CodeBlock generateCode(@Nullable Object value) { catch (Exception ex) { throw new ValueCodeGenerationException(value, ex); } + finally { + this.generatedCodeDepth.set(this.generatedCodeDepth.get() - 1); + this.toGenerateObjectSet.get().remove(value); + } } + /** + * Returns a String that can be used as a new identifier in + * a {@link CodeBlock} expression. + * For a given {@link Delegate} call, it will always return the same value. + * That is, each {@link Delegate} can define at most one new variable. + * @return an identifier that can be used in a {@link CodeBlock} that + * is guaranteed to not be used in an upper {@link Delegate} call. + */ + public String getIdentifierForCurrentDepth() { + return this.identifierPrefix + this.generatedCodeDepth.get(); + } /** * Return the {@link GeneratedMethods} that represents the scope 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 index ce4ade2027b6..feb289984f3e 100644 --- a/spring-core/src/main/java/org/springframework/aot/generate/ValueCodeGeneratorDelegates.java +++ b/spring-core/src/main/java/org/springframework/aot/generate/ValueCodeGeneratorDelegates.java @@ -16,10 +16,18 @@ package org.springframework.aot.generate; +import java.lang.reflect.Field; +import java.lang.reflect.InaccessibleObjectException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.RecordComponent; import java.nio.charset.Charset; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.Comparator; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; @@ -28,6 +36,7 @@ import java.util.Set; import java.util.TreeMap; import java.util.TreeSet; +import java.util.function.Supplier; import java.util.stream.Stream; import org.springframework.aot.generate.ValueCodeGenerator.Delegate; @@ -37,6 +46,7 @@ import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; +import org.springframework.util.ReflectionUtils; /** * Code generator {@link Delegate} for well known value types. @@ -59,7 +69,9 @@ public abstract class ValueCodeGeneratorDelegates { *
  • 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}.
  • + *
  • Map via {@code Map.of} or {@code Map.ofEntries},
  • + *
  • Records containing common value types for all its {@link RecordComponent components},
  • + *
  • Objects with public setters for all non-public fields, with the value of each field being a common value type.
  • * * Those implementations do not require the {@link ValueCodeGenerator} to be * {@linkplain ValueCodeGenerator#scoped(GeneratedMethods) scoped}. @@ -74,8 +86,21 @@ public abstract class ValueCodeGeneratorDelegates { new ArrayDelegate(), new ListDelegate(), new SetDelegate(), - new MapDelegate()); - + new MapDelegate(), + new RecordDelegate(), + new ObjectDelegate() + ); + + private static boolean isAccessible(Class type) { + Class currentType = type; + while (currentType != null) { + if (!Modifier.isPublic(currentType.getModifiers())) { + return false; + } + currentType = currentType.getDeclaringClass(); + } + return true; + } /** * Abstract {@link Delegate} for {@code Collection} types. @@ -126,7 +151,6 @@ protected final CodeBlock generateCollectionOf(ValueCodeGenerator valueCodeGener } } - /** * {@link Delegate} for {@link Map} types. */ @@ -188,7 +212,6 @@ private Map orderForCodeConsistency(Map map) { } } - /** * {@link Delegate} for {@code primitive} types. */ @@ -243,7 +266,6 @@ private String escape(char ch) { } } - /** * {@link Delegate} for {@link String} types. */ @@ -259,7 +281,6 @@ public CodeBlock generateCode(ValueCodeGenerator codeGenerator, Object value) { } } - /** * {@link Delegate} for {@link Charset} types. */ @@ -275,7 +296,6 @@ public CodeBlock generateCode(ValueCodeGenerator codeGenerator, Object value) { } } - /** * {@link Delegate} for {@link Enum} types. */ @@ -292,7 +312,6 @@ public CodeBlock generateCode(ValueCodeGenerator codeGenerator, Object value) { } } - /** * {@link Delegate} for {@link Class} types. */ @@ -308,22 +327,11 @@ public CodeBlock generateCode(ValueCodeGenerator codeGenerator, Object value) { } } - /** * {@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); @@ -349,8 +357,16 @@ private static CodeBlock generateCodeWithGenerics(ResolvableType target, Class> { } } - /** * {@link Delegate} for {@link Set} types. */ @@ -415,4 +429,150 @@ private Set orderForCodeConsistency(Set set) { } } + /** + * {@link Delegate} for accessible public {@link Record}. + */ + private static class RecordDelegate implements Delegate { + @Override + public CodeBlock generateCode(ValueCodeGenerator valueCodeGenerator, Object value) { + if (!(value instanceof Record record) || !isAccessible(record.getClass())) { + return null; + } + // Guaranteed to be in the same order as the canonical constructor + RecordComponent[] recordComponents = record.getClass().getRecordComponents(); + // A public record is guaranteed to have a public constructor taking all its components + CodeBlock.Builder builder = CodeBlock.builder(); + builder.add("new $T(", record.getClass()); + for (int i = 0; i < recordComponents.length; i++) { + if (i != 0) { + builder.add(", "); + } + Object componentValue; + try { + componentValue = recordComponents[i].getAccessor().invoke(record); + } + catch (IllegalAccessException | InvocationTargetException ex) { + throw new ValueCodeGenerationException("Unable to generate code for value (" + record + ") since its component (" + recordComponents[i].getName() + ") could not be read.", record, ex); + } + builder.add(valueCodeGenerator.generateCode(componentValue)); + } + builder.add(")"); + return builder.build(); + } + } + + /** + * {@link Delegate} for accessible public {@link Record} and {@link Object} types with a public no-args constructor and + * public setters for all non-public fields. + */ + private static class ObjectDelegate implements Delegate { + private static boolean isNotObjectCommonValueType(Object value) { + return getGeneratedCodeType(value.getClass()) != value.getClass() + || value instanceof Record + || ClassUtils.isSimpleValueType(value.getClass()) + || value.getClass().isArray(); + } + + @Override + public CodeBlock generateCode(ValueCodeGenerator valueCodeGenerator, Object value) { + if (isNotObjectCommonValueType(value) || !isAccessible(value.getClass())) { + // Use a different delegate to generate the code + return null; + } + try { + CodeBlock.Builder builder = CodeBlock.builder(); + + // A trick to get a code block where we can generate statements + // anywhere in an expression (i.e. we can put the generated code as an argument + // to a method): create a supplier lambda and immediately call its + // "get" method. + // We need to cast the lambda to Supplier, so Java will + // know the type of the lambda + // String tryMe = ((Supplier) () -> { return "hi"; }).get(); + builder.add("(($T<$T>) () -> {$>\n", Supplier.class, value.getClass()); + + String identifier = valueCodeGenerator.getIdentifierForCurrentDepth(); + + generateCodeToSetFields(valueCodeGenerator, builder, identifier, + value); + + builder.add("return $N;", identifier); + builder.add("$<\n}).get()"); + return builder.build(); + } + catch (ValueCodeGenerationException ex) { + // If we fail to generate code, return null, since a different Delegate + // might be able to handle this Object + return null; + } + } + + private static Class getGeneratedCodeType(Class type) { + if (Set.class.isAssignableFrom(type)) { + return Set.class; + } + if (List.class.isAssignableFrom(type)) { + return List.class; + } + if (Map.class.isAssignableFrom(type)) { + return Map.class; + } + return type; + } + + private static void generateCodeToSetFields(ValueCodeGenerator valueCodeGenerator, + CodeBlock.Builder builder, + String identifier, + Object value) { + try { + // A Constructor is returned only if it is public; a private no-args + // Constructor will also raise a NoSuchMethodException + value.getClass().getConstructor(); + } + catch (NoSuchMethodException ex) { + throw new ValueCodeGenerationException("Unable to generate code for value (" + value + ") since its class does not have an accessible no-args constructor.", value, ex); + } + builder.add("$T $N = new $T();\n", value.getClass(), identifier, value.getClass()); + // Sort the object's fields, so we have a consistent ordering + List objectFields = new ArrayList<>(); + ReflectionUtils.doWithFields(value.getClass(), objectFields::add); + objectFields.sort(Comparator.comparing(Field::getName)); + + for (Field field : objectFields) { + if (Modifier.isStatic(field.getModifiers())) { + continue; + } + Object fieldValue; + try { + // Set the field to accessible so we can read it + field.setAccessible(true); + // Use Field::get instead of ReflectionUtils::getField, + // since we want to throw a ValueCodeGenerationException + // if the field cannot be read for any reason + fieldValue = field.get(value); + } + catch (InaccessibleObjectException | IllegalAccessException | SecurityException ex) { + throw new ValueCodeGenerationException("Unable to generate code for value (" + value + ") since its field (" + field + ") cannot be read.", + value, ex); + } + if (Modifier.isPublic(field.getModifiers())) { + builder.add("$N.$N = $L;\n", identifier, field.getName(), + valueCodeGenerator.generateCode(fieldValue)); + continue; + } + String methodSuffix = Character.toUpperCase(field.getName().charAt(0)) + field.getName().substring(1); + String setterMethodName = "set" + methodSuffix; + Class expectedParameterType = getGeneratedCodeType(field.getType()); + Method setterMethod = ReflectionUtils.findMethod(value.getClass(), setterMethodName, expectedParameterType); + if (setterMethod == null + || !Modifier.isPublic(setterMethod.getModifiers()) + || Modifier.isStatic(setterMethod.getModifiers())) { + throw new ValueCodeGenerationException("Unable to generate code for value (" + value + ") since its field (" + field + ") is not public and does not have a public setter.", + value, null); + } + builder.add("$N.$N($L);\n", identifier, setterMethodName, + valueCodeGenerator.generateCode(fieldValue)); + } + } + } } 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 index dced0ed7bbf1..751da00ce936 100644 --- a/spring-core/src/test/java/org/springframework/aot/generate/ValueCodeGeneratorTests.java +++ b/spring-core/src/test/java/org/springframework/aot/generate/ValueCodeGeneratorTests.java @@ -29,6 +29,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.function.Supplier; import org.assertj.core.api.AbstractAssert; import org.assertj.core.api.AssertProvider; @@ -48,6 +49,8 @@ import org.springframework.javapoet.JavaFile; import org.springframework.javapoet.TypeSpec; import org.springframework.lang.Nullable; +import org.springframework.tests.sample.objects.TestObject; +import org.springframework.tests.sample.objects.TestRecord; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; @@ -112,6 +115,16 @@ void scopedReturnsImmutableCopy() { assertThat(valueCodeGenerator.getGeneratedMethods()).isNull(); } + @Test + void withIdentifierPrefixReturnsImmutableCopy() { + ValueCodeGenerator valueCodeGenerator = ValueCodeGenerator.withDefaults(); + ValueCodeGenerator withIdentifierPrefixValueCodeGenerator = valueCodeGenerator.withIdentifierPrefix("myPrefix"); + assertThat(withIdentifierPrefixValueCodeGenerator).isNotSameAs(valueCodeGenerator); + assertThat(withIdentifierPrefixValueCodeGenerator.getIdentifierForCurrentDepth()).isEqualTo("myPrefix0"); + assertThat(valueCodeGenerator.getIdentifierForCurrentDepth()).isEqualTo(ValueCodeGenerator.DEFAULT_IDENTIFIER_PREFIX + "0"); + assertThat(valueCodeGenerator.getGeneratedMethods()).isNull(); + } + } @Nested @@ -390,6 +403,84 @@ void generateWhenMapWithOverTenElements() { } + @Nested + class ObjectTests { + @Test + void generateTestObject() { + TestObject testObject = new TestObject("Bob", 12); + assertThat(generateCode(testObject).toString()) + .isEqualTo(CodeBlock.of("(($T<$T>) () -> {$>\n" + + "$T $$valueCodeGeneratorObject1 = new $T();\n" + + "$$valueCodeGeneratorObject1.setAge(12);\n" + + "$$valueCodeGeneratorObject1.setName(\"Bob\");\n" + + "$$valueCodeGeneratorObject1.setSpouse(null);\n" + + "return $$valueCodeGeneratorObject1;$<\n" + + "}).get()", + Supplier.class, TestObject.class, TestObject.class, TestObject.class) + .toString()); + } + + @Test + void generateNestedTestObject() { + TestObject spouse = new TestObject("Ann", 13); + TestObject testObject = new TestObject("Bob", 12); + testObject.setSpouse(spouse); + assertThat(generateCode(testObject).toString()) + .isEqualTo(CodeBlock.of("(($T<$T>) () -> {$>\n" + + "$T $$valueCodeGeneratorObject1 = new $T();\n" + + "$$valueCodeGeneratorObject1.setAge(12);\n" + + "$$valueCodeGeneratorObject1.setName(\"Bob\");\n" + + "$$valueCodeGeneratorObject1.setSpouse((($T<$T>) () -> {$>\n" + + "$T $$valueCodeGeneratorObject2 = new $T();\n" + + "$$valueCodeGeneratorObject2.setAge(13);\n" + + "$$valueCodeGeneratorObject2.setName(\"Ann\");\n" + + "$$valueCodeGeneratorObject2.setSpouse(null);\n" + + "return $$valueCodeGeneratorObject2;$<\n" + + "}).get());\n" + + "return $$valueCodeGeneratorObject1;$<\n" + + "}).get()", + Supplier.class, TestObject.class, TestObject.class, TestObject.class, + Supplier.class, TestObject.class, TestObject.class, TestObject.class) + .toString()); + } + + @Test + void generateTestObjectWithDifferentIdentifierPrefix() { + TestObject testObject = new TestObject("Bob", 12); + assertThat(ValueCodeGenerator.withDefaults() + .withIdentifierPrefix("testObject") + .generateCode(testObject).toString()) + .isEqualTo(CodeBlock.of("(($T<$T>) () -> {$>\n" + + "$T testObject1 = new $T();\n" + + "testObject1.setAge(12);\n" + + "testObject1.setName(\"Bob\");\n" + + "testObject1.setSpouse(null);\n" + + "return testObject1;$<\n" + + "}).get()", + Supplier.class, TestObject.class, TestObject.class, TestObject.class) + .toString()); + } + } + + @Nested + class RecordTests { + @Test + void generateTestRecord() { + TestRecord testRecord = new TestRecord("Bob", 12); + assertThat(resolve(generateCode(testRecord))) + .hasImport(TestRecord.class) + .hasValueCode("new TestRecord(\"Bob\", 12, null)"); + } + + @Test + void generateNestedTestRecord() { + TestRecord testRecord = new TestRecord("Bob", 12, new TestRecord("Ann", 13)); + assertThat(resolve(generateCode(testRecord))) + .hasImport(TestRecord.class) + .hasValueCode("new TestRecord(\"Bob\", 12, new TestRecord(\"Ann\", 13, null))"); + } + } + @Nested class ExceptionTests { diff --git a/spring-core/src/test/java/org/springframework/tests/sample/objects/TestRecord.java b/spring-core/src/test/java/org/springframework/tests/sample/objects/TestRecord.java new file mode 100644 index 000000000000..6b66644a2ff7 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/tests/sample/objects/TestRecord.java @@ -0,0 +1,23 @@ +/* + * Copyright 2024-2024 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.tests.sample.objects; + +public record TestRecord(String name, int age, TestRecord spouse) { + public TestRecord(String name, int age) { + this(name, age, null); + } +}