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
This commit is contained in:
Phillip Webb
2022-04-13 18:21:50 -07:00
parent 16456342f5
commit 55d7f7a014
7 changed files with 574 additions and 0 deletions

View File

@@ -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<MethodSpec.Builder> 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();
}
}

View File

@@ -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<GeneratedMethod>, MethodGenerator {
private final MethodNameGenerator methodNameGenerator;
private final List<GeneratedMethod> 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<MethodSpec> action) {
stream().map(GeneratedMethod::getSpec).forEach(action);
}
@Override
public Iterator<GeneratedMethod> 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<GeneratedMethod> stream() {
return this.generatedMethods.stream();
}
}

View File

@@ -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:
* <p>
* <table border="1">
* <tr>
* <th>Original</th>
* <th>Updated</th>
* </tr>
* <tr>
* <td>run</td>
* <td>&lt;name&gt;Run</td>
* </tr>
* <tr>
* <td>getValue</td>
* <td>get&lt;Name&gt;Value</td>
* </tr>
* <tr>
* <td>setValue</td>
* <td>set&lt;Name&gt;Value</td>
* </tr>
* <tr>
* <td>isEnabled</td>
* <td>is&lt;Name&gt;Enabled</td>
* </tr>
* </table>
* @param nameParts the implicit name parts
* @return a new {@link MethodGenerator} instance
*/
default MethodGenerator withName(Object... nameParts) {
return new MethodGeneratorWithName(this, nameParts);
}
}

View File

@@ -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 "";
}
}