From 55d7f7a014df12642b100abf17fa65502d307629 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Wed, 13 Apr 2022 18:21:50 -0700 Subject: [PATCH] Add support for generating methods Add `GeneratedMethods` and `GeneratedMethod` support classes which can be used during code generation to generation additional methods that will ultimately be included in a `JavaFile`. See gh-28414 --- .../aot/generate/GeneratedMethod.java | 96 +++++++++++++++++ .../aot/generate/GeneratedMethods.java | 102 ++++++++++++++++++ .../aot/generate/MethodGenerator.java | 72 +++++++++++++ .../aot/generate/MethodGeneratorWithName.java | 71 ++++++++++++ .../aot/generate/GeneratedMethodTests.java | 72 +++++++++++++ .../aot/generate/GeneratedMethodsTests.java | 100 +++++++++++++++++ .../MethodGeneratorWithNameTests.java | 61 +++++++++++ 7 files changed, 574 insertions(+) create mode 100644 spring-core/src/main/java/org/springframework/aot/generate/GeneratedMethod.java create mode 100644 spring-core/src/main/java/org/springframework/aot/generate/GeneratedMethods.java create mode 100644 spring-core/src/main/java/org/springframework/aot/generate/MethodGenerator.java create mode 100644 spring-core/src/main/java/org/springframework/aot/generate/MethodGeneratorWithName.java create mode 100644 spring-core/src/test/java/org/springframework/aot/generate/GeneratedMethodTests.java create mode 100644 spring-core/src/test/java/org/springframework/aot/generate/GeneratedMethodsTests.java create mode 100644 spring-core/src/test/java/org/springframework/aot/generate/MethodGeneratorWithNameTests.java diff --git a/spring-core/src/main/java/org/springframework/aot/generate/GeneratedMethod.java b/spring-core/src/main/java/org/springframework/aot/generate/GeneratedMethod.java new file mode 100644 index 0000000000..253c1779b0 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/aot/generate/GeneratedMethod.java @@ -0,0 +1,96 @@ +/* + * 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.aot.generate; + +import java.util.function.Consumer; + +import org.springframework.javapoet.MethodSpec; +import org.springframework.javapoet.MethodSpec.Builder; +import org.springframework.util.Assert; + +/** + * A generated method. + * + * @author Phillip Webb + * @since 6.0 + * @see GeneratedMethods + * @see MethodGenerator + */ +public final class GeneratedMethod { + + private final String name; + + private MethodSpec spec; + + + /** + * Create a new {@link GeneratedMethod} instance with the given name. This + * constructor is package-private since names should only be generated via a + * {@link GeneratedMethods}. + * @param name the generated name + */ + GeneratedMethod(String name) { + this.name = name; + } + + + /** + * Return the generated name of the method. + * @return the name of the generated method + */ + public String getName() { + return this.name; + } + + /** + * Return the {@link MethodSpec} for this generated method. + * @return the method spec + * @throws IllegalStateException if one of the {@code generateBy(...)} + * methods has not been called + */ + public MethodSpec getSpec() { + Assert.state(this.spec != null, + () -> String.format("Method '%s' has no method spec defined", this.name)); + return this.spec; + } + + /** + * Generate the method using the given consumer. + * @param builder a consumer that will accept a method spec builder and + * configure it as necessary + * @return this instance + */ + public GeneratedMethod using(Consumer builder) { + Builder builderToUse = MethodSpec.methodBuilder(this.name); + builder.accept(builderToUse); + MethodSpec spec = builderToUse.build(); + assertNameHasNotBeenChanged(spec); + this.spec = spec; + return this; + } + + private void assertNameHasNotBeenChanged(MethodSpec spec) { + Assert.isTrue(this.name.toString().equals(spec.name), () -> String + .format("'spec' must use the generated name \"%s\"", this.name)); + } + + @Override + public String toString() { + return (this.spec != null) ? this.spec.toString() : this.name.toString(); + } + +} diff --git a/spring-core/src/main/java/org/springframework/aot/generate/GeneratedMethods.java b/spring-core/src/main/java/org/springframework/aot/generate/GeneratedMethods.java new file mode 100644 index 0000000000..27d391e8dc --- /dev/null +++ b/spring-core/src/main/java/org/springframework/aot/generate/GeneratedMethods.java @@ -0,0 +1,102 @@ +/* + * 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.aot.generate; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.function.Consumer; +import java.util.stream.Stream; + +import org.springframework.javapoet.MethodSpec; +import org.springframework.util.Assert; + +/** + * A managed collection of generated methods. + * + * @author Phillip Webb + * @since 6.0 + * @see GeneratedMethod + */ +public class GeneratedMethods implements Iterable, MethodGenerator { + + private final MethodNameGenerator methodNameGenerator; + + private final List generatedMethods = new ArrayList<>(); + + + /** + * Create a new {@link GeneratedMethods} instance backed by a new + * {@link MethodNameGenerator}. + */ + public GeneratedMethods() { + this(new MethodNameGenerator()); + } + + /** + * Create a new {@link GeneratedMethods} instance backed by the given + * {@link MethodNameGenerator}. + * @param methodNameGenerator the method name generator + */ + public GeneratedMethods(MethodNameGenerator methodNameGenerator) { + Assert.notNull(methodNameGenerator, "'methodNameGenerator' must not be null"); + this.methodNameGenerator = methodNameGenerator; + } + + + @Override + public GeneratedMethod generateMethod(Object... methodNameParts) { + return add(methodNameParts); + } + + /** + * Add a new {@link GeneratedMethod}. The returned instance must define the + * method spec by calling {@code using(builder -> ...)}. + * @param methodNameParts the method name parts that should be used to + * generate a unique method name + * @return the newly added {@link GeneratedMethod} + */ + public GeneratedMethod add(Object... methodNameParts) { + GeneratedMethod method = new GeneratedMethod( + this.methodNameGenerator.generateMethodName(methodNameParts)); + this.generatedMethods.add(method); + return method; + } + + /** + * Call the given action with each of the {@link MethodSpec MethodSpecs} + * that have been added to this collection. + * @param action the action to perform + */ + public void doWithMethodSpecs(Consumer action) { + stream().map(GeneratedMethod::getSpec).forEach(action); + } + + @Override + public Iterator iterator() { + return this.generatedMethods.iterator(); + } + + /** + * Return a {@link Stream} of all the methods in this collection. + * @return a stream of {@link GeneratedMethod} instances + */ + public Stream stream() { + return this.generatedMethods.stream(); + } + +} diff --git a/spring-core/src/main/java/org/springframework/aot/generate/MethodGenerator.java b/spring-core/src/main/java/org/springframework/aot/generate/MethodGenerator.java new file mode 100644 index 0000000000..bb782ecef2 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/aot/generate/MethodGenerator.java @@ -0,0 +1,72 @@ +/* + * 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.aot.generate; + +/** + * Generates new {@link GeneratedMethod} instances. + * + * @author Phillip Webb + * @since 6.0 + * @see GeneratedMethods + */ +@FunctionalInterface +public interface MethodGenerator { + + /** + * Generate a new {@link GeneratedMethod}. The returned instance must define + * the method spec by calling {@code using(builder -> ...)}. + * @param methodNameParts the method name parts that should be used to + * generate a unique method name + * @return the newly added {@link GeneratedMethod} + */ + GeneratedMethod generateMethod(Object... methodNameParts); + + /** + * Return a new {@link MethodGenerator} instance that generates method with + * additional implicit method name parts. The final generated name will be + * of the following form: + *

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
OriginalUpdated
run<name>Run
getValueget<Name>Value
setValueset<Name>Value
isEnabledis<Name>Enabled
+ * @param nameParts the implicit name parts + * @return a new {@link MethodGenerator} instance + */ + default MethodGenerator withName(Object... nameParts) { + return new MethodGeneratorWithName(this, nameParts); + } + +} diff --git a/spring-core/src/main/java/org/springframework/aot/generate/MethodGeneratorWithName.java b/spring-core/src/main/java/org/springframework/aot/generate/MethodGeneratorWithName.java new file mode 100644 index 0000000000..dee36e1894 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/aot/generate/MethodGeneratorWithName.java @@ -0,0 +1,71 @@ +/* + * 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.aot.generate; + +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +/** + * Internal class used to support {@link MethodGenerator#withName(Object...)}. + * + * @author Phillip Webb + * @since 6.0 + */ +class MethodGeneratorWithName implements MethodGenerator { + + private static final String[] PREFIXES = { "get", "set", "is" }; + + private final MethodGenerator methodGenerator; + + private final Object[] nameParts; + + + MethodGeneratorWithName(MethodGenerator methodGenerator, Object[] nameParts) { + this.methodGenerator = methodGenerator; + this.nameParts = nameParts; + } + + + @Override + public GeneratedMethod generateMethod(Object... methodNameParts) { + return this.methodGenerator.generateMethod(generateName(methodNameParts)); + } + + private Object[] generateName(Object... methodNameParts) { + String joined = MethodNameGenerator.join(methodNameParts); + String prefix = getPrefix(joined); + String suffix = joined.substring(prefix.length()); + Object[] result = this.nameParts; + if (StringUtils.hasLength(prefix)) { + result = ObjectUtils.addObjectToArray(result, prefix, 0); + } + if (StringUtils.hasLength(suffix)) { + result = ObjectUtils.addObjectToArray(result, suffix); + } + return result; + } + + private String getPrefix(String name) { + for (String candidate : PREFIXES) { + if (name.startsWith(candidate)) { + return candidate; + } + } + return ""; + } + +} diff --git a/spring-core/src/test/java/org/springframework/aot/generate/GeneratedMethodTests.java b/spring-core/src/test/java/org/springframework/aot/generate/GeneratedMethodTests.java new file mode 100644 index 0000000000..4d7c8f9eb1 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/aot/generate/GeneratedMethodTests.java @@ -0,0 +1,72 @@ +/* + * 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.aot.generate; + +import javax.lang.model.element.Modifier; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * Tests for {@link GeneratedMethod}. + * + * @author Phillip Webb + */ +class GeneratedMethodTests { + + private static final String NAME = "spring"; + + @Test + void getNameReturnsName() { + GeneratedMethod method = new GeneratedMethod(NAME); + assertThat(method.getName()).isSameAs(NAME); + } + + @Test + void getSpecReturnsSpec() { + GeneratedMethod method = new GeneratedMethod(NAME); + method.using(builder -> builder.addJavadoc("Test")); + assertThat(method.getSpec().javadoc.toString()).contains("Test"); + } + + @Test + void getSpecReturnsSpecWhenNoSpecDefinedThrowsException() { + GeneratedMethod method = new GeneratedMethod(NAME); + assertThatIllegalStateException().isThrownBy(() -> method.getSpec()) + .withMessage("Method 'spring' has no method spec defined"); + } + + @Test + void usingAddsSpec() { + GeneratedMethod method = new GeneratedMethod(NAME); + method.using(builder -> builder.addModifiers(Modifier.PUBLIC)); + assertThat(method.getSpec().toString()) + .isEqualToIgnoringNewLines("public void spring() {}"); + } + + @Test + void usingWhenBuilderChanagesNameThrowsException() { + GeneratedMethod method = new GeneratedMethod(NAME); + assertThatIllegalArgumentException() + .isThrownBy(() -> method.using(builder -> builder.setName("badname"))) + .withMessage("'spec' must use the generated name \"spring\""); + } + +} diff --git a/spring-core/src/test/java/org/springframework/aot/generate/GeneratedMethodsTests.java b/spring-core/src/test/java/org/springframework/aot/generate/GeneratedMethodsTests.java new file mode 100644 index 0000000000..6b091eff0c --- /dev/null +++ b/spring-core/src/test/java/org/springframework/aot/generate/GeneratedMethodsTests.java @@ -0,0 +1,100 @@ +/* + * 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.aot.generate; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import org.springframework.javapoet.MethodSpec; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * Tests for {@link GeneratedMethods}. + * + * @author Phillip Webb + */ +class GeneratedMethodsTests { + + private final GeneratedMethods methods = new GeneratedMethods(); + + @Test + void createWhenMethodNameGeneratorIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> new GeneratedMethods(null)) + .withMessage("'methodNameGenerator' must not be null"); + } + + @Test + void createWithExistingGeneratorUsesGenerator() { + MethodNameGenerator generator = new MethodNameGenerator(); + generator.generateMethodName("test"); + GeneratedMethods methods = new GeneratedMethods(generator); + assertThat(methods.add("test").getName()).hasToString("test1"); + } + + @Test + void addAddsMethod() { + this.methods.add("spring", "beans").using(this::build); + this.methods.add("spring", "context").using(this::build); + assertThat( + this.methods.stream().map(GeneratedMethod::getName).map(Object::toString)) + .containsExactly("springBeans", "springContext"); + } + + @Test + void doWithMethodSpecsAcceptsMethodSpecs() { + this.methods.add("spring", "beans").using(this::build); + this.methods.add("spring", "context").using(this::build); + List names = new ArrayList<>(); + this.methods.doWithMethodSpecs(spec -> names.add(spec.name)); + assertThat(names).containsExactly("springBeans", "springContext"); + } + + @Test + void doWithMethodSpecsWhenMethodHasNotHadSpecDefinedThrowsException() { + this.methods.add("spring"); + assertThatIllegalStateException() + .isThrownBy(() -> this.methods.doWithMethodSpecs(spec -> { + })).withMessage("Method 'spring' has no method spec defined"); + } + + @Test + void iteratorIteratesMethods() { + this.methods.add("spring", "beans").using(this::build); + this.methods.add("spring", "context").using(this::build); + Iterator iterator = this.methods.iterator(); + assertThat(iterator.next().getName()).hasToString("springBeans"); + assertThat(iterator.next().getName()).hasToString("springContext"); + assertThat(iterator.hasNext()).isFalse(); + } + + @Test + void streamStreamsMethods() { + this.methods.add("spring", "beans").using(this::build); + this.methods.add("spring", "context").using(this::build); + assertThat(this.methods.stream()).hasSize(2); + } + + private void build(MethodSpec.Builder builder) { + } + +} diff --git a/spring-core/src/test/java/org/springframework/aot/generate/MethodGeneratorWithNameTests.java b/spring-core/src/test/java/org/springframework/aot/generate/MethodGeneratorWithNameTests.java new file mode 100644 index 0000000000..0b4fcaf065 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/aot/generate/MethodGeneratorWithNameTests.java @@ -0,0 +1,61 @@ +/* + * 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.aot.generate; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests {@link MethodGeneratorWithName}. + * + * @author Phillip Webb + * @since 6.0 + */ +class MethodGeneratorWithNameTests { + + private final GeneratedMethods generatedMethods = new GeneratedMethods(); + + @Test + void withNameWhenGeneratingGetMethod() { + GeneratedMethod generateMethod = generatedMethods.withName("my", "bean") + .generateMethod("get", "test"); + assertThat(generateMethod.getName()).hasToString("getMyBeanTest"); + } + + @Test + void withNameWhenGeneratingSetMethod() { + GeneratedMethod generateMethod = generatedMethods.withName("my", "bean") + .generateMethod("set", "test"); + assertThat(generateMethod.getName()).hasToString("setMyBeanTest"); + } + + @Test + void withNameWhenGeneratingIsMethod() { + GeneratedMethod generateMethod = generatedMethods.withName("my", "bean") + .generateMethod("is", "test"); + assertThat(generateMethod.getName()).hasToString("isMyBeanTest"); + } + + @Test + void withNameWhenGeneratingOtherMethod() { + GeneratedMethod generateMethod = generatedMethods.withName("my", "bean") + .generateMethod("test"); + assertThat(generateMethod.getName()).hasToString("myBeanTest"); + } + +}